summaryrefslogtreecommitdiffstatshomepage
path: root/dot_local
diff options
context:
space:
mode:
authorsommerfeld <sommerfeld@sommerfeld.dev>2026-09-17 15:05:36 +0100
committersommerfeld <sommerfeld@sommerfeld.dev>2026-09-17 15:05:36 +0100
commitaff435a0fb3a56b1d7b738c7dede363440884b3a (patch)
tree07acb41d37760aa169ee42dbeb69da3621cc7489 /dot_local
parenta7f74c1fa4b89c5829f5896842eb7248c820704c (diff)
downloaddotfiles-aff435a0fb3a56b1d7b738c7dede363440884b3a.tar.gz
dotfiles-aff435a0fb3a56b1d7b738c7dede363440884b3a.tar.bz2
dotfiles-aff435a0fb3a56b1d7b738c7dede363440884b3a.zip
Add the Canonical laptop profile and setup workflows
Diffstat (limited to 'dot_local')
-rw-r--r--dot_local/bin/executable_dictate4
-rwxr-xr-xdot_local/bin/executable_linkhandler3
-rw-r--r--dot_local/bin/executable_ocr3
-rwxr-xr-xdot_local/bin/executable_record3
-rwxr-xr-xdot_local/bin/executable_wqr8
-rw-r--r--dot_local/lib/dotfiles/canonical_desktop.py201
-rw-r--r--dot_local/lib/dotfiles/record.py97
7 files changed, 317 insertions, 2 deletions
diff --git a/dot_local/bin/executable_dictate b/dot_local/bin/executable_dictate
index 7ad80bb..3cd763e 100644
--- a/dot_local/bin/executable_dictate
+++ b/dot_local/bin/executable_dictate
@@ -77,7 +77,9 @@ stop_and_transcribe() {
fi
printf '%s' "$text" | wl-copy
- wtype -- "$text"
+ if [ "$(cat "$HOME/.config/dotfiles/role" 2>/dev/null)" != canonical ]; then
+ wtype -- "$text"
+ fi
notify-send -t 2500 "🎙️ Dictated" "$text"
}
diff --git a/dot_local/bin/executable_linkhandler b/dot_local/bin/executable_linkhandler
index e44dcc1..1ca006f 100755
--- a/dot_local/bin/executable_linkhandler
+++ b/dot_local/bin/executable_linkhandler
@@ -1,4 +1,7 @@
#!/usr/bin/env dash
+if [ "$(cat "$HOME/.config/dotfiles/role" 2>/dev/null)" = canonical ]; then
+ exec /usr/bin/xdg-open "$@"
+fi
resolve_url() {
if [ -f "$1" ]; then
diff --git a/dot_local/bin/executable_ocr b/dot_local/bin/executable_ocr
index aeadb51..d770911 100644
--- a/dot_local/bin/executable_ocr
+++ b/dot_local/bin/executable_ocr
@@ -21,6 +21,9 @@ if [ "${1:-}" ]; then
}
text="$(tesseract "$1" - -l "$lang" 2>/dev/null || true)"
else
+ if [ "$(cat "$HOME/.config/dotfiles/role" 2>/dev/null)" = canonical ]; then
+ exec flatpak run com.github.dynobo.normcap
+ fi
region="$(slurp 2>/dev/null)" || exit 0
text="$(grim -g "$region" - | tesseract - - -l "$lang" 2>/dev/null || true)"
fi
diff --git a/dot_local/bin/executable_record b/dot_local/bin/executable_record
index ac88771..2601f9d 100755
--- a/dot_local/bin/executable_record
+++ b/dot_local/bin/executable_record
@@ -1,4 +1,7 @@
#!/usr/bin/env dash
+if [ "$(cat "$HOME/.config/dotfiles/role" 2>/dev/null)" = canonical ]; then
+ exec /usr/bin/python3 "$HOME/.local/lib/dotfiles/record.py" "$@"
+fi
pid_file="/tmp/recordpid"
log_file="/tmp/record.log"
diff --git a/dot_local/bin/executable_wqr b/dot_local/bin/executable_wqr
index 5f9d36d..b4cc123 100755
--- a/dot_local/bin/executable_wqr
+++ b/dot_local/bin/executable_wqr
@@ -8,4 +8,10 @@ else
text="$1"
fi
-printf '%s' "$text" | qrencode -t PNG -o - | imv -
+if [ "$(cat "$HOME/.config/dotfiles/role" 2>/dev/null)" = canonical ]; then
+ image=$(mktemp "${XDG_RUNTIME_DIR:?}/dotfiles-qr-XXXXXX.png")
+ printf '%s' "$text" | qrencode -t PNG -o "$image"
+ /usr/bin/xdg-open "$image"
+else
+ printf '%s' "$text" | qrencode -t PNG -o - | imv -
+fi
diff --git a/dot_local/lib/dotfiles/canonical_desktop.py b/dot_local/lib/dotfiles/canonical_desktop.py
new file mode 100644
index 0000000..fc9e946
--- /dev/null
+++ b/dot_local/lib/dotfiles/canonical_desktop.py
@@ -0,0 +1,201 @@
+"""Apply owned GNOME keys and restore their previous values."""
+
+import importlib
+import json
+import os
+import shlex
+import sys
+from pathlib import Path
+
+HOME = Path.home()
+STATE = HOME / ".local/state/dotfiles/gnome-settings.json"
+EXTENSIONS = [
+ "paperwm@paperwm.github.com",
+ "copyous@boerdereinar.dev",
+ "emoji-copy@felipeftn",
+]
+
+
+def save_state(saved: dict) -> None:
+ STATE.parent.mkdir(parents=True, exist_ok=True)
+ temporary = STATE.with_suffix(".tmp")
+ temporary.write_text(json.dumps(saved, indent=2) + "\n")
+ temporary.chmod(0o600)
+ temporary.replace(STATE)
+
+
+def settings_object(schema: str, path: str | None = None):
+ gio = importlib.import_module("gi.repository.Gio")
+ source = gio.SettingsSchemaSource.get_default()
+ for directory in (HOME / ".local/share/gnome-shell/extensions").glob("*/schemas"):
+ if (directory / "gschemas.compiled").exists():
+ source = gio.SettingsSchemaSource.new_from_directory(
+ str(directory), source, False
+ )
+ definition = source.lookup(schema, True)
+ if definition is None:
+ print(
+ f"Missing schema: {schema}. Install extensions, log in again, then retry."
+ )
+ return None
+ return gio.Settings.new_full(definition, None, path)
+
+
+def write_key(
+ schema: str, key: str, value, saved: dict, path: str | None = None
+) -> None:
+ settings = settings_object(schema, path)
+ if settings is None:
+ return
+ if key not in settings.props.settings_schema.list_keys():
+ raise RuntimeError(f"Unknown setting: {schema} {key}")
+ if not settings.is_writable(key):
+ print(f"Locked by policy: {schema} {key}")
+ return
+ glib = importlib.import_module("gi.repository.GLib")
+ variant = glib.Variant(settings.get_value(key).get_type_string(), value)
+ name = json.dumps([schema, key, path])
+ if name not in saved:
+ previous = settings.get_user_value(key)
+ saved[name] = {
+ "before": previous.print_(True) if previous is not None else None
+ }
+ saved[name]["applied"] = variant.print_(True)
+ save_state(saved)
+ if not settings.set_value(key, variant):
+ raise RuntimeError(f"Cannot set {schema} {key}")
+
+
+def merge_key(schema: str, key: str, values: list[str], saved: dict) -> None:
+ settings = settings_object(schema)
+ if settings is not None:
+ write_key(
+ schema, key, list(dict.fromkeys([*settings.get_strv(key), *values])), saved
+ )
+
+
+def shortcuts(saved: dict) -> None:
+ shell = shlex.quote(str(HOME / ".nix-profile/bin/zsh"))
+ actions = {
+ "terminal": ("<Super>Return", "/snap/bin/ghostty"),
+ "files": ("<Super><Shift>Return", f"/snap/bin/ghostty -e {shell} -lc yazi"),
+ "browser": ("<Super><Shift>b", "/snap/bin/firefox"),
+ "mail": ("<Super>t", "/snap/bin/thunderbird"),
+ "dictate": ("<Super>i", f"{shell} -lc dictate"),
+ "ocr": ("<Super><Shift>o", f"{shell} -lc ocr"),
+ "record": ("<Super><Shift>r", f"{shell} -lc 'record toggle'"),
+ "clipboard": (
+ "<Super>p",
+ "gdbus call --session --dest org.gnome.Shell.Extensions.Copyous --object-path /org/gnome/Shell/Extensions/Copyous --method org.gnome.Shell.Extensions.Copyous.Show",
+ ),
+ }
+ schema = "org.gnome.settings-daemon.plugins.media-keys"
+ paths = []
+ for name, (binding, command) in actions.items():
+ path = f"/org/gnome/settings-daemon/plugins/media-keys/custom-keybindings/dotfiles-{name}/"
+ paths.append(path)
+ for key, value in {
+ "name": name,
+ "binding": binding,
+ "command": command,
+ }.items():
+ write_key(schema + ".custom-keybinding", key, value, saved, path)
+ merge_key(schema, "custom-keybindings", paths, saved)
+
+
+def apply_settings(saved: dict) -> None:
+ merge_key("org.gnome.shell", "enabled-extensions", EXTENSIONS, saved)
+ merge_key(
+ "org.gnome.desktop.input-sources",
+ "xkb-options",
+ ["caps:escape", "compose:rctrl"],
+ saved,
+ )
+ write_key(
+ "org.gnome.desktop.input-sources", "sources", [("xkb", "us+altgr-intl")], saved
+ )
+ write_key("org.gnome.desktop.wm.keybindings", "close", ["<Super><Shift>q"], saved)
+ write_key("org.gnome.desktop.wm.keybindings", "minimize", ["<Alt>F9"], saved)
+ write_key(
+ "org.gnome.shell.keybindings", "toggle-application-view", ["<Super>d"], saved
+ )
+ write_key(
+ "org.gnome.desktop.wm.keybindings", "toggle-fullscreen", ["<Super>f"], saved
+ )
+ paper = "org.gnome.shell.extensions.paperwm.keybindings"
+ for key in [
+ "new-window",
+ "take-window",
+ "toggle-maximize-width",
+ "slurp-in",
+ "barf-out-active",
+ "cycle-height",
+ ]:
+ write_key(paper, key, [], saved)
+ for direction, letter in zip(["left", "down", "up", "right"], "hjkl"):
+ binding = "<Super>Right" if direction == "right" else f"<Super>{letter}"
+ write_key(paper, f"switch-{direction}", [binding], saved)
+ write_key(paper, f"move-{direction}", [f"<Super><Shift>{letter}"], saved)
+ shortcuts(saved)
+ workspaces(saved)
+
+
+def workspaces(saved: dict) -> None:
+ write_key("org.gnome.mutter", "dynamic-workspaces", False, saved)
+ write_key("org.gnome.desktop.wm.preferences", "num-workspaces", 10, saved)
+ write_key("org.gnome.shell.extensions.dash-to-dock", "hot-keys", False, saved)
+ for number in range(1, 11):
+ key = str(number % 10)
+ for action, modifier in [("switch", ""), ("move", "<Shift>")]:
+ write_key(
+ "org.gnome.desktop.wm.keybindings",
+ f"{action}-to-workspace-{number}",
+ [f"<Super>{modifier}{key}"],
+ saved,
+ )
+ if number < 10:
+ write_key(
+ "org.gnome.shell.keybindings",
+ f"switch-to-application-{number}",
+ [],
+ saved,
+ )
+
+
+def restore(saved: dict) -> None:
+ glib = importlib.import_module("gi.repository.GLib")
+ for name, entry in list(saved.items()):
+ schema, key, path = json.loads(name)
+ settings = settings_object(schema, path)
+ if settings is None or not settings.is_writable(key):
+ continue
+ if settings.get_value(key).print_(True) != entry["applied"]:
+ print(f"Changed since deployment; retained: {schema} {key}")
+ continue
+ if entry["before"] is None:
+ settings.reset(key)
+ else:
+ settings.set_value(
+ key, glib.Variant.parse(None, entry["before"], None, None)
+ )
+ del saved[name]
+ save_state(saved)
+
+
+def main() -> None:
+ if (HOME / ".config/dotfiles/role").read_text().strip() != "canonical":
+ raise SystemExit("The desktop settings require the canonical role.")
+ if "GNOME" not in os.environ.get("XDG_CURRENT_DESKTOP", "").upper():
+ raise SystemExit("Run this command from the GNOME desktop session.")
+ saved = json.loads(STATE.read_text()) if STATE.exists() else {}
+ if sys.argv[1:] == ["settings"]:
+ apply_settings(saved)
+ elif sys.argv[1:] == ["restore"]:
+ restore(saved)
+ else:
+ raise SystemExit("Use settings or restore.")
+ importlib.import_module("gi.repository.Gio").Settings.sync()
+
+
+if __name__ == "__main__":
+ main()
diff --git a/dot_local/lib/dotfiles/record.py b/dot_local/lib/dotfiles/record.py
new file mode 100644
index 0000000..d6b3cf9
--- /dev/null
+++ b/dot_local/lib/dotfiles/record.py
@@ -0,0 +1,97 @@
+"""Control one GNOME portal recording in a transient user service."""
+
+import fcntl
+import os
+import subprocess
+import sys
+from datetime import UTC, datetime
+from pathlib import Path
+
+APP = "com.dec05eba.gpu_screen_recorder"
+UNIT = "dotfiles-record.service"
+
+
+def active() -> bool:
+ return (
+ subprocess.run(
+ ["systemctl", "--user", "is-active", "--quiet", UNIT],
+ check=False,
+ ).returncode
+ == 0
+ )
+
+
+def start(runtime: Path) -> None:
+ if active():
+ return
+ (runtime / "control.sock").unlink(missing_ok=True)
+ videos = Path.home() / "vids"
+ videos.mkdir(exist_ok=True)
+ output = videos / (datetime.now(UTC).strftime("%Y-%m-%d_%H-%M-%S-%fZ") + ".mkv")
+ subprocess.run(
+ [
+ "systemd-run",
+ "--user",
+ "--collect",
+ "--unit=" + UNIT,
+ "--property=KillSignal=SIGINT",
+ "--property=TimeoutStopSec=30s",
+ "/usr/bin/flatpak",
+ "run",
+ "--filesystem=" + str(videos),
+ "--filesystem=" + str(runtime),
+ "--command=gpu-screen-recorder",
+ APP,
+ "-w",
+ "portal",
+ "-f",
+ "60",
+ "-o",
+ str(output),
+ "-ipc",
+ str(runtime / "control.sock"),
+ ],
+ check=True,
+ )
+
+
+def stop(runtime: Path) -> None:
+ if not active():
+ return
+ if (runtime / "control.sock").exists():
+ subprocess.run(
+ [
+ "/usr/bin/flatpak",
+ "run",
+ "--filesystem=" + str(runtime),
+ "--command=gsr-cli",
+ APP,
+ "-ipc",
+ str(runtime / "control.sock"),
+ "stop",
+ ],
+ check=True,
+ )
+ else:
+ subprocess.run(["systemctl", "--user", "stop", UNIT], check=True)
+
+
+def main() -> None:
+ action = sys.argv[1] if len(sys.argv) == 2 else "toggle"
+ if action not in {"start", "stop", "toggle", "status"}:
+ raise SystemExit("Use record start|stop|toggle|status")
+ if action == "status":
+ print("recording" if active() else "stopped")
+ return
+ runtime = Path(os.environ["XDG_RUNTIME_DIR"]) / "dotfiles-record"
+ runtime.mkdir(mode=0o700, exist_ok=True)
+ with (runtime / "lock").open("w") as lock:
+ fcntl.flock(lock, fcntl.LOCK_EX)
+ if action == "stop" or (action == "toggle" and active()):
+ stop(runtime)
+ else:
+ start(runtime)
+
+
+if __name__ == "__main__":
+ main()