فهرست منبع

Initial commit: cadmus spell picker with keyd Right Alt hotkey

fc 1 روز پیش
کامیت
e66d6f3fd7
7فایلهای تغییر یافته به همراه328 افزوده شده و 0 حذف شده
  1. 3 0
      .gitignore
  2. 15 0
      LICENSE
  3. 131 0
      README.md
  4. 85 0
      bin/cadmus
  5. 22 0
      bin/cadmus-launch
  6. 58 0
      install.sh
  7. 14 0
      keyd/cadmus.conf

+ 3 - 0
.gitignore

@@ -0,0 +1,3 @@
+*.swp
+*.tmp
+.DS_Store

+ 15 - 0
LICENSE

@@ -0,0 +1,15 @@
+MIT License
+
+Copyright (c) 2026
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND.

+ 131 - 0
README.md

@@ -0,0 +1,131 @@
+# cadmus
+
+> *Cadmus, the mythological prince who brought the Phoenician alphabet to Greece —
+> now reduced to a one-tap spell picker on your keyboard.*
+
+A tiny dark-mode spell-check popup for Linux Wayland. Tap **Right Alt** anywhere,
+type a word (or pre-select one), pick the right spelling, and it lands on your
+clipboard. Paste it back where you came from.
+
+- Single popup, dark UI, live filtering as you type
+- Powered by `fuzzel` + `hunspell` + the system word list
+- Right Alt **tap** = open popup. Right Alt **hold** = normal AltGr (untouched)
+- ~80 lines of Bash, no daemon, no GUI framework
+
+## Why
+
+Built for KDE Plasma 6 on Wayland where modifier-only global hotkeys
+(like "tap Right Alt by itself") aren't first-class. The trick is to use
+[`keyd`](https://github.com/rvaiya/keyd) at the kernel/evdev layer for the
+hotkey, and a small Bash script for the popup. Works in any app, in any
+desktop environment that supports `wl-copy`.
+
+## Quick start
+
+```bash
+git clone <this-repo> ~/Projects/cadmus
+cd ~/Projects/cadmus
+./install.sh
+```
+
+That's it. Tap Right Alt to try it.
+
+## What `install.sh` does
+
+1. Verifies runtime deps: `fuzzel`, `hunspell`, `wl-clipboard`, `libnotify`.
+2. Installs the two scripts to `/usr/local/bin/`:
+   - `cadmus` — the spell picker itself
+   - `cadmus-launch` — env shim for launching from the root-owned `keyd`
+3. Builds and installs [`keyd`](https://github.com/rvaiya/keyd) from source if
+   it's not already on `PATH` (no `keyd` in Fedora's default repos).
+4. Drops a config at `/etc/keyd/cadmus.conf` mapping Right Alt.
+5. Enables and starts the `keyd` systemd service.
+
+## Manual install
+
+If you'd rather skip the script:
+
+```bash
+sudo install -D -m 755 bin/cadmus        /usr/local/bin/cadmus
+sudo install -D -m 755 bin/cadmus-launch /usr/local/bin/cadmus-launch
+
+# build keyd from source if your distro doesn't ship it
+git clone --depth 1 https://github.com/rvaiya/keyd.git /tmp/keyd
+make -C /tmp/keyd -j"$(nproc)"
+sudo make -C /tmp/keyd install
+
+# install + load the keyd config (replace YOURUSER)
+sed "s/__USER__/$USER/g" keyd/cadmus.conf | sudo tee /etc/keyd/cadmus.conf >/dev/null
+sudo systemctl enable --now keyd
+sudo keyd reload
+```
+
+## Dependencies (Fedora)
+
+```bash
+sudo dnf install fuzzel hunspell hunspell-en-US wl-clipboard libnotify
+```
+
+`keyd` is built from source by `install.sh` (it's not in Fedora repos).
+On Arch, Debian 13+, Ubuntu 25.04+, Alpine, openSUSE, and Void it's a package.
+
+## How it works
+
+```
+Right Alt tap ──► keyd ──► /usr/local/bin/cadmus-launch ──► cadmus
+                                                              │
+                                            fuzzel  ◄─────────┤
+                                            hunspell ◄────────┤
+                                            wl-copy  ◄────────┘
+```
+
+- `keyd` watches `/dev/input/*` at the evdev level and decides "tap vs hold"
+  for Right Alt. On tap, it runs a shell command (as root); the launcher
+  immediately drops to your normal user with `runuser` and sets the
+  Wayland/DBus env vars so GUI tools work.
+- `cadmus` builds (and caches) a filtered word list from `/usr/share/dict/words`.
+- `fuzzel` shows a dark popup with that list, prefilled from your primary
+  selection if any. As you type, it fuzzy-filters live.
+- The chosen word goes through `hunspell` to confirm + suggest, then ends up
+  on the clipboard via `wl-copy`. A `notify-send` toast tells you what happened.
+
+## Configuration
+
+Environment variables read by `cadmus`:
+
+| Var                  | Default                  | Purpose                                     |
+| -------------------- | ------------------------ | ------------------------------------------- |
+| `CADMUS_DICT`        | `en_US`                  | Hunspell dictionary (e.g. `en_GB`, `de_DE`) |
+| `CADMUS_WORDS_FILE`  | `/usr/share/dict/words`  | Source word list for the picker             |
+
+Theme/colors live inline in `bin/cadmus` (the `fuzzel --*-color` flags).
+Edit and re-run `install.sh` (or just the `sudo install` step) to update.
+
+## Uninstall
+
+```bash
+sudo systemctl disable --now keyd
+sudo rm -f /etc/keyd/cadmus.conf
+sudo rm -f /usr/local/bin/cadmus /usr/local/bin/cadmus-launch
+rm -rf "${XDG_CACHE_HOME:-$HOME/.cache}/cadmus"
+```
+
+If `keyd` was *only* installed for cadmus, you can remove it too:
+
+```bash
+sudo make -C /tmp/keyd uninstall   # or via your distro's package manager
+```
+
+## Troubleshooting
+
+- **Hotkey doesn't fire** — check `sudo systemctl status keyd` and
+  `sudo keyd monitor` (tap Right Alt; you should see events).
+- **"missing dependency" toast** — install the deps from the table above.
+- **Wrong language suggestions** — set `CADMUS_DICT=en_GB` (or whatever)
+  in your shell rc *and* in `/etc/keyd/cadmus.conf` if launched via keyd.
+- **Popup opens on the wrong monitor** — tweak `--output` / `--anchor`
+  in `bin/cadmus`.
+
+## License
+
+MIT. Do whatever Cadmus would have done.

+ 85 - 0
bin/cadmus

@@ -0,0 +1,85 @@
+#!/usr/bin/env bash
+# cadmus — dark, live spell picker (fuzzel + hunspell).
+# Named after the mythological founder who brought the alphabet to Greece.
+# Type/select a word in the popup; selection is copied to the clipboard.
+
+set -euo pipefail
+
+DICT="${CADMUS_DICT:-en_US}"
+APP="cadmus"
+WORDS_FILE="${CADMUS_WORDS_FILE:-/usr/share/dict/words}"
+CACHE_DIR="${XDG_CACHE_HOME:-$HOME/.cache}/cadmus"
+WORD_CACHE="$CACHE_DIR/words.txt"
+
+die() { notify-send -a "$APP" -i dialog-error "$APP" "$1" >/dev/null 2>&1 || true; exit 1; }
+
+for bin in fuzzel hunspell wl-copy wl-paste; do
+    command -v "$bin" >/dev/null || die "missing dependency: $bin"
+done
+
+build_cache() {
+    [[ -f "$WORDS_FILE" ]] || die "missing dictionary source: $WORDS_FILE"
+    mkdir -p "$CACHE_DIR"
+    awk '
+        length($0) >= 2 && length($0) <= 24 &&
+        $0 ~ /^[A-Za-z][A-Za-z-]*$/ {
+            print tolower($0)
+        }
+    ' "$WORDS_FILE" | sort -u > "$WORD_CACHE"
+}
+
+[[ -s "$WORD_CACHE" ]] || build_cache
+
+# Prefill input with primary selection (highlight a word, hit hotkey).
+seed="$(wl-paste --primary 2>/dev/null | tr -d '\r\n' | awk '{print $1}')"
+seed="$(printf '%s' "$seed" | tr '[:upper:]' '[:lower:]')"
+
+word=$(
+    fuzzel --dmenu \
+           --prompt "spell> " \
+           --placeholder "type a word…" \
+           --search "$seed" \
+           --match-mode fuzzy \
+           --width 40 \
+           --lines 10 \
+           --background-color 1e1e2eff \
+           --text-color cdd6f4ff \
+           --input-color f5e0dcff \
+           --prompt-color 89b4faff \
+           --placeholder-color 6c7086ff \
+           --match-color f9e2afff \
+           --selection-color 313244ff \
+           --selection-text-color ffffffff \
+           --border-color 89b4faff \
+           --border-width 2 \
+           --border-radius 12 \
+           < "$WORD_CACHE"
+) || exit 0
+
+word="$(printf '%s' "$word" | tr -d '\r\n' | awk '{$1=$1};1' | tr '[:upper:]' '[:lower:]')"
+[[ -z "$word" ]] && exit 0
+
+line=$(printf '%s\n' "$word" | hunspell -d "$DICT" -a 2>/dev/null | awk 'NR>1 && NF{print; exit}')
+
+copy() { printf '%s' "$1" | wl-copy; }
+
+case "${line:-}" in
+    '*'|'+ '*|'')
+        copy "$word"
+        notify-send -a "$APP" -i dialog-ok-apply "✓ $word" "spelled correctly · copied"
+        ;;
+    '#'*)
+        copy "$word"
+        notify-send -a "$APP" -i dialog-warning "$word" "not in dictionary · copied anyway"
+        exit 0
+        ;;
+    '&'*)
+        best="$(printf '%s' "$line" | sed -E 's/^& [^:]+: ([^,]+).*/\1/' | sed 's/^ //')"
+        copy "$word"
+        notify-send -a "$APP" -i dialog-information "Copied: $word" "did you mean: $best"
+        ;;
+    *)
+        notify-send -a "$APP" -i dialog-warning "Unexpected hunspell output" "$line"
+        exit 1
+        ;;
+esac

+ 22 - 0
bin/cadmus-launch

@@ -0,0 +1,22 @@
+#!/usr/bin/env bash
+# cadmus-launch — sets Wayland session env vars when launched from a
+# root-owned key daemon (e.g. keyd's command()), then exec's `cadmus`.
+
+set -euo pipefail
+
+USER_NAME="${CADMUS_USER:-$USER}"
+USER_ID="$(id -u "$USER_NAME")"
+
+export XDG_RUNTIME_DIR="${XDG_RUNTIME_DIR:-/run/user/${USER_ID}}"
+export DBUS_SESSION_BUS_ADDRESS="${DBUS_SESSION_BUS_ADDRESS:-unix:path=${XDG_RUNTIME_DIR}/bus}"
+
+if [[ -z "${WAYLAND_DISPLAY:-}" ]]; then
+    for sock in "${XDG_RUNTIME_DIR}"/wayland-*; do
+        [[ -S "$sock" ]] || continue
+        export WAYLAND_DISPLAY
+        WAYLAND_DISPLAY="$(basename "$sock")"
+        break
+    done
+fi
+
+exec cadmus

+ 58 - 0
install.sh

@@ -0,0 +1,58 @@
+#!/usr/bin/env bash
+# Cadmus installer — wires up the spell picker + Right Alt hotkey via keyd.
+#
+# Re-runnable. Needs sudo for steps that touch /usr/local and /etc.
+
+set -euo pipefail
+
+REPO_DIR="$(cd "$(dirname "$0")" && pwd)"
+USER_NAME="${SUDO_USER:-$USER}"
+
+bold() { printf '\033[1m%s\033[0m\n' "$*"; }
+
+bold "==> Checking runtime dependencies"
+missing=()
+for bin in fuzzel hunspell wl-copy wl-paste notify-send; do
+    if ! command -v "$bin" >/dev/null; then
+        missing+=("$bin")
+    fi
+done
+if (( ${#missing[@]} )); then
+    echo "Missing: ${missing[*]}"
+    echo "On Fedora try: sudo dnf install fuzzel hunspell hunspell-en-US wl-clipboard libnotify"
+    exit 1
+fi
+
+bold "==> Installing scripts to /usr/local/bin"
+sudo install -D -m 755 "$REPO_DIR/bin/cadmus"        /usr/local/bin/cadmus
+sudo install -D -m 755 "$REPO_DIR/bin/cadmus-launch" /usr/local/bin/cadmus-launch
+
+if ! command -v keyd >/dev/null; then
+    bold "==> keyd not found — building from source"
+    BUILD_DIR="${TMPDIR:-/tmp}/cadmus-keyd-build"
+    rm -rf "$BUILD_DIR"
+    git clone --depth 1 https://github.com/rvaiya/keyd.git "$BUILD_DIR"
+    make -C "$BUILD_DIR" -j"$(nproc)"
+    sudo make -C "$BUILD_DIR" install
+fi
+
+bold "==> Installing keyd config"
+TMP_CONF="$(mktemp)"
+sed "s/__USER__/${USER_NAME}/g" "$REPO_DIR/keyd/cadmus.conf" > "$TMP_CONF"
+sudo install -D -m 644 "$TMP_CONF" /etc/keyd/cadmus.conf
+rm -f "$TMP_CONF"
+
+bold "==> Enabling and (re)starting keyd"
+sudo systemctl enable --now keyd
+sudo keyd reload 2>/dev/null || sudo systemctl restart keyd
+
+bold "==> Done"
+cat <<'EOF'
+
+Tap Right Alt to open the spell picker.
+Hold Right Alt to keep normal AltGr.
+
+Pick a word -> it lands on your clipboard, ready to paste.
+
+Tip: highlight a word first; the popup opens with that word prefilled.
+EOF

+ 14 - 0
keyd/cadmus.conf

@@ -0,0 +1,14 @@
+# /etc/keyd/cadmus.conf
+#
+# Tap Right Alt  -> launch cadmus (spell picker)
+# Hold Right Alt -> normal AltGr behavior (unchanged)
+#
+# `command(...)` is run by the keyd daemon as root, so we use runuser
+# to drop into your normal user account, then call the launcher
+# wrapper which sets up Wayland/DBus env vars before exec'ing `cadmus`.
+
+[ids]
+*
+
+[main]
+rightalt = overload(altgr, command(runuser -u __USER__ -- /usr/local/bin/cadmus-launch))