-
Notifications
You must be signed in to change notification settings - Fork 0
/
dconf_manager.py
executable file
·207 lines (170 loc) · 6.81 KB
/
dconf_manager.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
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
#!/usr/bin/env python3
from __future__ import annotations
import argparse
import configparser
import posixpath
import subprocess
from collections.abc import Sequence
from typing import Generic
from typing import TypeVar
from typing import cast
IGNORED = "\033[38;5;244m? "
REMOVE = "\033[31m< "
ADD = "\033[32m> "
def format_kv(section: str, option: str, value: str) -> str:
return posixpath.join(section, option) + "=" + value
T = TypeVar("T")
class HierarchicalSet(Generic[T]):
"""A set of paths [a, b, c, ...]
The root is represented by [].
Adding a path X causes all paths starting with X to be part of the set.
For example, adding "a" causes "a/b" to be a member.
However, adding "a/b" does not cause "a" to be a member.
"""
def __init__(self) -> None:
"""Create an empty set"""
self._children: dict[T, HierarchicalSet[T]] | None = {}
# Map from child to HierarchicalSet.
# If empty, this set is empty.
# If None, this set contains all children.
def _add(self, path: Sequence[T], i: int) -> None:
if self._children is not None:
if i < len(path):
if path[i] not in self._children:
self._children[path[i]] = HierarchicalSet()
self._children[path[i]]._add(path, i + 1)
else:
# we have everything under here
self._children = None
def add(self, path: Sequence[T]) -> None:
self._add(path, 0)
def __contains__(self, path: Sequence[T], i: int = 0) -> bool:
"""Check if everything in the given path is in this set
Empty list means to check if the entirety of this node is in the set
"""
if self._children is None:
return True
elif i < len(path):
if path[i] in self._children:
return self._children[path[i]].__contains__(path, i + 1)
else:
return False
else:
return self._children is None
def _expand_tree(self) -> Sequence[tuple[int, T | None]]:
if self._children is None:
return [(0, None)]
else:
result: list[tuple[int, T | None]] = []
for k, v in sorted(self._children.items()):
result.append((0, k))
for level, item in v._expand_tree():
result.append((1 + level, item))
return result
def __str__(self) -> str:
parts = []
for level, item in self._expand_tree():
if item is None:
parts.append(" " * level + "*")
else:
parts.append(" " * level + str(item))
return "\n".join(parts)
def dconf_dump(root: str) -> str:
output: bytes = subprocess.check_output(["dconf", "dump", root])
return output.decode()
def dconf_write(key: str, value: str) -> None:
subprocess.check_call(["dconf", "write", key, value])
def dconf_reset(key: str) -> None:
subprocess.check_call(["dconf", "reset", key])
class ConfigParser(configparser.ConfigParser):
def __init__(self) -> None:
super().__init__(interpolation=None)
def optionxform(self, optionstr: str) -> str:
return optionstr
def main(argv: Sequence[str] | None) -> None:
parser = argparse.ArgumentParser(
description="Tool for managing dconf settings",
)
parser.add_argument(
"-a",
"--apply",
action=argparse.BooleanOptionalAction,
help="if not passed, only show a diff",
)
parser.add_argument("config", type=open, nargs="+", help="INI files to load")
parser.add_argument(
"--root", default="/", help="all actions will be relative to this root"
)
parser.add_argument(
"-i",
"--show-ignored",
action=argparse.BooleanOptionalAction,
help="if true, print unmanaged options",
)
args = parser.parse_args(argv)
root = args.root
dconf_output = dconf_dump(root)
dconf_config = ConfigParser()
dconf_config.read_string(dconf_output)
def write(section: str, option: str, value: str, apply: bool) -> None:
key = posixpath.join(root, section, option)
print(ADD + format_kv(section, option, value))
if apply:
dconf_write(key, value)
def reset(section: str, option: str, value: str, apply: bool) -> None:
key = posixpath.join(root, section, option)
print(REMOVE + format_kv(section, option, value))
if apply:
dconf_reset(key)
desired_config = ConfigParser()
for f in args.config:
desired_config.read_file(f)
f.close()
# excluded sections override managed sections
managed_sections = HierarchicalSet[str]()
excluded_sections = HierarchicalSet[str]()
for section in desired_config:
if section.startswith("-"):
excluded_sections.add(section[1:].split("/"))
else:
managed_sections.add(section.split("/"))
sections_union = sorted(
set(dconf_config.keys())
| {k for k in desired_config.keys() if not k.startswith("-")}
)
for section in sections_union:
section_parts = section.split("/")
if section_parts in excluded_sections or section_parts not in managed_sections:
# Section is not managed at all.
if args.show_ignored:
for option, value in dconf_config[section].items():
print(IGNORED + format_kv(section, option, value))
elif section not in dconf_config:
# Adding a new section.
for option, value in desired_config[section].items():
write(section, option, value, args.apply)
else:
# Section is present and managed, so diff at option level.
dconf_section = dconf_config[section]
# But it might be managed at a higher level, so it might not be in desired_config.
# In that case we'll end up resetting everything.
desired_section = (
desired_config[section]
if section in desired_config
else cast(dict[str, str], {})
)
for option in sorted(
set(dconf_section.keys()) | set(desired_section.keys())
):
if option not in dconf_section:
write(section, option, desired_section[option], args.apply)
elif option not in desired_section:
reset(section, option, dconf_section[option], args.apply)
elif dconf_section[option] != desired_section[option]:
reset(section, option, dconf_section[option], False)
write(section, option, desired_section[option], args.apply)
else:
# option is equal, do nothing
pass
if __name__ == "__main__":
main(None)