feat: sims cli as python package

This commit is contained in:
2026-05-03 22:19:52 +02:00
parent afcf8d51fe
commit 0cd58f4a1f
6 changed files with 171 additions and 60 deletions

View File

@@ -1,2 +1,15 @@
run: run:
python -m sims.main --config ./example-stylix-dev.yaml python -m sims.main --config ./example-stylix-dev.yaml
# Talk to the running sims daemon over DBus.
# Usage: make cli list
# make cli finder
# make cli screenrec stop
# make cli ARGS="list --json" # for flags (make eats leading dashes)
ifeq (cli,$(firstword $(MAKECMDGOALS)))
CLI_ARGS := $(wordlist 2,$(words $(MAKECMDGOALS)),$(MAKECMDGOALS))
$(eval $(CLI_ARGS):;@:)
endif
cli:
@python -m sims.cli $(if $(ARGS),$(ARGS),$(CLI_ARGS))

View File

@@ -32,13 +32,9 @@
{ {
formatter = pkgs.nixfmt-rfc-style; formatter = pkgs.nixfmt-rfc-style;
devShells.default = pkgs.callPackage ./nix/shell.nix { inherit pkgs; }; devShells.default = pkgs.callPackage ./nix/shell.nix { inherit pkgs; };
packages = { packages = rec {
default = pkgs.callPackage ./nix/derivation.nix { inherit (pkgs) lib python3Packages; }; default = pkgs.callPackage ./nix/derivation.nix { inherit (pkgs) lib python3Packages; };
sims-cli = pkgs.writeShellApplication { sims = default;
name = "sims-cli";
runtimeInputs = [ pkgs.dbus ];
text = builtins.readFile ./scripts/sims-cli.sh;
};
}; };
apps.default = { apps.default = {
type = "app"; type = "app";

View File

@@ -15,6 +15,7 @@
notmuch, notmuch,
khal, khal,
emacs, emacs,
dbus,
... ...
}: }:
@@ -64,6 +65,8 @@ python3Packages.buildPythonApplication {
mkdir -p $out/bin mkdir -p $out/bin
cp scripts/launcher.py $out/bin/sims cp scripts/launcher.py $out/bin/sims
chmod +x $out/bin/sims chmod +x $out/bin/sims
cp scripts/cli_launcher.py $out/bin/sims-cli
chmod +x $out/bin/sims-cli
runHook postInstall runHook postInstall
@@ -71,7 +74,7 @@ python3Packages.buildPythonApplication {
preFixup = '' preFixup = ''
makeWrapperArgs+=("''${gappsWrapperArgs[@]}") makeWrapperArgs+=("''${gappsWrapperArgs[@]}")
makeWrapperArgs+=(--prefix PATH : ${lib.makeBinPath [ khal notmuch emacs ]}) makeWrapperArgs+=(--prefix PATH : ${lib.makeBinPath [ khal notmuch emacs dbus ]})
''; '';
passthru = { passthru = {

20
scripts/cli_launcher.py Normal file
View File

@@ -0,0 +1,20 @@
#!/usr/bin/env python3
import os
import sys
script_dir = os.path.dirname(os.path.abspath(__file__))
site_packages_dir = os.path.join(
script_dir,
os.pardir,
"lib",
f"python{sys.version_info.major}.{sys.version_info.minor}",
"site-packages",
)
if site_packages_dir not in sys.path:
sys.path.insert(0, site_packages_dir)
from sims.cli import main
sys.argv[0] = os.path.join(script_dir, os.path.basename(__file__))
sys.exit(main())

View File

@@ -1,53 +0,0 @@
#!/usr/bin/env bash
# sims-cli — talk to the running `sims` status bar over DBus.
set -euo pipefail
DEST=org.Fabric.fabric.sims
OBJ=/org/Fabric/fabric
IFACE=org.Fabric.fabric
invoke() {
dbus-send --session --print-reply --dest="$DEST" "$OBJ" \
"$IFACE.InvokeAction" "string:$1" "array:string:" >/dev/null
}
list_actions() {
dbus-send --session --print-reply --dest="$DEST" "$OBJ" \
org.freedesktop.DBus.Properties.Get \
"string:$IFACE" string:Actions
}
usage() {
cat <<'EOF' >&2
usage: sims-cli <command> [args]
finder open window finder
apps open application launcher
power open power menu
screenshot open screenshot menu
notmuch-refresh refresh unread mail count
screenrec menu open screenrec menu (auto-detects state)
screenrec start-monitor start recording the focused monitor
screenrec start-region start recording a slurp-selected region
screenrec stop stop active recording
list list registered actions
EOF
exit 2
}
case "${1:-}" in
finder) invoke open-finder ;;
apps) invoke open-app-launcher ;;
power) invoke open-power-menu ;;
screenshot) invoke open-screenshot-menu ;;
notmuch-refresh) invoke refresh-notmuch ;;
screenrec)
case "${2:-}" in
menu) invoke open-screenrec-menu ;;
start-monitor) invoke screenrec-start-monitor ;;
start-region) invoke screenrec-start-region ;;
stop) invoke screenrec-stop ;;
*) usage ;;
esac ;;
list) list_actions ;;
*) usage ;;
esac

132
sims/cli.py Normal file
View File

@@ -0,0 +1,132 @@
"""sims-cli — talk to the running sims status bar over DBus."""
from __future__ import annotations
import argparse
import json
import re
import subprocess
import sys
from dataclasses import dataclass
from typing import Callable
DEST = "org.Fabric.fabric.sims"
OBJ = "/org/Fabric/fabric"
IFACE = "org.Fabric.fabric"
def _dbus_send(*args: str) -> str:
proc = subprocess.run(
["dbus-send", "--session", "--print-reply", f"--dest={DEST}", OBJ, *args],
capture_output=True,
text=True,
)
if proc.returncode != 0:
sys.stderr.write(proc.stderr)
sys.exit(proc.returncode)
return proc.stdout
def invoke_action(action: str) -> None:
_dbus_send(f"{IFACE}.InvokeAction", f"string:{action}", "array:string:")
_ACTION_RE = re.compile(r'dict entry\(\s*string "([^"]+)"')
def list_actions() -> list[str]:
out = _dbus_send(
"org.freedesktop.DBus.Properties.Get",
f"string:{IFACE}",
"string:Actions",
)
return _ACTION_RE.findall(out)
@dataclass
class Command:
name: str
help: str
run: Callable[[argparse.Namespace], None]
def _action(name: str) -> Callable[[argparse.Namespace], None]:
return lambda _ns: invoke_action(name)
COMMANDS: list[Command] = [
Command("finder", "open window finder", _action("open-finder")),
Command("apps", "open application launcher", _action("open-app-launcher")),
Command("power", "open power menu", _action("open-power-menu")),
Command("screenshot", "open screenshot menu", _action("open-screenshot-menu")),
Command("notmuch-refresh", "refresh unread mail count", _action("refresh-notmuch")),
]
def _cmd_screenrec(ns: argparse.Namespace) -> None:
mapping = {
"menu": "open-screenrec-menu",
"start-monitor": "screenrec-start-monitor",
"start-region": "screenrec-start-region",
"stop": "screenrec-stop",
}
invoke_action(mapping[ns.screenrec_cmd])
def _cmd_list(ns: argparse.Namespace) -> None:
actions = list_actions()
if ns.json:
json.dump(actions, sys.stdout, indent=2)
sys.stdout.write("\n")
else:
for a in actions:
print(a)
def _cmd_invoke(ns: argparse.Namespace) -> None:
invoke_action(ns.action)
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
prog="sims-cli",
description="Talk to the running sims status bar over DBus.",
)
sub = parser.add_subparsers(dest="cmd", required=True, metavar="COMMAND")
for cmd in COMMANDS:
p = sub.add_parser(cmd.name, help=cmd.help)
p.set_defaults(func=cmd.run)
rec = sub.add_parser("screenrec", help="screen recording controls")
rec_sub = rec.add_subparsers(dest="screenrec_cmd", required=True, metavar="ACTION")
for sub_name, sub_help in [
("menu", "open screenrec menu (auto-detects state)"),
("start-monitor", "start recording the focused monitor"),
("start-region", "start recording a slurp-selected region"),
("stop", "stop active recording"),
]:
rec_sub.add_parser(sub_name, help=sub_help)
rec.set_defaults(func=_cmd_screenrec)
lst = sub.add_parser("list", help="list registered actions")
lst.add_argument("--json", action="store_true", help="emit JSON array")
lst.set_defaults(func=_cmd_list)
inv = sub.add_parser(
"invoke",
help="invoke a raw action by name (escape hatch for actions without a dedicated subcommand)",
)
inv.add_argument("action", help="dbus action name, e.g. open-finder")
inv.set_defaults(func=_cmd_invoke)
return parser
def main() -> None:
parser = build_parser()
ns = parser.parse_args()
ns.func(ns)
if __name__ == "__main__":
main()