All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
runex config <show|type|where>subcommands (#15). Locate and inspect the active config file. Naming mirrors the OS commands users already know:config whereprints the resolved config path (--config/RUNEX_CONFIG/ default order; the path prints even when the file is missing, exit code 1 flags the absence,--jsonemits{path, exists}),config typestreams the file contents to stdout (Windowstype/ Unixcatsemantics, through the same symlink/size-cap read policy as config loading), andconfig showopens the file with the OS-associated application (explorer.exe/open/xdg-open, spawned detached).
-
runex exportno longer rejects binary paths containing spaces.--bindefaults tocurrent_exe(), but the validator required every character to be printable-ASCII-non-space — so an install underC:\Program Files\...or aC:\Users\John Doe\...home maderunex exportexit 1 (whilerunex init, which never validated, baked the same path fine). The ASCII space is now allowed; control characters and non-ASCII stay rejected. Every shell template quotes the bin placeholder, so the space is safe to embed. -
runex addcan no longer write a config the next load rejects. The append path validated the new rule's fields but not the aggregate constraints: a--whenlist over 64 entries, a 10,001st rule, or an append onto a missing /version-less file (which silently created an unloadable config) all succeeded at write time and then failed wholesale at the next load — surfacing as every keypress silently falling back to a literal space. All three now fail up front with a specific error and leave the file untouched; config creation stays owned byrunex init. -
runex --config <path> init <shell>now generates the integration cache from<path>. The non-clink install path resolved its config independently and ignored the--configoverride, so the cache body (bash bake dispatcher table, trigger keybinds) silently came from the default config while the seed file was written to<path>. The baked hook still resolves its config at runtime viaRUNEX_CONFIG/ the default path;--configat init time shapes the static parts.
- All clippy warnings under Rust 1.91 cleared (let-chain collapses,
std::slice::from_ref, cfg-gating the unix-onlyClipboardError::Timeoutvariant, test-module placement). CONTRIBUTING.mdArchitecture section rewritten around the actual single-crate module layering (cmd → app → domain,infra → domain) instead of the pre-0.1.14 two-crate split.
-
Git Bash bake path: argument-position tokens no longer expand (#9). 0.1.17 introduced the bake-mode dispatcher to fix the cygwin signal loss after
bind -x(#7), but as a carved-out interim trade-off the bake path expanded any trailing token that matched an abbreviation regardless of whether the prefix was a command position. Linux bash / WSL bash / zsh / pwsh / nu refused to expandecho gst<Space>becauseechois not a command position; Git Bash expanded it.The bake dispatcher now reproduces
domain::hook::is_command_positionin pure bash — acasestatement over the four pipeline operators (&&,||,|,;) plus a trailing-sudorecursion that defers to the same operator check — so the bake path produces the same buffer rewrite as the exec path for every input. The 0.1.17 documented trade-off no longer applies.
-
app::bash_static_dispatcher::generate_cygwin_dispatchernow emits a helper__runex_cyg_is_command_positionand invokes it from__runex_cyg_expandbefore the abbreviation lookup. The trade-off docstring at the top of the module is replaced with a parity note that maps each Rust branch (trim_trailing_spaces,ends_with_pipeline_operator,strip_trailing_sudo) to its bash counterpart. -
The bake expand function's
prefixcomputation switches from${left%$token}to a substring slice (${left:0:$((${#left} - ${#token}))}). The old form treated the token as a%glob pattern, so a token containing?/*/[would strip an unintended portion of the left side. The substring form is byte-faithful regardless of the token's contents.
- 5 new unit tests in
app::bash_static_dispatcher::testspin structural invariants of the generated dispatcher (helper function present, every pipeline-opcasepattern present, sudo word check present, command-position check runs before lookup, the prefix uses the substring slice rather than the%glob). - 5 new Linux PTY tests in
tests/bash_cygwin_bake_pty.rscover argument position (echo gst<Space>does not expand) and each command-position prefix (sudo,|,&&,;). The 0.1.17 trade-off pin (cygwin_bake_expands_even_when_token_is_not_in_command_position) is replaced by a positivecygwin_bake_skips_expansion_after_echotest — same input buffer, opposite expectation. - 4 new Windows-local smoke tests in
tests/bash_gitbash_smoke.rsdrive__runex_expandwithREADLINE_LINEdirectly on every cygwin-family bash installed (Git Bash, MSYS2, optionally upstream Cygwin) and assert the rewritten buffer matches the exec-path output. - 1 new Linux exec-path PTY test
(
bash_pty_integration::space_does_not_expand_after_echo_argument_position) mirrors the bake-path counterpart so the parity is visible in a single diff.
docs/setup.md/docs/setup.ja.mdtroubleshooting item 7 ("Git Bash 0.1.17 interim trade-off") is removed — the trade-off no longer applies.
-
Multi-byte (UTF-8) input corrupted by
runex hookcursor mismatch (#6). Every shell sends--cursorin its own native unit — bash, zsh, clink, and nu count Unicode scalar values (= chars); pwsh counts UTF-16 code units. The Rust hook used the raw value as a byte offset, which agreed by accident on pure ASCII but split mid-character on Japanese, emoji, or any other non-ASCII input. The reporter's PoC: typingls ./おはよう ./test1with the cursor at char 10 and pressing Space inserted the space between 「は」 and 「よ」 instead of after 「う」, and pressing Space repeatedly grew that misplaced gap. The same misalignment was surfacing as silent corruption in any expansion that ran with a multi-byte buffer.cmd::hooknow converts the incoming cursor into a byte offset exactly once at the cmd/app boundary (viaapp::hook::shell_cursor_to_byte), andapp::hook::renderconverts back to the shell's native unit on the way out. pwsh's UTF-16 path uses a separate helper so cursors landing right after an emoji (a surrogate pair = 2 code units) round-trip byte-for-byte. zsh keeps splittingLBUFFER/RBUFFERby byte because that's what zsh's renderer needs; the conversion only applies where a cursor number crosses the shell boundary.The Git Bash bake-mode dispatcher introduced in 0.1.17 is unaffected — it slices
${READLINE_LINE:0:READLINE_POINT}in pure bash, which is char-based, so the bake path was correct to begin with.
-
app::hookgained four conversion helpers (char_cursor_to_byte,byte_cursor_to_char,utf16_cursor_to_byte,byte_cursor_to_utf16) and a dispatcher (shell_cursor_to_byte) used at the cmd/app boundary. Each is unit-tested for ASCII identity, Japanese (BMP 3-byte), emoji (surrogate-pair / 4-byte), combining marks, RTL marks, NUL, empty line, past-end clamping, and round-trip symmetry against its inverse. -
cmd::hook::handlelost three near-identicallet cursor_safe = cursor.min(line.len()); let mut s = String::with_capacity(...); s.push_str(&line[..cursor_safe]); ...blocks. The shared shape now lives behindapp::hook::insert_space_action(line, byte_cursor), which the oversize-line / paste-pending / config-load-failure short-circuits call. Tidy First refactor that ships in the same PR as the fix.
- 28 new unit tests in
app::hook(= the four conversion helpers, plus round-trip property tests against their inverses). - 7 new e2e tests in
tests/cli_integration.rs::hook_*runningrunex hook --shell <s> --line <multibyte> --cursor <N>for each of bash / zsh / pwsh / clink / nu and asserting the shell-correct cursor unit comes back. The bash case is the issue reporter's PoC verbatim; the pwsh case exercises the emoji surrogate-pair path.
-
Git Bash: Ctrl+C is lost after an expansion (#7). Under Git Bash (cygwin/msys bash) the readline
bind -xhandler is invoked on top of the cygwin signal layer. Spawning a Win32.exefrom inside that handler — which is exactly what everyrunex hookcall did at trigger time — caused the very nextSIGINTto be lost, so the user's reflexiveCtrl+Cafter an unwanted expansion no longer cleared the line buffer, and pressingEnterran the stale expanded command. Reproduced on Windows 11 + Git Bash 2.50 with every abbreviation, regardless of cursor position or whether the subprocess output was consumed via$(...)or a temp file. The root cause is the spawn itself, not how the output is read.The bash integration cache now ships a bake-mode dispatcher selected at source time by
case "${OSTYPE-}". Undermsys*/cygwin*the trigger handler resolves the abbreviation from a static table baked into the cache file (associative arrays for exact + condition + pattern rules, plus a tiny pure-bash renderer for{}cursor placement and{number}repetition). No subprocess is spawned, so the nextSIGINTreaches the shell as it should andCtrl+Cclears the line as on every other platform.
-
Shell taxonomy:
Shell::CygwinBashvariant dropped from the plan. The 0.1.16 CHANGELOG mentioned that 0.1.17 would introduce aShell::CygwinBashenum variant. After PoC we found the difference is purely a runtime-environment quirk, not a language-level shell distinction, and that taxonomy expansion would have pushed across the enum, config, export, init, and infra layers for a problem that fits in one runtimecaseblock. The shipping approach keepsShellunchanged and routes through$OSTYPEinside the cache file instead. Linux bash, WSL bash, zsh, pwsh, and nu users see no change. -
Bash integration cache version bumped 1 → 2. Caches written by 0.1.16 still source cleanly under 0.1.17 (the legacy exec path is still the
*)arm of thecase), butrunex doctornow flags v1 caches as stale so users get nudged intorunex init bashto pick up the bake dispatcher on Git Bash.
- Git Bash only: argument-position tokens also expand in 0.1.17.
The exec-path hook understood that
echo gstdoes not expandgstbecause it is in argument position, not command position. The 0.1.17 bake path skips that check — re-implementing the state machine in pure bash is straightforward but adds enough surface that it was carved out into 0.1.18 so the Ctrl+C fix could ship first. Until 0.1.18 lands, the bake path expands any trailing token that matches an abbreviation regardless of the preceding word. Note thatdocs/recipes.md's explicit command-position rules (sudo gst, tokens after|/||/&&/;) keep working on both paths because those positions are command positions. Documented indocs/setup.{md,ja.md}and pinned by a regression test (tests/bash_cygwin_bake_pty.rs::cygwin_bake_expands_even_when_token_is_not_in_command_position) so the 0.1.18 fix is a deliberate behaviour change, not a stealth regression. Workarounds while 0.1.17 is current: quote literals you don't want expanded (echo "gst") or pick abbreviation keys that won't collide with English words. Every other shell (including Linux bash and WSL bash) retains full command-position detection.
-
Misleading
sudorecipe in docs (#4).docs/recipes.mdand its Japanese translation showedapt-up = "apt update && apt upgrade"paired withsudo apt-up<Space>— butsudoonly applies to the command immediately after it, so theapt upgradehalf ran as the unprivileged user and silently failed. The section now spells out the pitfall, shows the correct multi-command form (aptup = "sudo apt update && sudo apt upgrade", called without a leadingsudo), and gives a clear rule of thumb for when to bakesudointo the expansion vs. typing it on the command line. -
Trigger space leaks into cursor placeholders (#3). When an abbreviation's
expandcontained{}(the cursor placeholder), the trigger space that fired the expansion was also inserted at the placeholder position.gca<Space>withexpand = "git commit -am '{}'"yieldedgit commit -am ' 'with the cursor after the stray space, instead ofgit commit -am ''with the cursor between the quotes. The trigger space is now suppressed whenever the expansion declares a placeholder, so the rule author's chosen cursor position is preserved.
-
{number}placeholder for numeric repetition (#1). New named placeholder lets a single rule capture trailing digits in the token and repeat a unit string that many times in the expansion:[[abbr]] key = "up{number}" expand = "cd {number}" number = "../"
Typing
up3<Space>then expands tocd ../../../. Exact rules still win when both could match the same token (e.g.up2exact beatsup{number}), so adding a pattern rule never weakens the existing exact ones. Bounded byMAX_NUMERIC_REPEAT = 128and a 32-byte unit cap, so a dynamic expansion can never exceed what a hand-written 4096-byteexpandcould already produce. -
runex list <FILTER>exact-key filter (#2).runex listnow accepts an optional positional argument that narrows the output to the single rule whose key matches exactly. Works for both the TSV default output and--json. A no-match filter is a normal exit-0 with empty output (or an empty JSON array), so the command stays scriptable. Match is case-sensitive and literal — no prefix / substring / glob expansion; reach forrunex which <token>when you want the full per-shell + when_command_exists picture. -
Static shell integration cache (per-keystroke latency fix).
runex init <shell>for bash/zsh/pwsh/nu now writes a static script to<XDG_CACHE_HOME>/runex/integration.<ext>(matching clink's long-standing pattern) and appends a one-linesourceto the user's rcfile/profile. The cache file has the absolutecurrent_exe()path baked in, so per-keystroke hook invocations no longer re-resolverunexthrough$PATH. This removes the ~470 ms-per-keystroke latency users on WSL with amiseshim ahead of~/.cargo/bin/runexwere seeing (mise startup overhead × every Space press inbind -x-style callbacks).- Versioned cache header. Each cache file starts with
# runex-integration-version: 1plus a# runex-bin: <abs>line and a "do not edit" notice. Bumping the format in a future release will be a one-line change here that doctor surfaces as "outdated cache, re-runrunex init <shell>". - Interactive guard inside the cache. Templates now
early-return when sourced by a non-interactive shell
(
bash -c '...', CI scripts, plugin sandboxes), so integration installs leave no side effects on those paths. - Auto-refresh on
runex add/runex remove. Existing caches for shells the user has already installed get silently regenerated when config changes, so new abbreviations are picked up by the next shell start without needing an explicit re-init. Shells without a cache are skipped (no opt-in side effect). runex doctorcache freshness check. Newintegration:<shell>:cacherow per shell flags missing binaries, version mismatches, and legacyeval "$(runex export bash)"-style content. Clink keeps its existing byte-compare freshness probe.
- Versioned cache header. Each cache file starts with
-
runex export <shell>defaults--bintocurrent_exe(). Omitting--bin(the recommended path) bakes the absolute binary path into the generated script. Passing--bin runexexplicitly keeps the legacy bare-name behaviour for power users hand-managing dotfiles that source the same exported script across multiple machines with different installations. Also:runex export <shell>(non-clink) now prepends the same versioned header as the cache file, so the byte stream is interchangeable.
lua_quote_stringdrops Unicode visual-deception characters (RLO, BOM, ZWSP, etc.). Previously these were passed through unchanged because clink's only consumer (--bin) restricted input to printable ASCII viavalidate_bin. With the new static-cache layout, the clink install path also flows throughlua_quote_string, so the quoter is now hardened in isolation rather than relying on upstream validation.
- New module
infra::integration_cache. Owns the cache path resolution, atomic write (sibling-temp + fsync + rename), and header generation. Generalises the pattern that was inline incmd::init::install_clink_luasince 0.1.13. - New
infra::env::xdg_cache_home_with. Mirrors the existingxdg_config_home_with:$XDG_CACHE_HOME→$LOCALAPPDATA(Windows) →~/.cache(non-Windows) →~/AppData/Local(Windows fallback). Resolver-injectable for hermetic tests. - Cleaned up
app::init. Removed the inlinenu_quote_pathhelper (replaced bydomain::shell::nu_quote_stringnow that cache paths flow through there). Thenu_quote_path_escaping/nu_quote_path_deceptivetest mods were re-pinned against the new public API surface (integration_line(Shell::Nu, …)) so the security regression coverage stays intact through the refactor.
- New
tests/shell_integration.rswith five subprocess pins against bash 4+ (Linux only): non-interactive guard works, interactive subshell defines__runex_expand, header contains version + bin lines, rcfile gains asourceline pointing at the cache, init cleans up a stale.tmpfrom a simulated previous crash. infra::integration_cache::tests(7 tests on Windows + 9 on Linux): cache_path resolution per shell, XDG fallback, atomic write, parent-dir auto-creation, symlink reject (Unix), header format pinning.infra::integration_check::tests::cache_freshness(7 tests): every doctor branch (Skipped × 2, Ok × 2, Outdated × 4) including the bare-runexopt-out path.cmd::add_remove::tests(3 tests): silent refresh on add, no-op preservation on zero-match remove, no auto-creation for shells without a pre-existing cache.cli_integrationgains 3 new tests forrunex export bashdefault vs explicit--bin, and an env-isolation fix forinit_cmd_in_dirso parallel tests no longer race on the real~/.cache.
- New ADR
docs/decisions/0001-static-integration-cache.mdrecords the design rationale, considered alternatives (doctor-WARN-only, rcfile-baked absolute path, current_exe-default-only, lazy bind via PROMPT_COMMAND), and the long-term implementation contract. - New ADR
docs/decisions/0002-containerized-linux-ci.mdcaptures the containerised Linux CI design: why Linux CI runs inside a pinned GHCR image, why macOS / Windows stay native, and the digest-pin bump procedure. CONTRIBUTING.mddocuments the dev-container hand-check command and how to roll a newrunex-ciimage digest into.github/workflows/ci.yml.
- Containerised Linux CI.
test-linuxin.github/workflows/ci.ymlnow runs inside the pinnedghcr.io/shortarrow/runex-ci@sha256:...image instead of installing zsh / pwsh / nu / xclip / wl-clipboard / xsel ad-hoc on each run. The image is built and pushed by.github/workflows/build-ci-image.yml(Dockerfile:containers/ci/ubuntu.Dockerfile, sanity check:containers/ci/sanity.sh). Bumping the digest is a one-line commit so a re-built image cannot silently change what the gate runs against. macOS and Windows jobs stay on native runners. - Build-time reproducibility hardening.
ubuntu:24.04is pinned by manifest-list digest;NU_VERSION,RUST_TOOLCHAIN, andNODE_MAJORare explicitARGs so bumps show up ingit log -p;cargo test --lockedon every job (linux/macos/windows) makes Cargo.lock drift fail loudly. - Workflow security tightening. All
actions/checkoutsteps inci.ymlandbuild-ci-image.ymlnow setpersist-credentials: false, matchingrelease.yml.build-ci-image.ymlalso runs as a build-only check onpull_request(no GHCR push, nopackages: writeuse), so a broken Dockerfile fails CI before it can land ondevelop.
docs/setup.md(and the Japanese translation) rewrites the PowerShell section for the static-cache install path, calls out the PSReadLine dependency explicitly, and documents two PS5- specific traps surfaced during 0.1.16 hand-checks: the defaultRestrictedexecution policy refusing to dot-source the cache file, and theAllSignedpolicy plus a newer PSReadLine inDocuments\PowerShell\Modulestriggering an untrusted-publisher prompt. The Troubleshooting list grows two pwsh-specific rows pointing at the same conditions.
Users on 0.1.14 with eval "$(runex export bash)" (or the
shell-equivalent Invoke-Expression (& 'runex' export pwsh | ...))
in their rcfile see no immediate functional change — that form
keeps working. But they don't get the static-cache speedup until
they (a) delete the legacy line and (b) re-run
runex init <shell>.
runex doctor now detects this case explicitly. After upgrading
to 0.1.16 the integration:<shell> row reports Outdated with
the rcfile path, the cache path, and a remediation hint, e.g.:
[WARN] integration:bash: marker found in ~/.bashrc but rcfile uses
still calls `runex export bash` directly instead of sourcing the
cache at ~/.cache/runex/integration.bash
is unused — delete the old line and re-run `runex init bash`
If both the new cache-source line and the legacy export <shell>
line are present (rare — usually because init was re-run before
the legacy line was removed), the same row reports Outdated with
a slightly different message asking to delete the duplicate.
Doctor leaves the rcfile untouched. The fix is one line in the user's rcfile; runex deliberately does not auto-edit shell startup files.
- Git Bash + cursor placeholder + Ctrl+C (cygwin/msys readline
limitation). On Windows Git Bash (the cygwin/msys port of bash
used by Git for Windows), expanding an abbreviation whose
expandcontains{}leaves the cursor in the middle of the line. PressingCtrl+Cright after the expansion does not clear the line buffer — the nextEnterwill then run the stale expanded command (e.g. an unintended emptygit commit -am ''). The same flow works correctly on Linux bash, WSL bash, zsh, pwsh, and nu — only Git Bash's cygwin readline backend is affected. As a workaround, pressBackspace(or any character key) beforeCtrl+C, or just delete the line manually. Runex 0.1.16 will treat cygwin/msys bash as a distinctShell::CygwinBashvariant so the bash template can apply a workaround tailored to that backend.
runex paste-clipboard(hidden subcommand) and nu Ctrl+V paste binding. Reads the system clipboard text and writes it to stdout; the nu integration uses it to inject paste content viacommandline edit --insert, sidestepping nu's per-keystroke abbreviation trigger. Enable by adding toconfig.toml:Provider chain: Windows uses native[keybind.paste_intercept] nu = "ctrl-v"
OpenClipboard/GetClipboardData(CF_UNICODETEXT)viawindows-sys; Linux trieswl-paste→xclip -selection clipboard -o→xsel --clipboard --output; WSL falls back topowershell.exe Get-Clipboardwhen no Linux clipboard daemon is available; macOS usespbpaste. Cap is 1 MiB; per-provider timeout is 500 ms. The paste_intercept binding is not generated when the config does not opt in, so existing nu setups are unaffected.- Config schema:
[keybind.paste_intercept]andTriggerKey::ctrl-v. Currently onlynu = "ctrl-v"is supported. Settingctrl-vas a regular trigger or self-insert binding is rejected withCtrlVAsTrigger/CtrlVAsSelfInsert; setting paste_intercept on bash/zsh/pwsh is rejected withPasteInterceptUnsupportedShell(those shells either have no trigger-on-paste race, or short-circuit viapaste_pending).
- nu (
nushell0.111): pasting content that contains the trigger space drops everything after the first triggering space — UNLESS you opt into the[keybind.paste_intercept] nu = "ctrl-v"binding added in this release. Without paste_intercept, nu's reedline delivers paste characters one keystroke at a time, and theexecutehostcommandevent the runex space binding uses resets the command line at fire time, so paste content arriving after the triggering space is lost. Workarounds, in order of preference:- (Recommended) Configure the Ctrl+V paste binding:
Then paste with Ctrl+V — runex reads the clipboard and inserts it without the abbr binding ever seeing the spaces. Mouse middle-click and terminal right-click paste still go through the keymap and remain affected.
[keybind.paste_intercept] nu = "ctrl-v"
- Switch nu's trigger to a chord paste streams cannot contain:
[keybind.trigger] nu = "shift-space"
- Quote/escape paste content, or paste it in pieces.
This is upstream behaviour for every nu keymap binding, not just
runex; bash/zsh/pwsh/clink are unaffected (no trigger-on-paste
race,
paste_pendingshort-circuit, or standalone-keypress-only bindings respectively).
- (Recommended) Configure the Ctrl+V paste binding:
- Windows Terminal swallows
Ctrl+V(and several other chords) before nu sees them. This breaks the new[keybind.paste_intercept] nu = "ctrl-v"workaround on Windows Terminal even when the runex binding is correctly registered (verified via reedlinekeybindings list). Workarounds:- Use a terminal that does not intercept Ctrl+V — WezTerm and Alacritty pass it through to nu unchanged.
- Remap or disable Ctrl+V in Windows Terminal settings (the
pastebinding) so the chord reaches the shell. - Fall back to
[keybind.trigger] nu = "shift-space"(Known limitation entry above), which sidesteps the trigger-on-paste issue without needing a Ctrl+V binding at all. bash/zsh/pwsh/clink are unaffected because they don't use paste_intercept. Needs investigation: during hand-check,Ctrl+Shift+V(the alternative paste chord on many Windows setups) also failed to reach a registered nu binding under both Windows Terminal and WezTerm. The root cause was not pinned down — it could be the terminal emulator, reedline's modifier name parsing, or nu's bracketed-paste handling. Until that's investigated, treatCtrl+Shift+Vas not a viable alternative chord and stick with the workarounds listed above.
- clink (cmd.exe) integration: rejected
%and!in shell buffer content to block cmd.exe injection. The clink template'srunex_is_safe_linegate previously rejected only ASCII control characters. cmd.exe expands%FOO%even inside double-quoted argv, and!FOO!when SETLOCAL ENABLEDELAYEDEXPANSION is in effect anywhere upstream — so a buffer containing%PATH%or worse%X%" & calc & "%Y%was rewritten by cmd before runex hook saw it, including being able to inject extra commands. The gate now drops on either of those metacharacters; users typing literal%or!lose the runex expansion on that keypress (the trigger key's plain literal-space fallback applies instead) but cmd itself still executes the typed command normally. runex init clinknow writes the lua via atomic-temp + rename and refuses to follow a symlink at the install path. Previously a pre-existing symlink would silently redirect the export to whatever the symlink pointed at, and a crash mid-write left a half-written lua file that clink would parse-fail on the next cmd window.runex init's rcfile marker check now usesO_NOFOLLOWon Unix, matching the policy of the rcfile write side. Previously the read could decide "marker already present" by following a symlink target while the write would refuse to follow — confusing at minimum and potentially usable for information leakage about the target file's contents via init's stdout.- Windows registry
Environment\Pathreads are now bounded to 64 KiB and 256 entries per hive, preventing an attacker (or a runaway installer) who can write to HKCU from making everyrunex hookkeystroke spend extra CPU on a giant PATH walk. - Documented why
read_config_sourceallows symlinks at the final path component — the dotfiles pattern (~/.config/runex/config.toml -> ~/dotfiles/...) is widely used and a deliberate trade-off; the previous docstring claimed stricter behaviour than the code delivered.
Phase B refactor — internal-only restructure, no user-visible
behaviour change: config schema, hook output format, and runex doctor --json are all unchanged from 0.1.13.
runex/src/main.rssplit into per-subcommand handlers underrunex/src/cmd/(one file perCommandsenum variant). The pre-Phase-B 1542-linemain.rsshrinks to dispatch +Cli/Commandsderives + the runtime builder. Each handler is now unit-testable from inside the process —cmd::which::handle("a" .repeat(1025), …)returnsCmdOutcome::ExitCode(1)instead of killing the test process.std::process::exitcalls collapsed from 8 sites to 1. Handlers report failures by returningOk(CmdOutcome::ExitCode(n))through the newCmdResulttype; onlymain()ever callsprocess::exit.AppContextruntime builder centralises the `resolve_config + resolve_shell + compute_precache_fingerprint- make_command_exists
four-line dance that used to be open- coded in five handlers.AppContext::buildfor the strict path;AppContext::build_optional(returningOptionalContext`) for hook / doctor where missing config is non-fatal.
- make_command_exists
- Leaf utilities extracted to
runex/src/util/(shell/path/prompt). Command-specific policy stays with the owning handler —validate_binincmd/export.rs,install_rcfile_integrationincmd/init.rs, etc. - Shared PTY/subprocess test harness at
runex/tests/support/.PtySession::spawn(PtyShell::Bash | Zsh | Pwsh, …)factors out the per-shell launch flags and prompt setup that were previously open-coded in each shell test. runex-core::env::HomeDirResolver— new resolver trait withSystemHomeDir(production) andEnvHomeDir(test, closure- driven) implementations._withvariants ofrc_file_for,xdg_config_home, anddefault_clink_lua_pathsaccept a resolver so init-handler tests can be hermetic without touching process env. The non-_withvariants remain as thin wrappers overSystemHomeDir; public API is additive only.
- New
bash_pty_integration.rs(rewritten via the support harness — 1 scenario),zsh_pty_integration.rs(1 scenario),pwsh_pty_integration.rs(1 scenario). Linux only: expectrl's Windows ConPTY backend is still flagged unstable in the dep declaration, so Windows continues to rely on the existing*_integration.rssubprocess tests. tests::handler_outcomes(7 unit tests) pin the newCmdOutcome::ExitCode(1)contract forhandle_which,handle_expand, andvalidate_bin.tests::app_context(3 unit tests) pin fingerprint stability (same args → same fingerprint) and the missing-config branches for both builder variants.- 18 new unit tests in
runex-corecovering theHomeDirResolvertrait and the_withvariants (rc_file_for_with,default_clink_lua_paths_with,xdg_config_home_with).
Phase C refactor — workspace single-crate switch, no user-visible
behaviour change: config schema, hook output format, and runex doctor --json are all unchanged from 0.1.13. cargo install runex
keeps working exactly as before.
runex-coreabsorbed intorunex. The two-crate workspace the project shipped since 0.1.0 collapses to a single crate. Every module that lived underrunex-core/src/is now underrunex/src/{domain,app,infra}/:domain/(pure logic, no I/O):model,expand,hook,sanitize,timings,shell(+ embedded shell-script templates).app/(orchestration / parse / validate / generate):config,doctor,init,precache.infra/(file / registry / env access):env(withHomeDirResolver),integration_check. Rationale:runex-corehad zero external reverse dependencies on crates.io but was published every release becausecargo publishrequires version-pinned path-deps to be on the index. The internalpubboundary it carried was inappropriate (the crate was always internal-only — see the "Not a public API" disclaimer the 0.1.13 docstring carried). Folding the modules into the bin crate removes the publish ceremony and lets the dependency direction (cmd → app → domain,cmd → util/infra,infra → domain) be enforced by module visibility instead of crate boundaries.
- crates.io publish reduced to one crate. The release
workflow's
publish-cratesjob no longer publishesrunex-core; onlyrunexships. The Trusted Publisher registration forrunex-coreon crates.io is left in place (harmless), andrunex-core 0.1.13(the last published version) stays on crates.io un-yanked for any cargo lockfile that still pins it.
Phase D — strict Clean Architecture cleanup on top of the Phase C
single-crate layout. No user-visible behaviour change: config
schema, hook output, and runex doctor --json remain identical to
0.1.13.
infra → appimport cycle removed.RUNEX_INIT_MARKERandrc_file_for*moved out ofapp::initintoinfra::integration_checkandinfra::envrespectively. The former cycle (app::doctor → infra::integration_check → app::init) is now gone.domain::shellsplit. Orchestration symbols (export_script,trigger_for,*_bind_lines, etc.) moved toapp::shell_export.domain::shellretains only theShellenum and pure quoting helpers — noConfigdependency.app::configfile I/O moved toinfra::config_store.default_config_path,read_config_source,load_config's body,append_abbr_block,remove_abbr_block, and the atomic write/symlink-reject helpers all live underinfra/now.app::configkeeps parse + validate; thin wrappers preserve the call-site API.app::expandandapp::hookuse-case wrappers added. Everycmd/*handler that used to importcrate::domain::expandorcrate::domain::hooknow goes throughapp/. TheHookActiontype is re-exported fromapp::hookso cmd code doesn't reach intodomainfor it either.HomeDirResolverinjection wired to the productioncmd::init::handlepath. The handler now accepts&dyn HomeDirResolver; main dispatch passes&SystemHomeDir. Inlinecmd::init::testsdrive the handler withEnvHomeDirfor hermetic end-to-end coverage. The standalone_with/ resolver-less helper variants are removed in favour of the single resolver-injectable form.- Architecture rules pinned in CI.
runex/tests/architecture.rsadds four compile-time-ish tests:no_infra_to_app_imports(with a small exempt list for type-only imports),no_domain_to_anyone_else_imports,no_cmd_to_domain_behavior_imports,no_filesystem_calls_in_app_layer. Future regressions surface in CI rather than in code review.
- Visibility tightened crate-wide: every
pubitem is nowpub(crate).runexis bin-only; there is no library API to preserve. The narrower visibility makes accidental cross-layer reach harder and shrinks the surface clippy needs to lint. - Dropped unused
IntegrationCheck::{name, detail}accessors. Every consumer destructures viamatch; the methods had zero callers and were carried over from the dropped runex-core public surface. Cargo.toml[lib] deferredcomment removed, replaced with the actual bin-only contract (no[lib]is intentional).util/,cmd/,infra/env,app/module docstrings updated to describe the post-Phase-D layering instead of the Phase-C-future tense they were written in.main.rscrate-root docstring documents the layering diagram and points at the architecture test.
runex init <shell>—initnow accepts an optional shell positional argument so users can target a specific shell (e.g.runex init pwsh,runex init clink) instead of relying on$SHELLauto-detection. Plainrunex initkeeps the existing detect-and-do-one-shell behaviour. Closes the documentation / implementation mismatch whererunex doctorand the docs were recommendingrunex init <shell>against a CLI that didn't accept shell arguments.runex init clinkwrites%LOCALAPPDATA%\clink\runex.luafor you. The lua file is generated fromrunex export clinkagainst the current config, written under%LOCALAPPDATA%\clink\runex.luaby default (override withRUNEX_CLINK_LUA_PATH). Drift with the on-disk file is detected and confirmed before overwriting; identical content is a no-op. This replaces the manualrunex export clink > %LOCALAPPDATA%\clink\runex.luastep that every clink user previously had to run by hand and re-run after every upgrade.- Next-steps guidance after
runex init. Each successful init prints a four-step blurb tailored to the target shell (how to reload, the seedgst<Space>demo, the recipes link, andrunex doctorfor verification). - Seed config now includes a working sample.
runex initwrites a[keybind.trigger] default = "space"block plus agst → git status[[abbr]]rule so a fresh install demonstrates expansion immediately. Existing configs are untouched (initstill usesOpenOptions::create_newand refuses to overwrite). docs/recipes.mdcookbook — 12 use-case-driven, copy-pasteableconfig.tomlsnippets covering Git shortcuts, per-shell command variants (theexpand = { default = …, pwsh = … }table form), three-step fallback chains, cursor placeholders for fill-in-the-blank templates,[keybind.self_insert]for skip-expansion-this-once, Docker/kubectl bundles, and doctor-driven troubleshooting recipes. Cross-linked from README anddocs/config-reference.md. Japanese mirror atdocs/recipes.ja.md.
release.ymlgates binary build oncargo test --workspacepassing across ubuntu/windows/macOS. Previously,buildstarted in parallel with whatever CI workflow the bump commit had triggered, so a tag push could in principle ship binaries from a commit whose tests never finished. The newtestjob isneeds:-required bybuild, closing that race.- README & docs/setup explicitly document rcfile-write safety. New
"What
runex initwill and won't do" section indocs/setup.md(and the Japanese mirror) lists the append-only /O_NOFOLLOW/ marker-idempotent / size-cap properties so users can confidently runinitwithout fearing for their existing rcfile. - crates.io publish moved into CI via OIDC Trusted Publishing.
The
publish-cratesjob inrelease.ymlexchanges the workflow's GitHub OIDC token for a short-lived crates.io token (rust-lang/crates-io-auth-action@v1.0.4), publishesrunex-core, waits for the sparse index to propagate, then publishesrunex. No long-livedCARGO_REGISTRY_TOKENis stored as a repository secret or kept on a developer laptop. One-time per-crate Trusted Publisher setup is required on crates.io — seeCONTRIBUTING.md### crates.io (OIDC Trusted Publishing). Skip the publish on a particular tag by including[skip publish]in the bump commit message.
Release-time reminder: bump the AUR
runex-binPKGBUILD alongside any hook/CLI surface change. Older binaries (e.g. AUR 0.1.11 pre-hook) on a user'sPATHmake rcfile-driven integrations silently fall back to "literal space" becauserunex hookerrors out as an unknown subcommand. The shell template safe-fails by design, so this isn't a bug to fix in the code — it's an operations note.
runex doctornow reports shell-integration health. Newintegration:<shell>rows tell the user whether each shell's rcfile contains therunex-initmarker (so a forgottenrunex init <shell>is visible at a glance) and, for clink specifically, whether therunex.luafile on disk has drifted from whatrunex export clinkwould emit today. The clink check catches the most common upgrade pitfall — bash/zsh/pwsh/nu re-source their integration on shell start, but clink keeps a static copy that has to be refreshed by re-runningrunex init clink. A missing clink lua file is treated as "user doesn't run clink" and is silently skipped rather than warned about. New modulerunex-core/src/integration_check.rshouses the comparison logic.runex doctornow reports every rejected abbreviation rule with its field path.parse_configstill stops at the first invalid field (soconfig_parseshows one error), but doctor walks the TOML source and surfaces everyconfig_validation.abbr[N].<field>failure in one pass. Lets users fix all the typos in one edit instead of running doctor in a loop.- AUR
runex-binand Homebrew tap (shortarrow/runex) packaging.packaging/aur-bin/andpackaging/homebrew/ship the manifests plus release-helper scripts that fetch tarball sha256s from a tag's GitHub Release artifacts and stage commits for both downstream clones. SeeCONTRIBUTING.md#publish-to-package-registries.
- License: dual-licensed under MIT OR Apache-2.0 (was MIT only in
0.1.11). Follows the Rust-ecosystem convention of letting recipients
pick whichever fits their project. The
LICENSEfile now contains both texts,Cargo.tomldeclareslicense = "MIT OR Apache-2.0", and the README sections (English + Japanese) are updated. No code change; this only affects the legal terms under which 0.1.12+ binaries and source can be redistributed. - Shell integration rewritten as thin wrappers around the new
runex hooksubcommand. The script emitted byrunex export <shell>is now a small bootstrap (~16 lines for bash, ~95 for pwsh) that callsrunex hookon every trigger keypress. Command-position detection, token extraction, cursor placeholder handling and shell escaping have all moved into the Rust core. After upgrading, re-runrunex init(orrunex export <shell>) — the existingevalline in your rc file keeps working but the contents it sources change.
[precache]config section is now a no-op. Existing configs continue to parse without errors, but thepath_onlyfield has no run-time effect since the hook bootstrap consults the config (andwhich) per keypress.runex doctor --strictwarns when the section is present so you can remove it at your leisure.runex precachesubcommand is hidden from--helpand is no longer invoked by any shell integration. It remains available for one additional release for backward compatibility and may be removed in a future version.
- Per-shell embedded
case/switchtoken tables in exported scripts. Abbreviation keys are no longer baked into shell code at export time; the hook reads them from config at keypress time. bash_quote_patternand the*_known_caseshelpers used to render those tables. Internal API only — no user-visible impact.
- clink shell integration mis-quoted argv0 for cmd.exe, causing
'runex' is not recognizedon every keypress when the binary path reached io.popen. POSIX single-quote wrapping is interpreted literally by cmd, so the template now uses cmd's own double-quote wrapping. Subsumed (and re-validated) by the hook migration that rewrote the clink template wholesale. - clink (cmd.exe) integration: abbreviations failed to expand when the
cmd host process had a degraded PATH. When clink injected into a
cmd.exe whose PATH lacked the User-scope entries from the registry
(e.g.
~/.cargo/bin,~/AppData/Local/Microsoft/WinGet/Links),runex hook'swhich::whichlookups would fail andwhen_command_existsrules silently evaluated false, producing a no-op space insertion instead of expansion.runex hooknow augments command resolution with HKCU/HKLMEnvironment\Pathon Windows so binaries installed under the User PATH stay reachable regardless of how the parent process was launched.runex doctoralso reports aneffective_search_path: N entries (process=…, +user=…, +system=…)line so this kind of degradation is visible at a glance. Seerunex/src/win_path.rsand the regression testrunex/tests/windows_path_isolation.rs. runex export clinknow embeds the absolute path of the running executable when called with the default--bin runex, sidestepping the same PATH-inheritance issue for clink's lua side.
Initial public release.
- Cross-shell abbreviation engine for bash, zsh, PowerShell, cmd/Clink and Nushell.
runex add/runex removefor in-place config edits.runex doctorwith strict-mode validation, unknown-field detection, and per-rule rejection diagnostics.runex timingsfor per-phase expand profiling.- Cursor placeholder (
{}) support inside expansions. - Distribution: winget (PR submitted), AUR (
runex-bin), Homebrew tap (shortarrow/runex).