Skip to main content

zsh/
lib.rs

1//! Zsh interpreter and parser in Rust
2//!
3//! This crate provides:
4//! - A complete zsh lexer (`lexer` module)
5//! - A zsh parser (`parser` module)  
6//! - Shell execution engine (`exec` module)
7//! - Job control (`jobs` module)
8//! - History management (`history` module)
9//! - ZLE (Zsh Line Editor) support (`zle` module)
10//! - ZWC (compiled zsh) support (`zwc` module)
11//! - Fish-style features (`fish_features` module)
12//! - Mathematical expression evaluation (`math` module)
13
14// Many doc comments reference C-source pages, shell constructs, and
15// zsh-internal identifiers by name in `[...]` form; they don't resolve as
16// rustdoc intra-doc links. Silence so docs build clean on CI.
17#![allow(rustdoc::broken_intra_doc_links)]
18#![allow(rustdoc::private_intra_doc_links)]
19#![allow(rustdoc::invalid_html_tags)]
20#![allow(dead_code)]
21#![allow(unused_variables)]
22#![allow(unused_imports)]
23#![allow(unused_assignments)]
24#![allow(unused_mut)]
25#![allow(unused_parens)]
26#![allow(unused_doc_comments)]
27#![allow(unreachable_patterns)]
28#![allow(deprecated)]
29#![allow(unexpected_cfgs)]
30// Allow zsh-canonical identifier names (lowercase statics/constants/types
31// like `ca_parsed`, `convchar_t`, `P_ISBRANCH`) so the ports stay
32// faithful to the C source per PORT.md.
33#![allow(non_snake_case)]
34#![allow(non_camel_case_types)]
35#![allow(non_upper_case_globals)]
36// Function-pointer-to-integer casts appear in ported dispatch tables.
37#![allow(function_casts_as_integer)]
38// Clippy: the C → Rust ports preserve idioms from the zsh source
39// (raw pointer derefs, dead-loop `do { ... } while (0)` shapes, bitmasks
40// that look redundant but match the C, etc.). Silence the whole group so
41// port fidelity wins over Rust-idiom rewrites. New non-ported code
42// should still aim for clippy-clean, but at file/function scope, not
43// crate-wide.
44#![allow(clippy::all)]
45
46/// Runtime shell-mode flag set by the binary entrypoint (`bins/zshrs.rs`)
47/// at startup. The library can't directly read `bins/zshrs.rs::shell_mode()`
48/// (it lives in the binary crate), so the binary writes this atomic when
49/// parsing `--zsh` / `--bash` / `--posix` and the library reads it from
50/// bridge / dispatch sites that need to gate bash-compat-vs-zsh behavior.
51/// Defaults to `false` (zshrs-native mode) when not explicitly set.
52///
53/// Bugs #475 / #504 / #555 in docs/BUGS.md — bash-only builtins
54/// (`caller`/`help`/`mapfile`/`readarray`/`compgen`/etc.) should
55/// dispatch as "command not found" when this is true.
56pub static IS_ZSH_MODE: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
57
58/// `compsys` submodule.
59pub mod compsys;
60/// `exec_jobs` submodule.
61pub mod exec_jobs;
62/// `extensions` submodule.
63pub mod extensions;
64/// `metafied_key` submodule (Rust-only; see the module docs).
65pub mod metafied_key;
66/// `ported` submodule.
67pub mod ported;
68/// `pattern_data_escape` submodule (Rust-only; see the module docs).
69pub mod pattern_data_escape;
70/// `script_bytes` submodule (Rust-only; see the module docs).
71pub mod script_bytes;
72/// `token_char` submodule (Rust-only): where token chars live in a `char` string.
73pub mod token_char;
74/// `zlong_cast` submodule (Rust-only): C's `(zlong) double` with the host CPU's out-of-range answer.
75pub mod zlong_cast;
76/// `host_libc` submodule (Rust-only): host-libc behaviours C zsh inherits by calling libc.
77pub mod host_libc;
78/// `subscript_escape` submodule (Rust-only; see the module docs).
79pub mod subscript_escape;
80/// `test_util` submodule.
81#[cfg(test)]
82pub mod test_util;
83/// `tiers` submodule.
84pub mod tiers;
85pub mod tolerant_sort;
86
87// Back-compat: re-export every ported submodule at the crate root so
88// historical call sites (`crate::exec::`, `crate::subst::`,
89// `crate::zle::`, `crate::modules::`, `crate::builtins::`, etc.)
90// continue to resolve unchanged after the physical move into
91// `src/ported/`. New code should prefer `crate::ported::<name>`.
92pub use ported::*;
93/// `ai` submodule — the `ai` builtin's provider engine (config, cost
94/// accounting, response cache, mock mode, SSE streaming). Ported from
95/// `strykelang/strykelang/ai.rs`.
96#[path = "extensions/ai.rs"]
97pub mod ai;
98/// `alias_input_frames` submodule — per-thread record of popped alias
99/// input-stack frames, restoring the reachability C's manually-indexed
100/// `instack` gives `input_hasalias`.
101#[path = "extensions/alias_input_frames.rs"]
102pub mod alias_input_frames;
103/// `aot` submodule.
104#[path = "extensions/aot.rs"]
105pub mod aot;
106/// `arith_compiler` submodule.
107#[path = "extensions/arith_compiler.rs"]
108pub mod arith_compiler;
109/// `atexit_teardown` submodule — whether a libc `atexit` hook is running,
110/// so the log sites they reach can skip TLS-backed `tracing`.
111#[path = "extensions/atexit_teardown.rs"]
112pub mod atexit_teardown;
113/// `atomic_write` submodule — the shared temp-file-safe shard writer
114/// used by both rkyv caches.
115#[path = "extensions/atomic_write.rs"]
116pub mod atomic_write;
117/// `autoload_cache` submodule.
118#[path = "extensions/autoload_cache.rs"]
119pub mod autoload_cache;
120/// `autoload_prewarm` submodule.
121#[path = "extensions/autoload_prewarm.rs"]
122pub mod autoload_prewarm;
123/// `deparse_cache` submodule.
124#[path = "extensions/deparse_cache.rs"]
125pub mod deparse_cache;
126/// `banner` submodule — the `zbanner` builtin and `zshrs --banner`
127/// (logo, builtin totals, daemon and shell counts). Ported from ztmux.
128#[path = "extensions/banner.rs"]
129pub mod banner;
130/// `bundled_docs` submodule — zsh's man/info pages, shipped in-binary.
131#[path = "extensions/bundled_docs.rs"]
132pub mod bundled_docs;
133#[path = "extensions/bundled_functions.rs"]
134pub mod bundled_functions;
135/// `c_locale` submodule — `mbrtowc`/`wcrtomb` with one C-locale (ASCII)
136/// answer on every platform; re-exported by `ported::utils`.
137#[path = "extensions/c_locale.rs"]
138pub mod c_locale;
139/// `system_libm` submodule — the C library's `cbrt` for zsh/mathfunc (Rust's
140/// compiler_builtins shadows it on Linux).
141#[path = "extensions/system_libm.rs"]
142pub mod system_libm;
143/// `bash_complete` submodule.
144#[path = "extensions/bash_complete.rs"]
145pub mod bash_complete;
146/// `canonical_apply` submodule.
147#[path = "extensions/canonical_apply.rs"]
148pub mod canonical_apply;
149/// Shared-handle accessors for the completion match accumulators (Rust-original
150/// glue restoring C's `matches = mgroup->lmatches` pointer alias; see the module
151/// doc). Deliberately outside `src/ported/` — not a C-fn port.
152pub mod comp_match_handles;
153pub mod comp_word_tok;
154/// `compile_zsh` submodule.
155#[path = "extensions/compile_zsh.rs"]
156pub mod compile_zsh;
157/// `completion` submodule.
158#[path = "extensions/completion.rs"]
159pub mod completion;
160/// `config` submodule.
161#[path = "extensions/config.rs"]
162pub mod config;
163/// `cow_map` submodule — copy-on-write HashMap wrapper for cheap subshell
164/// snapshot/restore (Rust-only helper).
165#[path = "extensions/cow_map.rs"]
166pub mod cow_map;
167/// `daemon_presence` submodule.
168#[path = "extensions/daemon_presence.rs"]
169pub mod daemon_presence;
170/// `errflag_cell` submodule — per-thread storage for `errflag`, restoring
171/// the copy-on-fork semantics C zsh gets for free.
172#[path = "extensions/errflag_cell.rs"]
173pub mod errflag_cell;
174/// `fast_hash` submodule — dependency-free FxHash for internal name tables.
175#[path = "extensions/fast_hash.rs"]
176pub mod fast_hash;
177/// `thread_shell_state` submodule — per-thread storage for the state C
178/// keeps per thread of execution (`scriptname`, `funcstack`,
179/// `zsh_eval_context`, the diagnostic half of `locallevel`).
180#[path = "extensions/thread_shell_state.rs"]
181pub mod thread_shell_state;
182/// `opts_cache` submodule — fast-path `isset()` option-state cache.
183#[path = "extensions/opts_cache.rs"]
184pub mod opts_cache;
185/// `overlay_snapshot` submodule.
186#[path = "extensions/overlay_snapshot.rs"]
187pub mod overlay_snapshot;
188/// `pat_cache` submodule — global compiled-pattern cache (Rust-only opt).
189#[path = "extensions/pat_cache.rs"]
190pub mod pat_cache;
191/// `provenance` submodule — value-lineage ledger over bytecode
192/// execution (zshrs-original; ported from stryke's `provenance.rs`).
193#[path = "extensions/provenance.rs"]
194pub mod provenance;
195/// `reaped_status` submodule — the raw wait statuses the SIGCHLD reaper
196/// collected, so a targeted `waitpid` that loses the race to it can
197/// still read the status it needed (C never needs this: it has exactly
198/// one collector).
199#[path = "extensions/reaped_status.rs"]
200pub mod reaped_status;
201/// `script_cache` submodule.
202#[path = "extensions/script_cache.rs"]
203pub mod script_cache;
204/// `stdout_ferror` submodule — the `ferror(stdout)` indicator for builtin
205/// output, which `std::io::Stdout` cannot report for a closed fd 1.
206#[path = "extensions/stdout_ferror.rs"]
207pub mod stdout_ferror;
208/// `shout` submodule — buffered terminal-output stream for the ZLE display
209/// (the stdio buffering C gets from libc's `FILE *shout`).
210#[path = "extensions/shout.rs"]
211pub mod shout;
212/// `startup_signals` submodule.
213#[path = "extensions/startup_signals.rs"]
214pub mod startup_signals;
215/// `subexp_cleanup` submodule — RAII eviction of `__subexp_arr_*`
216/// paramtab scratch temps created during array sub-expression expansion.
217#[path = "extensions/subexp_cleanup.rs"]
218pub mod subexp_cleanup;
219/// `vm_pool` submodule — per-thread pool of recyclable fusevm VMs.
220#[path = "extensions/vm_pool.rs"]
221pub mod vm_pool;
222// Daemon lives in the `zshrs-daemon` workspace crate. Re-export it as `daemon`
223// so existing `crate::daemon::...` (in vm_helper) and `zsh::daemon::...` (in bins,
224// integration tests) paths keep resolving without churn.
225//
226// The `daemon` feature gates the actual zshrs-daemon dep. When disabled
227// (--no-default-features), a stub module covers the call sites in vm_helper.
228// This lets the library compile in isolation while the daemon crate is
229// being refactored in a concurrent session.
230#[cfg(feature = "daemon")]
231pub use zshrs_daemon as daemon;
232/// `daemon` submodule.
233#[cfg(not(feature = "daemon"))]
234pub mod daemon {
235    //! Stub module used when the `daemon` feature is disabled. Provides
236    //! the minimal surface that `src/vm_helper` calls — the real
237    //! implementation lives in the `zshrs-daemon` workspace crate.
238    pub mod builtins {
239        pub const ZSHRS_BUILTIN_NAMES: &[&str] = &[];
240        /// `is_zshrs_builtin` — see implementation.
241        pub fn is_zshrs_builtin(_name: &str) -> bool {
242            false
243        }
244        /// `try_dispatch` — see implementation.
245        pub fn try_dispatch(_name: &str, _argv: &[String]) -> Option<i32> {
246            None
247        }
248        /// `dispatch` — see implementation.
249        pub fn dispatch(_name: &str, _args: &[String]) -> Option<i32> {
250            None
251        }
252    }
253}
254/// `ast_sexp` submodule.
255#[path = "extensions/ast_sexp.rs"]
256pub mod ast_sexp;
257/// `bash_arrays` submodule — bash sparse-array holes tracker (Rust-only).
258#[path = "extensions/bash_arrays.rs"]
259pub mod bash_arrays;
260/// `bash_prompt` submodule — bash `$PS1` backslash escapes (Rust-only).
261#[path = "extensions/bash_prompt.rs"]
262pub mod bash_prompt;
263/// `emulation_output` submodule — per-shell builtin output formats (Rust-only).
264#[path = "extensions/emulation_output.rs"]
265pub mod emulation_output;
266/// `emulation_startup` submodule — per-drop-in startup/logout files (Rust-only).
267#[path = "extensions/emulation_startup.rs"]
268pub mod emulation_startup;
269/// `dap` submodule.
270#[path = "extensions/dap.rs"]
271pub mod dap;
272/// `dash_mode` submodule — strict-dash emulation flag (Rust-only).
273#[path = "extensions/dash_mode.rs"]
274pub mod dash_mode;
275/// `dumpers` submodule.
276#[path = "extensions/dumpers.rs"]
277pub mod dumpers;
278/// `ext_builtins` submodule.
279#[path = "extensions/ext_builtins.rs"]
280pub mod ext_builtins;
281/// `fds` submodule.
282#[path = "extensions/fds.rs"]
283pub mod fds;
284/// `fish_features` submodule.
285#[path = "extensions/fish_features.rs"]
286pub mod fish_features;
287/// `fmt` submodule — zsh source formatter (CLI `--fmt` + LSP
288/// `textDocument/formatting`).
289#[path = "extensions/fmt.rs"]
290pub mod fmt;
291/// `ftime` submodule — TEMPORARY per-function timing scaffold (Rust-only).
292#[path = "extensions/ftime.rs"]
293pub mod ftime;
294/// `func_body_fmt` submodule.
295#[path = "extensions/func_body_fmt.rs"]
296pub mod func_body_fmt;
297/// `funcdef_capture` submodule — Rust-only verbatim capture of function
298/// body source text as `hgetc` consumes it, so `functions` / `typeset -f`
299/// work for functions defined interactively or on stdin (where zshrs's
300/// `LEX_INPUT` window does not exist). No C counterpart: C zsh
301/// reconstructs the text from wordcode via `getpermtext` (Src/text.c:189).
302#[path = "extensions/funcdef_capture.rs"]
303pub mod funcdef_capture;
304/// `global_rc` submodule — runtime sysconfdir resolution for the
305/// system-wide startup files (Rust-only; zsh bakes the path in at build
306/// time).
307#[path = "extensions/global_rc.rs"]
308pub mod global_rc;
309/// `lsp` submodule.
310#[path = "extensions/lsp.rs"]
311pub mod lsp;
312/// `lsp_symbols` submodule.
313#[path = "extensions/lsp_symbols.rs"]
314pub mod lsp_symbols;
315/// `native_cmds` submodule — builtins contributed by the linking binary
316/// (the fat `zshrs-native` build registers `git` / `arb` / `stryke` here).
317#[path = "extensions/native_cmds.rs"]
318pub mod native_cmds;
319// Lexer + parser live in `src/ported/lex.rs` and `src/ported/parse.rs`.
320// Re-export the modules so existing call sites (`zsh::lex::…`,
321// `zsh::parse::…`, `zsh::tokens::…`) keep resolving.
322// `tokens` aliases `lex` because tokens.rs's contents (lextok enum +
323// reserved-word table) now live inside lex.rs. Char tokens (Pound / Inpar /
324// Equals / …) and the REDIR_* / COND_* constants are not duplicated — they
325// live as flat `pub const` items in `ported::zsh_h` per `Src/zsh.h:144-679`.
326pub use ported::lex;
327pub use ported::lex as tokens;
328pub use ported::parse;
329/// `heredoc_ast` submodule.
330#[path = "extensions/heredoc_ast.rs"]
331pub mod heredoc_ast;
332/// `history` submodule.
333#[path = "extensions/history.rs"]
334pub mod history;
335/// `history_lazy` submodule — on-demand HISTFILE paging; the history
336/// is never slurped whole.
337#[path = "extensions/history_lazy.rs"]
338pub mod history_lazy;
339/// `log` submodule.
340#[path = "extensions/log.rs"]
341pub mod log;
342/// `startup_trace` submodule — phase timer for time-to-first-prompt work.
343#[path = "extensions/startup_trace.rs"]
344pub mod startup_trace;
345/// `lowfd` submodule — keeps the shell's own descriptors out of the user's fd space.
346#[path = "extensions/lowfd.rs"]
347pub mod lowfd;
348/// `zsh_ast` submodule.
349#[path = "extensions/zsh_ast.rs"]
350pub mod zsh_ast;
351// Backwards-compat flat re-exports — call sites that still write
352// `crate::datetime::…`, `crate::stat::…`, etc. resolve to the
353// `crate::modules::<modname>` ports without churn. New code should
354// reach for `crate::modules::<modname>` directly.
355pub use builtins::sched;
356pub use modules::attr;
357pub use modules::cap;
358pub use modules::clone;
359pub use modules::curses;
360pub use modules::datetime;
361pub use modules::db_gdbm;
362pub use modules::example;
363pub use modules::files;
364pub use modules::hlgroup;
365pub use modules::ksh93;
366pub use modules::langinfo;
367pub use modules::mapfile;
368pub use modules::mathfunc;
369pub use modules::nearcolor;
370pub use modules::newuser;
371pub use modules::param_private;
372pub use modules::parameter;
373pub use modules::pcre;
374pub use modules::random;
375pub use modules::random_real;
376pub use modules::regex as regex_module;
377pub use modules::socket;
378pub use modules::stat;
379pub use modules::system;
380pub use modules::tcp;
381pub use modules::termcap;
382pub use modules::terminfo;
383pub use modules::watch;
384pub use modules::zftp;
385pub use modules::zprof;
386pub use modules::zpty;
387pub use modules::zselect;
388pub use modules::zutil;
389/// `compinit_bg` submodule.
390#[path = "extensions/compinit_bg.rs"]
391pub mod compinit_bg;
392/// `fusevm_bridge` submodule.
393pub mod fusevm_bridge;
394/// `fusevm_disasm` submodule.
395pub mod fusevm_disasm;
396/// `intercepts` submodule.
397#[path = "extensions/intercepts.rs"]
398pub mod intercepts;
399/// `p10k` submodule — native powerlevel10k prompt engine.
400#[path = "extensions/p10k/mod.rs"]
401pub mod p10k;
402/// `pkg` — the `zpm` plugin package manager (global store).
403#[path = "extensions/pkg/mod.rs"]
404pub mod pkg;
405/// `plugin_cache` submodule.
406#[path = "extensions/plugin_cache.rs"]
407pub mod plugin_cache;
408/// `plugin_host` submodule — native (Rust) plugin loader (`zmodload -R`).
409#[path = "extensions/plugin_host.rs"]
410pub mod plugin_host;
411/// `recorder_ext` submodule.
412#[path = "extensions/recorder.rs"]
413pub mod recorder_ext;
414/// `rust_ffi` submodule — inline `rust { ... }` FFI desugaring.
415pub mod rust_ffi;
416// Plugin-Framework-Agnostic State-Modification Recorder. Entire module
417// is `#![cfg(feature = "recorder")]` so it disappears from the default
418// `zshrs` build at the rustc-expansion stage. See docs/RECORDER.md.
419/// `async_precmd` submodule — run precmd-style hooks on the worker pool so they
420/// don't block prompt rendering (writes into the shared param table).
421#[path = "extensions/async_precmd.rs"]
422pub mod async_precmd;
423/// `autopair` submodule — native bracket/quote auto-pairing
424/// (port of hlissner/zsh-autopair).
425#[path = "extensions/autopair.rs"]
426pub mod autopair;
427/// `autosuggest` submodule — native fish-style autosuggestions
428/// (port of the reader.rs autosuggestion state machine).
429#[path = "extensions/autosuggest.rs"]
430pub mod autosuggest;
431/// `gen_docs` submodule.
432#[path = "extensions/gen_docs.rs"]
433pub mod gen_docs;
434/// `history_search` submodule — native up-arrow prefix/substring/token history
435/// search (port of fish reader/history_search.rs).
436#[path = "extensions/history_search.rs"]
437pub mod history_search;
438/// `recorder` submodule.
439#[cfg(feature = "recorder")]
440pub mod recorder;
441/// `regex_mod` submodule.
442#[path = "extensions/regex_mod.rs"]
443pub mod regex_mod;
444/// `stringsort` submodule.
445#[path = "extensions/stringsort.rs"]
446pub mod stringsort;
447/// `terminfo_caps` submodule — the frozen terminfo capability-name tables
448/// that replace ncurses' exported `boolnames`/`numnames`/`strnames` arrays.
449#[path = "extensions/terminfo_caps.rs"]
450pub mod terminfo_caps;
451/// `terminfo_db` submodule — pure-Rust reader for the compiled terminfo
452/// database, replacing `setupterm`/`tigetstr`/`tgetent`/… from libtinfo.
453#[path = "extensions/terminfo_db.rs"]
454pub mod terminfo_db;
455/// `tparm` submodule — the terminfo parameterized-string evaluator plus
456/// `tgoto` and `tputs` padding, replacing the last libtinfo entry points.
457#[path = "extensions/tparm.rs"]
458pub mod tparm;
459/// `syntax_highlight` submodule — native command-line syntax highlighting
460/// (port of fish highlight/highlight.rs, driven by the zshrs lexer).
461#[path = "extensions/syntax_highlight.rs"]
462pub mod syntax_highlight;
463/// `worker` submodule.
464#[path = "extensions/worker.rs"]
465pub mod worker;
466/// `zle_file_tester` submodule — file-existence/permission tests for native ZLE
467/// syntax highlighting (port of fish highlight/file_tester.rs).
468#[path = "extensions/zle_file_tester.rs"]
469pub mod zle_file_tester;
470/// `zle_fx` submodule — wiring for the native ZLE effects (autosuggest,
471/// syntax highlight, history search, autopair) into zlecore + the renderer.
472#[path = "extensions/zle_fx.rs"]
473pub mod zle_fx;
474/// `zle_param_sync` submodule — ZLE special-param write-back sync
475/// (Rust-only adapter for C's live GSU setters).
476#[path = "extensions/zle_param_sync.rs"]
477pub mod zle_param_sync;
478/// `zsh_builtin_docs` submodule.
479#[path = "extensions/zsh_builtin_docs.rs"]
480pub mod zsh_builtin_docs;
481/// `zsh_ext_builtin_docs` submodule.
482#[path = "extensions/zsh_ext_builtin_docs.rs"]
483pub mod zsh_ext_builtin_docs;
484/// `zsh_keyword_docs` submodule.
485#[path = "extensions/zsh_keyword_docs.rs"]
486pub mod zsh_keyword_docs;
487/// `zsh_option_docs` submodule.
488#[path = "extensions/zsh_option_docs.rs"]
489pub mod zsh_option_docs;
490/// `zsh_special_var_docs` submodule.
491#[path = "extensions/zsh_special_var_docs.rs"]
492pub mod zsh_special_var_docs;
493/// `ztest` submodule — shell-level unit test framework
494/// (port of `../strykelang` test framework).
495#[path = "extensions/ztest.rs"]
496pub mod ztest;
497/// `zwc` submodule.
498#[path = "extensions/zwc.rs"]
499pub mod zwc;
500/// `zwc_decode` submodule.
501#[path = "extensions/zwc_decode.rs"]
502pub mod zwc_decode;
503// Backwards-compat re-export so `crate::rlimits::…` keeps resolving.
504pub use builtins::rlimits;
505
506// Top-level shell executor state + fusevm bridge glue. Not a port of
507// any single Src/*.c file — zsh's native wordcode VM lives in `Src/exec.c`;
508// zshrs runs fusevm instead (see src/fusevm_bridge.rs).
509/// `vm_helper` submodule.
510pub mod vm_helper;
511
512pub use fish_features::{
513    autosuggest_from_history,
514    colorize_line,
515    expand_abbreviation,
516    // Syntax highlighting
517    highlight_shell,
518    // Private mode
519    is_private_mode,
520    // Killring
521    kill_add,
522    kill_replace,
523    kill_yank,
524    kill_yank_rotate,
525    set_private_mode,
526    validate_autosuggestion,
527    // Validation
528    validate_command,
529    with_abbrs,
530    with_abbrs_mut,
531    AbbrPosition,
532    // Abbreviations
533    Abbreviation,
534    AbbreviationSet,
535    // Autosuggestions
536    Autosuggestion,
537    HighlightRole,
538    HighlightSpec,
539    KillRing,
540    ValidationStatus,
541};
542pub use tokens::lextok;
543pub use vm_helper::ShellExecutor;
544
545// ── Stryke integration hook ──
546// The fat binary registers a handler for @ prefix dispatch.
547// The thin binary leaves this as None — @ is treated as a normal character.
548
549use std::sync::OnceLock;
550
551type StrykeHandler = Box<dyn Fn(&str) -> i32 + Send + Sync>;
552static STRYKE_HANDLER: OnceLock<StrykeHandler> = OnceLock::new();
553
554/// Register a handler for @ prefix lines (fat binary sets this to stryke::run).
555pub fn set_stryke_handler<F>(f: F)
556where
557    F: Fn(&str) -> i32 + Send + Sync + 'static,
558{
559    let _ = STRYKE_HANDLER.set(Box::new(f));
560}
561
562/// Try to dispatch a line starting with @ to stryke.
563/// Returns Some(exit_code) if handled, None if no handler registered.
564pub fn try_stryke_dispatch(code: &str) -> Option<i32> {
565    STRYKE_HANDLER.get().map(|f| f(code))
566}
567
568/// Run an interactive `@ <code>` line through the registered stryke handler.
569///
570/// Called by `inputline` on each line it reads, before the lexer sees it:
571/// stryke syntax is not shell syntax (`1:10 |> sum` is a pipe into a
572/// redirect, `$_` would be expanded), so it cannot be parsed as a command
573/// named `@`. Only the first line of an interactive command is considered,
574/// so a `@` inside a continuation (heredoc body, open quote) stays shell
575/// input. On dispatch `$?` takes the handler's status, the line is queued for
576/// history (`stryke_history_flush`) unless HIST_IGNORE_SPACE drops it, and
577/// the lexer is handed an empty line. Without a handler — the
578/// thin binary — the line is returned untouched.
579pub(crate) fn stryke_line(line: String) -> String {
580    use crate::ported::zsh_h::{isset, HISTIGNORESPACE, SHINSTDIN};
581    use std::sync::atomic::Ordering;
582
583    if !(crate::ported::zsh_h::interact()
584        && isset(SHINSTDIN)
585        && crate::ported::lex::LEX_ISFIRSTLN.with(|c| c.get()))
586    {
587        return line;
588    }
589    let text = line.trim_end_matches('\n');
590    let Some(code) = text.trim_start().strip_prefix('@') else {
591        return line;
592    };
593    let code = code.trim();
594    if code.is_empty() {
595        return line;
596    }
597    let Some(status) = try_stryke_dispatch(code) else {
598        return line;
599    };
600    crate::ported::builtin::LASTVAL.store(status, Ordering::SeqCst);
601    if !(text.starts_with(' ') && isset(HISTIGNORESPACE)) {
602        *STRYKE_HISTORY.lock().unwrap() = Some(text.to_string());
603    }
604    "\n".to_string()
605}
606
607/// A dispatched `@` line waiting to be recorded in history.
608static STRYKE_HISTORY: std::sync::Mutex<Option<String>> = std::sync::Mutex::new(None);
609
610/// Record the last dispatched `@` line in history, the way `print -s` does.
611///
612/// Called by `r#loop` between the `hend` that closed the `@` line's (empty)
613/// event and the `hbegin` that opens the next one. Recording earlier, from
614/// inside `inputline`, takes the event number `hbegin` already gave the
615/// current line, and the duplicate number sends `fc -l` round the ring forever.
616pub(crate) fn stryke_history_flush() {
617    let Some(text) = STRYKE_HISTORY.lock().unwrap().take() else {
618        return;
619    };
620    let event_id = crate::ported::hist::prepnexthistent();
621    crate::ported::hashtable::addhistnode(&text, event_id as i32);
622    let ent = crate::ported::hist::make_histent(event_id, text);
623    if let Ok(mut ring) = crate::ported::hist::hist_ring.lock() {
624        ring.insert(0, ent);
625        crate::ported::hist::histlinect.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
626    }
627}
628
629/// Register a native command contributed by the linking binary.
630///
631/// Convenience re-spelling of [`native_cmds::register`] at the crate root, so
632/// a fat binary's `main` reads as one call per runtime:
633///
634/// ```ignore
635/// zsh::register_native_command("git", |argv| zvcs::run_argv(argv));
636/// ```
637///
638/// The name then dispatches in-process — `whence -w git` says `builtin`,
639/// `${+builtins[git]}` is 1, `builtin git` reaches it, a user `git()` function
640/// still shadows it, and `command git` still runs the one on `PATH`.
641pub fn register_native_command<F>(name: &str, f: F)
642where
643    F: Fn(&[String]) -> i32 + Send + Sync + 'static,
644{
645    native_cmds::register(name, f);
646}