-
Notifications
You must be signed in to change notification settings - Fork 0
/
firefox_profiles.py
executable file
·147 lines (117 loc) · 4.07 KB
/
firefox_profiles.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
#!/usr/bin/env python3
import json
from configparser import ConfigParser
from dataclasses import dataclass, field
from itertools import starmap
from operator import itemgetter as get
from pathlib import Path
from multisn8 import sh
from collections.abc import Callable
from typing import TypeAlias, Self
ROOT = Path.home() / ".mozilla" / "firefox"
INDEX = ROOT / "profiles.ini"
class Config(dict):
def to_userjs(self) -> str:
ser = json.dumps
calls = []
for name, value in self.items():
calls.append(f"user_pref({ser(name)}, {ser(value)})")
return "\n".join(calls)
@dataclass
class Profile:
# User-facing profile identifier.
name: str
# Extra custom configuration (to be) set in about:config.
config: Config = field(default_factory=Config)
# Actual on-disk folder name of the profile, or None if not created.
path: str | None = None
def __post_init__(self):
# https://docs.python.org/3/library/dataclasses.html#post-init-processing
if "/" in self.name:
raise RuntimeError(
f"`{name}` contains slashes which would break firefox. "
"Don't use slashes in profile names."
)
@property
def root(self):
return ROOT / self.path
@property
def userjs(self):
return self.root / "user.js"
def create(self):
"""
Creates the profile by telling firefox about it.
This does not configure it and not set `path`.
This is done by the manager, usually.
"""
sh("firefox", "-createprofile", self.name)
def configure(self):
"""
Creates the profile by telling firefox about it.
It has to exist already and `path` has to be set.
"""
assert self.path is not None
self.userjs.write_text(self.config.to_userjs())
@dataclass
class Manager:
profiles: dict[str, Profile] = field(default_factory=dict)
@classmethod
def default(cls) -> Self:
allow_login = lambda urls: {
"privacy.clearOnShutdown_v2.cookies": False,
"privacy.clearOnShutdown_v2.siteSettings": False,
}
homepage = lambda url: {
"browser.startup.homepage": url,
}
only = lambda url: (allow_login([only]) | homepage(url))
all = dict(
fedi=only("https://mastodon.catgirl.cloud"),
fedi2={},
discord=only("https://discord.com"),
git=allow_login(["https://github.com", "https://gitlab.com"]),
spotify=only("https://open.spotify.com"),
zulip={},
# used by e.g. typst-preview or wled
_app={},
)
all = zip(
all.keys(),
starmap(
lambda name, config: Profile(name=name, config=Config(config)),
all.items(),
),
)
return cls(profiles=dict(all))
def load(self, index=INDEX):
"""
Loads from disk from the index path
and populates this manager with the current profiles,
notably also setting `Profile.path`.
Note that configurations are not replaced.
"""
root = ConfigParser()
root.read(index)
for section in root.values():
# OT: wonder how an entry api for python could look like
if "name" not in section:
continue
name, path = get("name", "path")(section)
if name in self.profiles:
self.profiles[name].path = path
else:
# FIXME: also load existing cfg overrides
self.profiles[name] = Profile(name=name, path=path)
def write(self):
"""Writes to disk and applies configuration overrides."""
self.forall(Profile.create)
self.load()
self.forall(Profile.configure)
def forall(self, op: Callable[[Profile], None]):
"""Calls `op` over all profiles."""
for profile in self.profiles.values():
op(profile)
def main():
Manager.default().write()
if __name__ == "__main__":
main()