{ config, pkgs, ... }: let palette = import ./palette.nix; evdevPython = pkgs.python3.withPackages (ps: [ ps.evdev ]); # Text-capable window classes. wvkbd's own --auto relies on Hyprland's # input-method-v2 protocol, which Hyprland advertises but never actually # fires — so auto show/hide is driven here by watching activewindow events. # Add classes (lowercase prefixes are allowed) for anything you type into. textApps = [ "kitty" "alacritty" "wezterm" "konsole" "com.mitchellh.ghostty" "ghostty" "xterm" "firefox" "librewolf" "chromium" "chromium-browser" "google-chrome" "microsoft-edge" "brave-browser" "thunderbird" "com.github.xournalpp.xournalpp" "code" "code-url-handler" "zen" ]; # Shell-escaped arg list for the CLI fallback (same content as wvkbdFlags # but space-joined for shell expansion). The daemon receives the JSON form. # The font spec is double-quoted so it survives shell word-splitting. wvkbdFlagsShell = pkgs.lib.concatStringsSep " " [ "-l" "simple" "-H" "300" "-L" "200" "--fn" "\"DejaVu Sans 24\"" "--alpha" "235" "--bg" "${palette.wvkbdBg}" "--fg" "${palette.surface}" "--fg-sp" "${palette.wvkbdFgSp}" "--press" "${palette.neon}" "--press-sp" "${palette.violet}" "--text" "${palette.text}" "--text-sp" "${palette.muted}" "--text-press" "${palette.ink}" "--text-press-sp" "${palette.ink}" "--swipe" "${palette.cyan}" "--swipe-sp" "${palette.violet}" "--text-swipe" "${palette.ink}" "--text-swipe-sp" "${palette.ink}" ]; # wvkbd argv as a JSON array. The daemon splices this into place as a bare # Python expression — a JSON array of strings IS valid Python (list of # double-quoted strings), so no quote-escaping headaches at build time. wvkbdArgs = [ "-l" "simple" "-H" "300" "-L" "200" "--fn" "DejaVu Sans 24" "--alpha" "235" "--bg" "${palette.wvkbdBg}" "--fg" "${palette.surface}" "--fg-sp" "${palette.wvkbdFgSp}" "--press" "${palette.neon}" "--press-sp" "${palette.violet}" "--text" "${palette.text}" "--text-sp" "${palette.muted}" "--text-press" "${palette.ink}" "--text-press-sp" "${palette.ink}" "--swipe" "${palette.cyan}" "--swipe-sp" "${palette.violet}" "--text-swipe" "${palette.ink}" "--text-swipe-sp" "${palette.ink}" ]; wvkbdArgsJson = builtins.toJSON wvkbdArgs; # Text-app allowlist as a JSON array, spliced the same way (valid Python # list literal after substitution). textAppsJson = builtins.toJSON textApps; # Place build-time paths into the daemon (replaceVars honours the # @VAR@ placeholders) and run it with the evdev-enabled python. daemon = pkgs.replaceVarsWith { name = "hyprland-tablet-daemon"; src = pkgs.writeScript "hyprland-tablet-daemon-src" '' #!${evdevPython}/bin/python3 ${builtins.readFile ./hyprland-tablet-daemon.py} ''; isExecutable = true; replacements = { EVDEV_DEVICE = "/dev/input/event17"; WVKBD_PATH = "${pkgs.wvkbd}/bin/wvkbd-mobintl"; WVKBD_ARGS = wvkbdArgsJson; TEXT_APPS_JSON = textAppsJson; MONITOR_SENSOR = "${pkgs.iio-sensor-proxy}/bin/monitor-sensor"; HYPRCTL = "${pkgs.hyprland}/bin/hyprctl"; }; }; # Toggle/show/hide are forwarded to the daemon over a Unix datagram socket # so manual control and focus-driven auto-hide share one visibility state. cli = pkgs.writeShellScriptBin "hyprland-tablet" '' set -eu state_file="$XDG_RUNTIME_DIR/hyprland-tablet" ctl_sock="$XDG_RUNTIME_DIR/hyprland-tablet.ctl" ctl_send() { if [ -S "$ctl_sock" ]; then ${pkgs.python3}/bin/python3 -c ' import os, socket, sys path = os.environ.get("XDG_RUNTIME_DIR", "/tmp") + "/hyprland-tablet.ctl" s = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM) s.sendto(sys.argv[1].encode(), path) ' "$1" return 0 fi return 1 } osk_pid() { pgrep -f 'wvkbd-mobintl' | head -n1 || true } case "''${1:-}" in status) if [ -f "$state_file" ]; then cat "$state_file" else echo '{"tablet":false,"osk":false}' fi ;; # Toggle on-screen keyboard visibility. Manual control wins over the # daemon's focus-derived state until the next activewindow change. keyboard-toggle|osk-toggle) ctl_send toggle || { pid="$(osk_pid)" if [ -z "$pid" ]; then nohup ${pkgs.wvkbd}/bin/wvkbd-mobintl ${wvkbdFlagsShell} >/dev/null 2>&1 & else kill -SIGRTMIN "$pid" fi } ;; osk-show) ctl_send show || { pid="$(osk_pid)"; [ -n "$pid" ] && kill -SIGUSR2 "$pid"; } ;; osk-hide) ctl_send hide || { pid="$(osk_pid)"; [ -n "$pid" ] && kill -SIGUSR1 "$pid"; } ;; *) echo "usage: hyprland-tablet {status|keyboard-toggle|osk-show|osk-hide}" >&2 exit 2 ;; esac ''; in { home.packages = [ cli ]; systemd.user.services.hyprland-tablet = { Unit = { Description = "Hyprland tablet-mode auto-rotation + on-screen keyboard"; After = [ "graphical-session.target" ]; PartOf = [ "graphical-session.target" ]; }; Service = { Type = "simple"; ExecStart = daemon; Restart = "on-failure"; }; Install = { WantedBy = [ "graphical-session.target" ]; }; }; }