English · Español
Offline voice dictation daemon: records the microphone, transcribes with
whisper.cpp (via whisper-rs), and pastes the text into kitty through kitty
remote control — cross-workspace, without stealing focus and with perfect
Unicode.
Heads up — this is a personal tool with a narrow target. It only pastes into kitty, only runs on X11 (built and used under i3), and has no GUI. If that matches your setup, great. If you need Wayland, another terminal, or a graphical app, this is not it (yet — the backend architecture is designed to grow, see below).
- kitty only as paste target (no fallback: if the focused window is not kitty, the error is logged and the text stays in the history).
- X11 only (i3): window capture and hotkeys use X11/XInput2.
- No graphical UI: integration happens over the socket. The binary ships
sparktype watchfor bar-agnostic streaming (see Integrations); the polybar module is thepolybar/sparktype.shscript.
sudo apt install cmake pkg-config libasound2-devcmake— compiles whisper.cpp (build script ofwhisper-rs-sys)pkg-config+libasound2-dev— ALSA, required bycpal/rodiowl-clipboard(Wayland) orxclip(X11) — sohistorycan copy to the clipboard- Rust with edition 2024 (toolchain ≥ 1.85)
just(optional, for the development recipes)
With HDMI as the audio output, the sink enters SUSPENDED after inactivity.
When a beep plays, the HDMI link can take 250ms–2s to wake up depending on the
monitor, which clips the start of the sound. The fix is to disable automatic
suspension of the HDMI sink via WirePlumber:
-- ~/.config/wireplumber/main.lua.d/51-disable-hdmi-suspend.lua
rule = {
matches = { { { "node.name", "matches", "alsa_output.*hdmi*" } } },
apply_properties = { ["session.suspend-timeout-seconds"] = 0 },
}
table.insert(alsa_monitor.rules, rule)Then: systemctl --user restart wireplumber. Verify with
pactl list sinks short (the HDMI sink should stay RUNNING while idle).
Paste uses kitty remote control. In the kitty.conf of the target kitty:
allow_remote_control yes
listen_on unix:@kitty
kitty creates an abstract socket unix:@kitty-<pid> per instance; SparkType
derives it from the _NET_WM_PID of the captured window. Without this config
the paste fails with an error explaining what is missing (the daemon stays
alive).
If you already have the Rust toolchain, you can install straight from git without cloning:
cargo install --git https://github.com/Ithril-Laydec/sparktypeThis still needs cmake and the ALSA dev headers present (whisper.cpp is
compiled from source — see Requirements). It drops the sparktype
binary into ~/.cargo/bin. The repo's install.sh is the fuller path: it
detects your distro, installs build deps, and copies the binary to
/usr/local/bin. When run interactively it finishes by offering to open the
interactive menu (which includes picking your microphone); otherwise it prints
a tip on how to open it later.
Once sparktype is on your PATH, the CLI is self-sufficient — no just
required:
sparktype start # launch the daemon in background and wait until it responds
sparktype stop # stop it (SIGTERM, escalates to SIGKILL after 5s)
sparktype restart # runtime restart: stop then start — does NOT compile
sparktype status # daemon status (GET /status)
sparktype logs # tail -f of the log
sparktype history # interactive picker over the transcription historystopis an alias ofkill; both spellings run the identical teardown.killstays valid for existing scripts (e.g. the hibernate hook).restartperforms a runtime restart: it stops any running daemon and starts a fresh one from the installed binary. It does not build. It is idempotent — if nothing is running, it just starts fresh. A real signal or start failure still surfaces as an error.- Zero-flag by default: with no
--runtime-dirand noSPARKTYPE_RUNTIME_DIR, state follows the XDG Base Directory spec — config in~/.config/sparktype/, data in~/.local/share/sparktype/, logs/models in~/.cache/sparktype/, socket/PID in$XDG_RUNTIME_DIR/sparktype/. No flag is needed for correct operation. --runtime-dir DIR(or envSPARKTYPE_RUNTIME_DIR, flag wins) overrides everything to a single dir (used by the dev justfile with.runtime/).
Full subcommand list: run | start | stop (alias kill) | restart |
status | logs | config get [key] | config set <key> <value> |
history [--limit N] [--search text] | watch [--json].
Run sparktype with no subcommand on an interactive terminal and it opens a
curated arrow-key menu instead of erroring out:
sparktype # opens the interactive menu (TTY only)The menu offers the everyday actions — start / stop / restart the daemon,
status, view logs, history, select microphone, doctor — plus Models… and
Configuration… submenus (each with a Back entry). It deliberately omits
run (foreground), watch, completions, and man, which only make sense as
direct subcommands. Each action runs and returns to the menu; Quit or Esc
exits cleanly. A failing action (e.g. daemon down) prints the error and keeps
the menu open rather than aborting.
- View logs shows a bounded snapshot of the most recent log lines and
returns to the menu (it does not
tail -f). - Non-TTY fallback: when stdin/stdout are redirected
(
sparktype </dev/null | cat), there is no menu — standard--helpis printed and the process exits without blocking on a prompt.
The justfile is a dev-only convenience that injects --runtime-dir .runtime
so all state (socket, PID, logs, config, DB) lands in .runtime/, isolated from
your system install at ~/.cache/sparktype/. The models are shared in
~/.cache/sparktype/models/ so nothing is re-downloaded.
just start # cargo build --release + daemon in background
just status # daemon status (GET /status)
just logs # tail -f of the log
just history # interactive picker (accepts --limit/--search as pre-filter)
just restart # kill + cargo build --release + start ← BUILDS, unlike `sparktype restart`
just kill # stop (SIGTERM, escalates to SIGKILL after 5s)
just run # daemon in foreground (logs in the terminal)
just test # cargo test (manual tests are separate, see below)CLI
restartvsjust restart:sparktype restartis a pure runtime restart with no compilation — it relaunches the currently installed binary.just restartstops the daemon, then runscargo build --release, then starts (the build lives insidejust start), so it picks up source changes. Usesparktype restartin production; usejust restartwhile developing.
watch streams the daemon's state to stdout, one line per meaningful change,
so any status bar or script can integrate via a pipe — see
Integrations.
history opens an interactive (fuzzy) picker over the history read directly
from SQLite (does not need the daemon running) and copies the full text of the
chosen entry to the clipboard (wl-copy on Wayland, xclip on X11).
--limit/--search pre-filter the list. Requires a TTY.
State locations. By default SparkType splits its state across the XDG base directories (each file lands in the right place per the freedesktop spec):
| File | Default (XDG) location | Base |
|---|---|---|
config.toml |
~/.config/sparktype/config.toml |
$XDG_CONFIG_HOME |
history.db |
~/.local/share/sparktype/history.db |
$XDG_DATA_HOME |
sparktype.sock |
$XDG_RUNTIME_DIR/sparktype/sparktype.sock (perms 600) |
$XDG_RUNTIME_DIR |
sparktype.pid |
$XDG_RUNTIME_DIR/sparktype/sparktype.pid |
$XDG_RUNTIME_DIR |
sparktype.log |
~/.cache/sparktype/sparktype.log (raw stdout/stderr) |
$XDG_CACHE_HOME |
sparktype.<date>.log |
~/.cache/sparktype/ (tracing, daily rotation, max 3) |
$XDG_CACHE_HOME |
| models | ~/.cache/sparktype/models/ |
$XDG_CACHE_HOME |
If $XDG_RUNTIME_DIR is unset, socket and PID fall back to
~/.cache/sparktype/ (today's location — still 600). Any $XDG_* value that
is empty or non-absolute is ignored and the ~/… default is used.
Override mode. --runtime-dir DIR (flag) or SPARKTYPE_RUNTIME_DIR=DIR
(env; the flag wins) collapses config/data/runtime/cache under one DIR and
never consults $XDG_*. This is what the dev justfile uses (.runtime/). The
models are the one exception: they always live in the shared
~/.cache/sparktype/models/ regardless of the override, so the ~1.6 GB model is
never re-downloaded per runtime dir.
One-time migration. The first time the daemon starts in XDG mode, if a
legacy ~/.cache/sparktype/config.toml or history.db exists but the XDG
target does not, it is copied (size-verified) to its XDG home. The legacy
file is never deleted and migration is idempotent — a rollback stays safe.
RUST_LOG=debug increases log detail (default info).
Note when diagnosing: the application logs (tracing) are in
sparktype.<date>.log, NOT insparktype.log— that one only contains the daemon's raw stderr (panics, whisper.cpp output, downloads).
Lives in ~/.config/sparktype/config.toml (or <DIR>/config.toml in override
mode). It is created with defaults the first time. A partial file is valid
(missing fields take defaults); an unknown field is a parse error. Full
defaults:
[model]
name = "large-v3-turbo" # one of the 4 allowed models (see below)
language = "auto" # "auto" or ISO code ("es", "en", ...)
[audio]
device = "HyperX" # partial case-insensitive search; "" = default
sample_rate = 16000 # only 16000 (required by whisper.cpp)
[inference]
beam_size = 8 # whisper.cpp max is 8 (v1.7.6 fails with >8)
patience = 2.0 # kept for parity; whisper.cpp 1.7.6 does not implement it
condition_on_previous_text = true
initial_prompt = "Transcripción en español con puntuación correcta."
hotwords = "Albarán Claude SparkType"
[hotkeys]
paste_normal = "F9"
paste_sticky = "super+5"
paste_sticky_enter = "super+6"
cancel_recording = "super+9"
[sound]
enabled = true # embedded beeps: start/stop/stop_sticky_enter/cancel
[app]
history_max_records = 10000Editable with the CLI (with the daemon running):
sparktype config get # full config (JSON)
sparktype config get model # a single top-level section (no dots)
sparktype config set sound.enabled false # set does accept a dotted key; hot-applied
sparktype config set model.name large-v3 # persisted, but requires a restart- Hot (no restart):
hotkeys,sound,app. - Require a daemon restart:
model,audio,inference(the response signals it withrestart_required: true). - Invalid values (unknown model, malformed hotkey) → error 400 and the TOML does not change.
They auto-download from HuggingFace on first start, to
~/.cache/sparktype/models/ (directory shared with the Python SparkType —
nothing is re-downloaded). Progress goes to stderr → visible in
sparktype.log / just logs.
model.name |
File | Size |
|---|---|---|
large-v3-turbo (default) |
ggml-large-v3-turbo.bin |
~1.6 GB |
large-v3 |
ggml-large-v3.bin |
~3.1 GB |
large-v3-turbo-q5_0 |
ggml-large-v3-turbo-q5_0.bin |
~574 MB |
large-v3-q5_0 |
ggml-large-v3-q5_0.bin |
~1.1 GB |
The Silero VAD model (ggml-silero-v5.1.2.bin, ~885 KB) is always downloaded
as well, to filter out silence.
Discover, pre-fetch, and select the active model without editing the TOML. Scope is the 4 allowed models above (the internal VAD is not selectable).
sparktype models list # all 4 models: size, DOWNLOADED, ACTIVE marks
sparktype models download # pre-fetch the configured model (default)
sparktype models download large-v3-q5_0 # pre-fetch a specific model
sparktype models use large-v3 # select the active model (requires a restart)| Action | What it does |
|---|---|
list |
Enumerates the 4 models with size, short description, and DOWNLOADED / ACTIVE flags. |
download [<model>] |
Pre-fetches a model (defaults to the configured one). Idempotent — an already-present valid model is a no-op. An unknown name is rejected before any download. |
use <model> |
Validates against the 4 allowed models and persists model.name. Unknown name → error, config unchanged. |
Download is otherwise lazy: any model auto-downloads on the first
transcription that needs it. models download just forces it ahead of time.
models use mirrors config set model.name (daemon-aware persistence) and
prints a restart hint because model.name requires a restart to take effect.
| Mode | Target window |
|---|---|
normal |
The focused one at paste time |
sticky |
The focused one when the recording stops — you can move to another workspace and the text still arrives |
sticky_enter |
Like sticky + Enter at the end. With empty audio (silence) it sends Enter only, respecting FIFO order |
Pastes go through a FIFO queue with sequencing: even if a short audio finishes transcribing before a longer earlier one, they are pasted in recording-start order.
In sticky_enter, the Enter is sent according to the context when each paste
is executed:
-
send-key enter(real Enter): the default.kitty @ send-key enterencodes the event according to the app's keyboard mode —CSI 13ufor apps with the kitty protocol (OpenCode, Neovim…) and\rfor classic shells. Works in both worlds. -
\nin the payload (Newline): when there is more dictation in flight for the same window, that paste's Enter is downgraded to\n. This keeps the correct order in dictation chains: each intermediate dictation ends with a newline, and only the last one sends the real Enter.
The decision is made when each job in the queue is executed (not at enqueue
time), by consulting has_pending_work_for_window: if there is pending work
for the target window, Newline; otherwise, Key. Pending work counts as:
- a transcription in progress whose captured destination (on stop) is that window,
- a job already enqueued for that window,
- a recording in progress whose window hint is that window. The hint is
captured when the recording starts (in all modes), separate from the
real destination that is still captured on stop. This covers the case where
a long dictation is pasted while the next one is still being recorded over
the same window: the long one is downgraded to
Newlineand does not break the chain. Assumed risk: if the in-progress recording ends over another window (the user moved), the previous dictation is left withNewlinewithout a real Enter (no repair).
| Key | Action |
|---|---|
F9 |
Toggle recording, normal mode |
super+5 |
Toggle recording, sticky mode |
super+6 |
Toggle recording, sticky+enter mode |
super+9 |
Cancel the recording in progress |
Toggle semantics: any paste hotkey with a recording in progress stops it,
and the mode of the stop hotkey wins over the start one (same as the
original Python SparkType). Example: start with super+6 and stop with
super+5 → without Enter; the other way around → with Enter. Stops without
their own mode (REST API /recording/stop, shutdown) keep the start mode.
Hotkeys use XInput2 passive grabs: they are consumed (the key does not
reach the focused app) and work with NumLock/CapsLock on. Format:
[mod+...]key with super/ctrl/alt/shift and F1–F12, letters or digits
(e.g. ctrl+F9).
Socket: $XDG_RUNTIME_DIR/sparktype/sparktype.sock (or <DIR>/sparktype.sock
in override mode), permissions 600.
| Endpoint | What it does |
|---|---|
GET /status |
{ state, paste_mode, model, queue_depth, in_flight, recording_time_secs } |
GET /events |
SSE: first event snapshot (current state), then each EngineEvent as data: <json> |
POST /recording/start |
Body { "mode": "normal"|"sticky"|"sticky_enter" } → { session_id } |
POST /recording/stop |
Stops the recording → { ok: true } |
POST /recording/cancel |
Cancels the recording → { ok: true } |
GET /config |
Full config (JSON) |
PUT /config |
Partial merge: {"model": {"name": "..."}} or {"model.name": "..."} → { ok, restart_required } |
GET /queue |
{ pending: N, current: <job | null> } |
GET /transcriptions?limit=N&q=text |
History (default limit 50) |
Examples:
curl --unix-socket .runtime/sparktype.sock http://localhost/status
curl --unix-socket .runtime/sparktype.sock http://localhost/events # SSE stream
curl --unix-socket .runtime/sparktype.sock -X POST \
-H 'Content-Type: application/json' -d '{"mode":"sticky"}' \
http://localhost/recording/start
curl --unix-socket .runtime/sparktype.sock -X PUT \
-H 'Content-Type: application/json' -d '{"sound":{"enabled":false}}' \
http://localhost/configSSE stream notes:
- Initial snapshot: on connect, the first event is
{"event":"snapshot","state":...,"in_flight":N,"model":...,"queue_depth":N}— the client does not need a priorGET /statusto sync. in_flight: dictations in flight (recorded but not yet pasted: transcribing + paste queue). Each change emits{"event":"in_flight_changed","count":N}.
The daemon exposes its state over the Unix socket, so any status bar or script can react to it without polling. There are two entry points.
X11-only for dictation. The HTTP/SSE daemon runs anywhere, so a Wayland bar (waybar, etc.) can read the state via
watch//eventsand render it. But hotkeys and paste require X11 (i3) — a Wayland setup can display SparkType's state but cannot dictate with it.
watch connects to /events and prints the daemon state to stdout, one line
per meaningful change (line-buffered, so pipes update immediately). It survives
the daemon not running or restarting: it prints a disconnected marker and keeps
retrying instead of exiting.
-
Plain mode (default) emits exactly one lowercase state word per change —
idle,recording,processing, orerror— andoffwhen the daemon is unreachable. In-flight counts are omitted; consecutive duplicates are suppressed. Ideal for a minimal bar label:# i3blocks / tmux / any bar that reads a command's stdout line by line sparktype watch # → off # → idle # → recording # → idle
# tmux status: refresh a pane variable on each state word sparktype watch | while read -r state; do tmux set -g @sparktype "$state" done
-
JSON mode (
--json) forwards each SSE event object as one compact JSON line, preserving every field (state,paste_mode,in_flight,queue_depth,model, counts). Emits{"event":"disconnected"}when the daemon is down. Ideal for waybar-style consumers:sparktype watch --json # → {"event":"snapshot","state":"idle","in_flight":0,"queue_depth":0,"model":"large-v3-turbo",...} # → {"event":"state_changed","state":"recording"} # → {"event":"in_flight_changed","count":1}
If you would rather parse the stream yourself, read the SSE endpoint directly.
This needs nothing but curl and jq:
curl --unix-socket ~/.cache/sparktype/sparktype.sock --no-buffer \
-H 'Accept: text/event-stream' http://localhost/events \
| grep --line-buffered '^data:' \
| sed -u 's/^data: //' \
| jq --unbuffered -c '{event, state, in_flight}'The first event on connect is always a snapshot with the full current state,
so a consumer never needs a prior GET /status to sync.
SparkType generates its own shell completions and man page on demand, to
STDOUT. Nothing is written at build time and install.sh does not install
these into system directories (/etc, /usr/share/man, …) — redirect the
output into your own paths.
# Shell completions — pick your shell and write to its completions dir
sparktype completions bash > ~/.local/share/bash-completion/completions/sparktype
sparktype completions zsh > ~/.zsh/completions/_sparktype
sparktype completions fish > ~/.config/fish/completions/sparktype.fish
# Man page (roff) — write it and view it, or drop it in your MANPATH
sparktype man > sparktype.1
man ./sparktype.1completions <shell> accepts any shell clap_complete supports
(bash, zsh, fish, elvish, powershell); an unknown shell is rejected
with the list of valid choices.
The contrib/ directory ships copy-pasteable configs:
-
polybar —
contrib/polybar/config.example.ini: a[module/sparktype]block (type=custom/script,tail=true,click-left=... --launch) plus amodules-centerplacement hint. It pointsexecat the single-source script inpolybar/sparktype.sh. -
systemd —
contrib/systemd/sparktype.service: a systemd user unit that supervises the daemon. Install and enable it with:mkdir -p ~/.config/systemd/user cp contrib/systemd/sparktype.service ~/.config/systemd/user/ systemctl --user daemon-reload systemctl --user enable --now sparktype
A thin API client: it consumes /events with curl --unix-socket --no-buffer
(reactive, no polling) and renders [queue][state] — one bullet per dictation
in flight (newest on the left) and a fixed state token on the right: off,
starting (there is a PID but no socket), idle, recording, error.
By default the module is vanilla-safe: it works on any polybar with no
custom fonts and no layout escapes. State shows as ASCII tokens
([off] / [...] / [idle] / [REC] / [err]) with standard %{F} colors,
so nothing renders as tofu even without Nerd Fonts.
Two independent switches control the look:
SPARKTYPE_ICONS=nerd|ascii— glyph set and whether a braille spinner animates while recording:nerd→ Nerd-Font glyphs (mic, etc.) plus a braille spinner (⠋⠙⠹…, ~8 fps) next to the red mic while recording. It uses only%{F}color escapes — no%{O}offset and no%{T}font slots — so a Nerd-Font user gets the nice animated look and can drop the module anywhere on the bar. This is fully decoupled from the author's centering hack.ascii→ the vanilla[off]/[...]/[idle]/[REC]/[err]tokens, no spinner, no font slots.- Default
auto→nerdwhenSPARKTYPE_OFFSET=1, elseascii.
SPARKTYPE_OFFSET=1— the author's centering hack, layered on top of nerd icons (see "Advanced" below). It adds the%{O}offset and%{T6}/%{T7}font slots; it impliesnerd.
Other flags:
--launch: startssparktype startonly if the daemon is not running (click action).- Path override:
SPARKTYPE_RUNTIME_DIR(or--runtime-dir) collapses all state under one dir; without it, XDG base directories are used.
Minimal config (config.ini) — copy from
contrib/polybar/config.example.ini:
[bar/main]
modules-center = sparktype
[module/sparktype]
type = custom/script
exec = <path-to-repo>/polybar/sparktype.sh
tail = true
click-left = <path-to-repo>/polybar/sparktype.sh --launchNote: after editing sparktype.sh you must restart polybar
(polybar-msg cmd restart) — the running bash instances read the script
incrementally and an in-place rewrite can leave them corrupted or frozen
(static module).
The author additionally centers the module under a date module using a
horizontal offset and dedicated font slots. This is layered on top of the
nerd icons + spinner (which you can already get on their own with
SPARKTYPE_ICONS=nerd, no offset). It is opt-in: export
SPARKTYPE_OFFSET=1 in the environment polybar runs under. When enabled, the
render is byte-identical to the pre-refactor script (Tokyo Night colors, Nerd
glyphs, %{T6}/%{T7} font slots, %{O} offset) — zero regression. The only
thing SPARKTYPE_OFFSET=1 adds over SPARKTYPE_ICONS=nerd is the %{O}
centering offset and the %{T} font slots.
While recording it shows a braille spinner (~8 fps) next to the red mic. The
icon, spinner and dots are wrapped in %{T6} (font-5 of the bar, IosevkaTerm
NF size 15). The spinner slot has a fixed width: in states without a
spinner a blank placeholder of the same width is emitted, so the module does
not shift when recording starts/stops. The spinner glyph uses its own font slot
(SPARKTYPE_SPINNER_FONT, default 7 = font-6, with a larger vertical offset
;4 to center the braille).
Compensation offset (centering the date): the usage block to the right of
the date would de-center it; the compensation is a single offset emitted by
sparktype.sh. date-claude.sh no longer emits %{O} (fallback:
POLYBAR_DATE_EMIT_OFFSET=1 restores it to use the date module without
sparktype) and publishes the usage width to
${XDG_RUNTIME_DIR:-/tmp}/polybar-ai-usage-width; sparktype.sh reads it on
each render and prepends
O_spark = usage_px − (visible_chars × PX_PER_CHAR) − SEP_PX − GAP_PX
(clamped to 0; no file → 0). Geometry:
[O_spark][spark][sep][date][usage] → to the left of the date there is
exactly usage_px → date centered and spark stuck to it.
The SSE loop uses read -t (tick ~1s while idle, ~0.12s while recording) to
refresh the width and animate the spinner without missing events. Calibration
via env (all only apply when SPARKTYPE_OFFSET=1):
| Env | Default | Meaning |
|---|---|---|
SPARKTYPE_OFFSET |
0 |
1 = add the author centering (%{O} offset + %{T} font slots) on top of the nerd icons/spinner |
SPARKTYPE_PX_PER_CHAR |
13 |
px per visible character at font size 15 |
SPARKTYPE_SEP_PX |
40 |
px of the module separator " | " |
SPARKTYPE_GAP_PX |
0 |
extra margin subtracted from the offset |
SPARKTYPE_WIDTH_FILE |
${XDG_RUNTIME_DIR:-/tmp}/polybar-ai-usage-width |
usage width file |
SPARKTYPE_SPINNER_FONT |
7 |
%{T} slot of the spinner (font-6 with vertical offset ;4) |
cargo test # automated tests (do not touch mic, model or X11)The tests that require a real environment (1.6 GB model, microphone, X11
grabs, kitty) are manual and are in MANUAL_TESTS.md —
run by the user by hand, never by an agent. There are auxiliary binaries
(paste_test, engine_test, hotkey_test, validate_*) in src/bin/.
Known flaky tests (they sometimes fail in the full suite due to timing — short sleeps + runner parallelism — but always pass in isolation or on a second run):
paste::queue::tests::pending_for_window_refleja_conteo_correcto(race with the PasteQueue worker)solo_enter_primero_luego_transcripcion_lenta_misma_ventana(tests/, 200ms sleep)
If one of these fails and you have not touched the paste queue, it is not your change — retry in isolation.
Before the migration, several mechanisms to paste text into a window on another workspace under i3/X11 were validated live:
| Mechanism | Result |
|---|---|
XSendEvent (xdotool type --window) — what the current Python uses |
Apps like kitty/Firefox reject events with send_event=True; accents lost due to the XChangeKeyboardMapping race |
AT-SPI EditableText.InsertText (D-Bus) |
Works in GTK/Qt/browsers, but kitty does not expose AT-SPI (it is OpenGL) |
| XTest + XSetInputFocus | BadMatch: i3 unmaps windows of invisible workspaces |
| XTest alone | Only reaches the focused window — no good for targeting |
kitty remote control (kitty @ send-text) |
✅ Works: it is not X11, it is kitty's own socket. Cross-workspace, silent, perfect Unicode |
That is why v1 uses kitty RC as the only paste backend: the PoC captures
the focused window via _NET_WM_PID, derives the socket unix:@kitty-<pid>
and sends with kitty @ --to <socket> send-text --stdin, verifying with a
read-back (get-text). The full detail is in
docs/plans/rust-migration/plan.md (parent worktree); the PoC code in
poc/atspi-paste/. The tiered backend architecture allows adding
AT-SPI/tmux/wezterm/XSendEvent in the future without touching the dispatcher
or the engine.
Issues and pull requests are welcome. Before your first PR you will need to sign the Contributor License Agreement (CLA) — a bot will request it automatically on the PR and you sign with a comment. See CONTRIBUTING.md for the full flow and the reasoning behind the CLA.
SparkType is distributed under the GNU Affero General Public License v3.0 or
later (AGPL-3.0-or-later). See LICENSE.
In short: you may use, study, modify and redistribute the code, but any derived version you distribute or offer as a network service must also be published under AGPL-3.0 with its source code available. Copyright is held by Ithril Laydec; by retaining full copyright (via the CLA) the project reserves the option to offer commercial licenses in the future.