mirror of
https://github.com/nix-community/plasma-manager.git
synced 2024-11-30 01:39:16 +03:00
Add rc2nix.py to replace rc2nix.rb (#256)
This commit is contained in:
parent
19e2925799
commit
565bb9f456
@ -47,8 +47,8 @@
|
|||||||
|
|
||||||
rc2nix = pkgs.writeShellApplication {
|
rc2nix = pkgs.writeShellApplication {
|
||||||
name = "rc2nix";
|
name = "rc2nix";
|
||||||
runtimeInputs = with pkgs; [ ruby ];
|
runtimeInputs = with pkgs; [ python3 ];
|
||||||
text = ''ruby ${script/rc2nix.rb} "$@"'';
|
text = ''python3 ${script/rc2nix.py} "$@"'';
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
224
script/rc2nix.py
Executable file
224
script/rc2nix.py
Executable file
@ -0,0 +1,224 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
|
||||||
|
################################################################################
|
||||||
|
#
|
||||||
|
# This file is part of the package Plasma Manager. It is subject to
|
||||||
|
# the license terms in the LICENSE file found in the top-level
|
||||||
|
# directory of this distribution and at:
|
||||||
|
#
|
||||||
|
# https://github.com/nix-community/plasma-manager
|
||||||
|
#
|
||||||
|
# No part of this package, including this file, may be copied,
|
||||||
|
# modified, propagated, or distributed except according to the terms
|
||||||
|
# contained in the LICENSE file.
|
||||||
|
#
|
||||||
|
################################################################################
|
||||||
|
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import List, Dict, Callable, Optional, Tuple
|
||||||
|
|
||||||
|
# The root directory where configuration files are stored.
|
||||||
|
XDG_CONFIG_HOME: str = os.path.expanduser(os.getenv("XDG_CONFIG_HOME", "~/.config"))
|
||||||
|
|
||||||
|
class Rc2Nix:
|
||||||
|
# Files that we'll scan by default.
|
||||||
|
KNOWN_FILES: List[str] = [os.path.join(XDG_CONFIG_HOME, f) for f in [
|
||||||
|
"kcminputrc",
|
||||||
|
"kglobalshortcutsrc",
|
||||||
|
"kactivitymanagerdrc",
|
||||||
|
"ksplashrc",
|
||||||
|
"kwin_rules_dialogrc",
|
||||||
|
"kmixrc",
|
||||||
|
"kwalletrc",
|
||||||
|
"kgammarc",
|
||||||
|
"krunnerrc",
|
||||||
|
"klaunchrc",
|
||||||
|
"plasmanotifyrc",
|
||||||
|
"systemsettingsrc",
|
||||||
|
"kscreenlockerrc",
|
||||||
|
"kwinrulesrc",
|
||||||
|
"khotkeysrc",
|
||||||
|
"ksmserverrc",
|
||||||
|
"kded5rc",
|
||||||
|
"plasmarc",
|
||||||
|
"kwinrc",
|
||||||
|
"kdeglobals",
|
||||||
|
"baloofilerc",
|
||||||
|
"dolphinrc",
|
||||||
|
"klipperrc",
|
||||||
|
"plasma-localerc",
|
||||||
|
"kxkbrc",
|
||||||
|
"ffmpegthumbsrc",
|
||||||
|
"kservicemenurc",
|
||||||
|
"kiorc",
|
||||||
|
]]
|
||||||
|
|
||||||
|
class RcFile:
|
||||||
|
# Any group that matches a listed regular expression is blocked
|
||||||
|
GROUP_BLOCK_LIST: List[str] = [
|
||||||
|
r"^(ConfigDialog|FileDialogSize|ViewPropertiesDialog|KPropertiesDialog)$",
|
||||||
|
r"^\$Version$",
|
||||||
|
r"^ColorEffects:",
|
||||||
|
r"^Colors:",
|
||||||
|
r"^DoNotDisturb$",
|
||||||
|
r"^LegacySession:",
|
||||||
|
r"^MainWindow$",
|
||||||
|
r"^PlasmaViews",
|
||||||
|
r"^ScreenConnectors$",
|
||||||
|
r"^Session:",
|
||||||
|
]
|
||||||
|
|
||||||
|
# Similar to the GROUP_BLOCK_LIST but for setting keys.
|
||||||
|
KEY_BLOCK_LIST: List[str] = [
|
||||||
|
r"^activate widget \d+$", # Depends on state :(
|
||||||
|
r"^ColorScheme(Hash)?$",
|
||||||
|
r"^History Items",
|
||||||
|
r"^LookAndFeelPackage$",
|
||||||
|
r"^Recent (Files|URLs)",
|
||||||
|
r"^Theme$i",
|
||||||
|
r"^Version$",
|
||||||
|
r"State$",
|
||||||
|
r"Timestamp$",
|
||||||
|
]
|
||||||
|
|
||||||
|
# List of functions that get called with a group name and a key name.
|
||||||
|
BLOCK_LIST_LAMBDA: List[Callable[[str, str], bool]] = [
|
||||||
|
lambda group, key: group == "org.kde.kdecoration2" and key == "library"
|
||||||
|
]
|
||||||
|
|
||||||
|
def __init__(self, file_name: str):
|
||||||
|
self.file_name: str = file_name
|
||||||
|
self.settings: Dict[str, Dict[str, str]] = {}
|
||||||
|
self.last_group: Optional[str] = None
|
||||||
|
|
||||||
|
def parse(self):
|
||||||
|
|
||||||
|
def is_group_line(line: str) -> bool:
|
||||||
|
return re.match(r'^\s*(\[[^\]]+\]){1,}\s*$', line) is not None
|
||||||
|
|
||||||
|
def is_setting_line(line: str) -> bool:
|
||||||
|
return re.match(r'^\s*([^=]+)=?(.*)\s*$', line) is not None
|
||||||
|
|
||||||
|
def parse_group(line: str) -> str:
|
||||||
|
return re.sub(r'\s*\[([^\]]+)\]\s*', r'\1/', line.replace("/", "\\\\/")).rstrip("/")
|
||||||
|
|
||||||
|
def parse_setting(line: str) -> Tuple[str, str]:
|
||||||
|
match = re.match(r'^\s*([^=]+)=?(.*)\s*$', line)
|
||||||
|
if match:
|
||||||
|
return match.groups() #type: ignore
|
||||||
|
raise Exception(f"{self.file_name}: can't parse setting line: {line}")
|
||||||
|
|
||||||
|
with open(self.file_name, 'r') as file:
|
||||||
|
for line in file:
|
||||||
|
line = line.strip()
|
||||||
|
if not line:
|
||||||
|
continue
|
||||||
|
if is_group_line(line):
|
||||||
|
self.last_group = parse_group(line)
|
||||||
|
elif is_setting_line(line):
|
||||||
|
key, val = parse_setting(line)
|
||||||
|
self.process_setting(key, val)
|
||||||
|
else:
|
||||||
|
raise Exception(f"{self.file_name}: can't parse line: {line}")
|
||||||
|
|
||||||
|
def process_setting(self, key: str, val: str):
|
||||||
|
|
||||||
|
def should_skip_group(group: str) -> bool:
|
||||||
|
return any(re.match(reg, group) for reg in self.GROUP_BLOCK_LIST)
|
||||||
|
|
||||||
|
def should_skip_key(key: str) -> bool:
|
||||||
|
return any(re.match(reg, key) for reg in self.KEY_BLOCK_LIST)
|
||||||
|
|
||||||
|
def should_skip_by_lambda(group: str, key: str) -> bool:
|
||||||
|
return any(fn(group, key) for fn in self.BLOCK_LIST_LAMBDA)
|
||||||
|
|
||||||
|
key = key.strip()
|
||||||
|
val = val.strip()
|
||||||
|
|
||||||
|
if self.last_group is None:
|
||||||
|
raise Exception(f"{self.file_name}: setting outside of group: {key}={val}")
|
||||||
|
|
||||||
|
if should_skip_group(self.last_group) or should_skip_key(key) or should_skip_by_lambda(self.last_group, key):
|
||||||
|
return
|
||||||
|
|
||||||
|
if self.last_group not in self.settings:
|
||||||
|
self.settings[self.last_group] = {}
|
||||||
|
self.settings[self.last_group][key] = val
|
||||||
|
|
||||||
|
class App:
|
||||||
|
def __init__(self, args: List[str]):
|
||||||
|
self.files: List[str] = Rc2Nix.KNOWN_FILES.copy()
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
settings: Dict[str, Dict[str, Dict[str, str]]] = {}
|
||||||
|
|
||||||
|
for file in self.files:
|
||||||
|
if not os.path.exists(file):
|
||||||
|
continue
|
||||||
|
|
||||||
|
rc = Rc2Nix.RcFile(file)
|
||||||
|
rc.parse()
|
||||||
|
|
||||||
|
path = Path(file).relative_to(XDG_CONFIG_HOME)
|
||||||
|
settings[str(path)] = rc.settings
|
||||||
|
|
||||||
|
self.print_output(settings)
|
||||||
|
|
||||||
|
def print_output(self, settings: Dict[str, Dict[str, Dict[str, str]]]):
|
||||||
|
print("{")
|
||||||
|
print(" programs.plasma = {")
|
||||||
|
print(" enable = true;")
|
||||||
|
print(" shortcuts = {")
|
||||||
|
print(self.pp_shortcuts(settings.get("kglobalshortcutsrc", {}), 6))
|
||||||
|
print(" };")
|
||||||
|
print(" configFile = {")
|
||||||
|
print(self.pp_settings(settings, 6))
|
||||||
|
print(" };")
|
||||||
|
print(" };")
|
||||||
|
print("}")
|
||||||
|
|
||||||
|
def pp_settings(self, settings: Dict[str, Dict[str, Dict[str, str]]], indent: int) -> str:
|
||||||
|
result : List[str] = []
|
||||||
|
for file in sorted(settings.keys()):
|
||||||
|
if file != "kglobalshortcutsrc":
|
||||||
|
for group in sorted(settings[file].keys()):
|
||||||
|
for key in sorted(settings[file][group].keys()):
|
||||||
|
if key != "_k_friendly_name":
|
||||||
|
result.append(f"{' ' * indent}\"{file}\".\"{group}\".\"{key}\" = {nix_val(settings[file][group][key])};")
|
||||||
|
return "\n".join(result)
|
||||||
|
|
||||||
|
def pp_shortcuts(self, groups: Dict[str, Dict[str, str]], indent: int) -> str:
|
||||||
|
if not groups:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
result : List[str] = []
|
||||||
|
for group in sorted(groups.keys()):
|
||||||
|
for action in sorted(groups[group].keys()):
|
||||||
|
if action != "_k_friendly_name":
|
||||||
|
keys = groups[group][action].split(r'(?<!\\),')[0].replace(r'\?', ',').replace(r'\t', '\t').split('\t')
|
||||||
|
|
||||||
|
if not keys or keys[0] == "none":
|
||||||
|
keys_str = "[ ]"
|
||||||
|
elif len(keys) > 1:
|
||||||
|
keys_str = f"[{' '.join(nix_val(k.rstrip(',')) for k in keys)}]"
|
||||||
|
else:
|
||||||
|
ks = keys[0].split(",")
|
||||||
|
k = ks[0] if len(ks) == 3 and ks[0] == ks[1] else keys[0]
|
||||||
|
keys_str = "[ ]" if k == "" or k == "none" else nix_val(k.rstrip(","))
|
||||||
|
|
||||||
|
result.append(f"{' ' * indent}\"{group}\".\"{action}\" = {keys_str};")
|
||||||
|
return "\n".join(result)
|
||||||
|
|
||||||
|
def nix_val(s: Optional[str]) -> str:
|
||||||
|
if s is None:
|
||||||
|
return "null"
|
||||||
|
if re.match(r'^true|false$', s, re.IGNORECASE):
|
||||||
|
return s.lower()
|
||||||
|
if re.match(r'^[0-9]+(\.[0-9]+)?$', s):
|
||||||
|
return s
|
||||||
|
return '"' + re.sub(r'(?<!\\)"', r'\\"', s) + '"'
|
||||||
|
|
||||||
|
Rc2Nix.App(sys.argv[1:]).run()
|
16
test/rc2nix/test_data/kcminputrc
Normal file
16
test/rc2nix/test_data/kcminputrc
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
[Keyboard]
|
||||||
|
KeyboardRepeating=0
|
||||||
|
NumLock=2
|
||||||
|
RepeatDelay=250
|
||||||
|
RepeatRate=30
|
||||||
|
|
||||||
|
[Mouse]
|
||||||
|
X11LibInputXAccelProfileFlat=true
|
||||||
|
XLbInptPointerAcceleration=1
|
||||||
|
cursorTheme=Oxygen_White
|
||||||
|
|
||||||
|
[Tmp]
|
||||||
|
update_info=delete_cursor_old_default_size.upd:DeleteCursorOldDefaultSize,kcminputrc_repeat.upd:kcminputrc_migrate_repeat_value
|
||||||
|
|
||||||
|
[Libinput][2][14][ETPS/2 Elantech Touchpad]
|
||||||
|
NaturalScroll=true
|
9
test/rc2nix/test_data/kglobalshortcutsrc
Normal file
9
test/rc2nix/test_data/kglobalshortcutsrc
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
[kwin]
|
||||||
|
Switch to Desktop 1=Meta+1,,
|
||||||
|
Switch to Desktop 2=Meta+2,,
|
||||||
|
Switch to Desktop 3=Meta+3,,
|
||||||
|
Switch to Desktop 4=Meta+4,,
|
||||||
|
Switch to Desktop 5=Meta+5,,
|
||||||
|
Switch to Desktop 6=Meta+6,,
|
||||||
|
Switch to Desktop 7=Meta+7,,
|
||||||
|
Switch to Desktop 8=Meta+8,,
|
17
test/rc2nix/test_data/kglobalshortcutsrc.bak
Normal file
17
test/rc2nix/test_data/kglobalshortcutsrc.bak
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
[kwin]
|
||||||
|
Switch to Desktop 1=Meta+1,,
|
||||||
|
Switch to Desktop 2=Meta+2,,
|
||||||
|
Switch to Desktop 3=Meta+3,,
|
||||||
|
Switch to Desktop 4=Meta+4,,
|
||||||
|
Switch to Desktop 5=Meta+5,,
|
||||||
|
Switch to Desktop 6=Meta+6,,
|
||||||
|
Switch to Desktop 7=Meta+7,,
|
||||||
|
Switch to Desktop 8=Meta+8,,
|
||||||
|
Window to Desktop 1=Meta+!,,
|
||||||
|
Window to Desktop 2=Meta+",,
|
||||||
|
Window to Desktop 3=Meta+#,,
|
||||||
|
Window to Desktop 4=Meta+¤,,
|
||||||
|
Window to Desktop 5=Meta+%,,
|
||||||
|
Window to Desktop 6=Meta+&,,
|
||||||
|
Window to Desktop 7=Meta+/,,
|
||||||
|
Window to Desktop 8=Meta+(,,
|
2
test/rc2nix/test_data/krunnerrc
Normal file
2
test/rc2nix/test_data/krunnerrc
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
[General]
|
||||||
|
FreeFloating=true
|
6
test/rc2nix/test_data/kscreenlockerrc
Normal file
6
test/rc2nix/test_data/kscreenlockerrc
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
[Greeter]
|
||||||
|
WallpaperPlugin=org.kde.potd
|
||||||
|
|
||||||
|
[Greeter][Wallpaper][org.kde.potd][General]
|
||||||
|
Provider=bing
|
||||||
|
UpdateOverMeteredConnection=0
|
9
test/rc2nix/test_data/kwinrc
Normal file
9
test/rc2nix/test_data/kwinrc
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
[Desktops]
|
||||||
|
Number=8
|
||||||
|
Rows=2
|
||||||
|
|
||||||
|
[Effect-overview]
|
||||||
|
BorderActivate=9
|
||||||
|
|
||||||
|
[Plugins]
|
||||||
|
shakecursorEnabled=true
|
39
test/rc2nix/test_rc2nix.py
Executable file
39
test/rc2nix/test_rc2nix.py
Executable file
@ -0,0 +1,39 @@
|
|||||||
|
#!/usr/bin/env nix
|
||||||
|
#! nix shell nixpkgs#python3Packages.python nixpkgs#ruby -c python3
|
||||||
|
import unittest
|
||||||
|
import subprocess
|
||||||
|
import os
|
||||||
|
|
||||||
|
def red(s: str) -> str:
|
||||||
|
return '\033[91m' + s + '\033[0m'
|
||||||
|
|
||||||
|
def green(s: str) -> str:
|
||||||
|
return '\033[32m' + s + '\033[0m'
|
||||||
|
|
||||||
|
def gray(s: str) -> str:
|
||||||
|
return '\033[90m' + s + '\033[0m'
|
||||||
|
|
||||||
|
current_dir = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
def path(relative_path: str) -> str:
|
||||||
|
return os.path.abspath(os.path.join(current_dir, relative_path))
|
||||||
|
|
||||||
|
rc2nix_py = path("../../script/rc2nix.py")
|
||||||
|
rc2nix_rb = path("../../script/rc2nix.rb")
|
||||||
|
|
||||||
|
class TestRc2nix (unittest.TestCase):
|
||||||
|
|
||||||
|
def test(self):
|
||||||
|
def run_script(*command: str) -> str:
|
||||||
|
rst = subprocess.run(command, env={'XDG_CONFIG_HOME': path('./test_data'), 'PATH': os.environ["PATH"]}, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
|
||||||
|
print(red(rst.stderr))
|
||||||
|
rst.check_returncode()
|
||||||
|
return rst.stdout
|
||||||
|
|
||||||
|
rst_py = run_script(rc2nix_py)
|
||||||
|
rst_rb = run_script(rc2nix_rb)
|
||||||
|
|
||||||
|
self.assertEqual(rst_py.splitlines(), rst_rb.splitlines())
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__': # pragma: no cover
|
||||||
|
_ = unittest.main()
|
Loading…
Reference in New Issue
Block a user