Split calc-live.zsh: extract the qalc coprocess

266 -> 204, with the warmed qalc coprocess (its four _CALC_Q* state vars
and four functions) in calc-live-qalc.zsh at 74.

The source line uses ${0:A:h}, matching what the file already does for
_CALC_EVAL_PY. That is load-bearing here: this file is loaded as
~/.oh-my-zsh/custom/calc-live.zsh, a symlink into the repo, so a naive
$0 would look for the sibling in the oh-my-zsh custom dir, where it does
not exist -- and break every new shell, not just one script run.

So it is verified THROUGH the symlink, not the repo path: sources clean,
functions defined across the seam, and the qalc path in the new file
answers end to end ("=5 ft to cm" -> 152.4 cm). Seven expressions give
byte-identical results to HEAD, and _calc_is_expr still accepts "2+2"
while rejecting "ls -la".

zsh -n is the parse check here; shellcheck cannot read zsh and the
repo's hook excludes it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
This commit is contained in:
Krzysztof Rudnicki 2026-08-16 22:12:32 +02:00
parent a61d195a89
commit 1506371976
3 changed files with 127 additions and 67 deletions

View File

@ -0,0 +1,48 @@
{
"intent": "Bring calc-live.zsh under the 250-line cap by extracting the warmed qalc coprocess into a sibling file, without changing what the live prompt calculator computes.",
"scope": [
"linux_configuration/zsh/calc-live.zsh (266 -> 204 lines)",
"New: linux_configuration/zsh/calc-live-qalc.zsh (74 lines)",
"Non-goal: changing the evaluator, the expression heuristic, or the ZLE bindings"
],
"changes": [
"Moved the qalc coprocess block -- its four _CALC_Q* state variables and _calc_qalc_line, _calc_qco_start, _calc_qco_stop, _calc_qalc_eval -- into calc-live-qalc.zsh, sourced from calc-live.zsh after _CALC_QALC_BIN and _CALC_TIMEOUT_BIN are set.",
"The source line uses ${0:A:h}, the same idiom the file already relies on for _CALC_EVAL_PY. This matters here more than usual: the file is loaded as ~/.oh-my-zsh/custom/calc-live.zsh, a symlink into the repo, so a naive $0 would resolve into the oh-my-zsh custom directory where the sibling does not exist."
],
"verification": [
{
"command": "zsh -c 'source ~/.oh-my-zsh/custom/calc-live.zsh; ...' -- through the real symlink, not the repo path",
"result": "pass",
"evidence": "Sources without error, all five probed functions are defined across the seam, _calc_eval '10/4' returns 2.5, and the qalc path in the NEW file works end to end: _calc_qalc_line '=5 ft to cm' strips to '5 ft to cm' and _calc_qalc_eval returns '152.4 cm'. This is the check that matters, since the symlink is how the file is actually loaded."
},
{
"command": "seven expressions evaluated under HEAD and under the split, compared",
"result": "pass",
"evidence": "Identical for all seven: 2+2=4, 1/3=0.333333333333, sqrt(2)*3=4.24264068712, sin(pi/2)=1, 2^10=1024, 10/4=2.5, factorial(5)=120. _calc_is_expr still accepts '2+2' and still rejects 'ls -la', so ordinary commands remain untouched."
},
{
"command": "zsh -n on both files",
"result": "pass",
"evidence": "Both parse. zsh is the right parser here; shellcheck cannot read zsh syntax and the repo's shellcheck hook excludes zsh files, so the zsh-syntax hook is the gate."
},
{
"command": "bash ~/utils/scripts/check_file_length.sh",
"result": "pass",
"evidence": "calc-live.zsh 204, calc-live-qalc.zsh 74. Neither appears in the violation list."
},
{
"command": "function-set and line-set diff against HEAD",
"result": "pass",
"evidence": "All ten functions preserved and identical. Zero executable lines lost; the single addition is the source line."
}
],
"risks": [
"This is a live interactive component: a broken source line would break every new shell, not just a script run on demand. That is why it was verified through the oh-my-zsh symlink rather than the repo path -- the symlink is the only path that actually exercises ${0:A:h}.",
"The coprocess variables are shared across the seam by design: calc-live-qalc.zsh sets _CALC_QRESULT and _CALC_QEXPR, which _calc_preview and _calc_accept_line in the main file read. This is a sourced-into-the-same-shell relationship, not two processes, and the end-to-end qalc test exercises exactly that path.",
"Already-running shells keep the old single-file version in memory until they exit; nothing needs restarting, but a shell opened mid-commit could source a half-written pair. Both files were written before either was staged."
],
"rollback": [
"git revert the commit; calc-live.zsh returns to its 266-line single-file form and the sibling disappears. No symlink change is needed -- the oh-my-zsh symlink points at calc-live.zsh either way.",
"After rollback, open a new shell and type '2+2' to confirm the live preview still appears."
]
}

View File

@ -0,0 +1,74 @@
# calc-live-qalc.zsh — the warmed qalc coprocess for "="-prefixed lines
# ----------------------------------------------------------------------------
# Sourced by calc-live.zsh; split out to keep both files under the 250-line
# cap. Not standalone: it relies on _CALC_QALC_BIN and _CALC_TIMEOUT_BIN, which
# calc-live.zsh sets above the source line.
# --- qalc (richer engine) for "="-prefixed lines: units, hex, percentages ----
# qalc cold start is ~120 ms, far too slow per keystroke; but a warmed qalc
# coprocess answers in ~1 ms, fast enough to evaluate live in the redraw hook.
# So we keep ONE persistent qalc per shell, started lazily on first "=" line.
#
# IMPORTANT: the coprocess lives in the main shell. Its evaluator must NOT be
# called via $(...) (that forks a subshell and loses the pipe) — it sets the
# global _CALC_QRESULT instead.
typeset -g _CALC_QCO_UP=0 # 1 once the qalc coprocess is running
typeset -gi _CALC_QSEQ=0 # unique-sentinel counter (prevents desync)
typeset -g _CALC_QEXPR= # stripped expression from the last "=" line
typeset -g _CALC_QRESULT= # result from the last _calc_qalc_eval
# If the line is "=<expr>", set _CALC_QEXPR to the stripped <expr> and return 0.
_calc_qalc_line() {
emulate -L zsh
setopt local_options extended_glob
local s=$1
_CALC_QEXPR=
[[ $s == (#s)[[:space:]]#=* ]] || return 1
s=${s##[[:space:]]#=[[:space:]]#} # drop leading spaces, the "=", trailing spaces
[[ -n $s ]] || return 1
_CALC_QEXPR=$s
return 0
}
# Start / stop the persistent qalc coprocess (no job-control chatter).
_calc_qco_start() {
setopt local_options no_monitor no_notify
coproc "$_CALC_QALC_BIN" -t 2>/dev/null
_CALC_QCO_PID=$!
disown %+ 2>/dev/null
_CALC_QCO_UP=1
}
_calc_qco_stop() {
[[ ${_CALC_QCO_UP:-0} == 1 ]] || return 0
kill "${_CALC_QCO_PID:-0}" 2>/dev/null
_CALC_QCO_UP=0
}
# Evaluate _CALC_QEXPR (arg) via the coprocess; sets _CALC_QRESULT ("" on fail).
_calc_qalc_eval() {
emulate -L zsh
setopt local_options extended_glob no_monitor no_notify
_CALC_QRESULT=
[[ -n $_CALC_QALC_BIN ]] || return 0
local expr=${1// of / * } # qalc mis-parses "A% of B"
(( _CALC_QCO_UP )) || _calc_qco_start
(( _CALC_QSEQ++ ))
local sentinel="909090909${_CALC_QSEQ}" # unique per call, echoes itself
if ! { print -p -- "$expr" && print -p -- "$sentinel" } 2>/dev/null; then
_calc_qco_stop; _calc_qco_start # pipe broke -> restart once
{ print -p -- "$expr" && print -p -- "$sentinel" } 2>/dev/null || return 0
fi
local line result='' saw=0
integer guard=0
while (( guard++ < 100 )) && read -rp -t 0.8 line 2>/dev/null; do
line=${line//$'\e'\[[0-9;?]##[a-zA-Z]/} # strip color escapes
line=${line##[[:space:]]##}; line=${line%%[[:space:]]##}
[[ -z $line || $line == '>'* ]] && continue # blank / echoed input
[[ $line == *$sentinel* ]] && { saw=1; break; } # our sentinel result
result=$line
done
(( saw )) || { _CALC_QCO_UP=0; return 0 } # timed out/wedged -> reset
# qalc echoes the input back when it cannot evaluate (e.g. "1 / 0").
[[ -n $result && ${result// /} != ${expr// /} ]] && _CALC_QRESULT=$result
return 0
}

View File

@ -84,74 +84,12 @@ _calc_eval() {
print -r -- "$out"
}
# --- qalc (richer engine) for "="-prefixed lines: units, hex, percentages ----
# qalc cold start is ~120 ms, far too slow per keystroke; but a warmed qalc
# coprocess answers in ~1 ms, fast enough to evaluate live in the redraw hook.
# So we keep ONE persistent qalc per shell, started lazily on first "=" line.
#
# IMPORTANT: the coprocess lives in the main shell. Its evaluator must NOT be
# called via $(...) (that forks a subshell and loses the pipe) — it sets the
# global _CALC_QRESULT instead.
typeset -g _CALC_QCO_UP=0 # 1 once the qalc coprocess is running
typeset -gi _CALC_QSEQ=0 # unique-sentinel counter (prevents desync)
typeset -g _CALC_QEXPR= # stripped expression from the last "=" line
typeset -g _CALC_QRESULT= # result from the last _calc_qalc_eval
# --- qalc (richer engine) for "="-prefixed lines ------------------------------
# Sourced after _CALC_QALC_BIN and _CALC_TIMEOUT_BIN above, which it reads.
# ${0:A:h} follows the oh-my-zsh symlink back to the real directory in the repo,
# the same idiom used for _CALC_EVAL_PY above.
source ${0:A:h}/calc-live-qalc.zsh
# If the line is "=<expr>", set _CALC_QEXPR to the stripped <expr> and return 0.
_calc_qalc_line() {
emulate -L zsh
setopt local_options extended_glob
local s=$1
_CALC_QEXPR=
[[ $s == (#s)[[:space:]]#=* ]] || return 1
s=${s##[[:space:]]#=[[:space:]]#} # drop leading spaces, the "=", trailing spaces
[[ -n $s ]] || return 1
_CALC_QEXPR=$s
return 0
}
# Start / stop the persistent qalc coprocess (no job-control chatter).
_calc_qco_start() {
setopt local_options no_monitor no_notify
coproc "$_CALC_QALC_BIN" -t 2>/dev/null
_CALC_QCO_PID=$!
disown %+ 2>/dev/null
_CALC_QCO_UP=1
}
_calc_qco_stop() {
[[ ${_CALC_QCO_UP:-0} == 1 ]] || return 0
kill "${_CALC_QCO_PID:-0}" 2>/dev/null
_CALC_QCO_UP=0
}
# Evaluate _CALC_QEXPR (arg) via the coprocess; sets _CALC_QRESULT ("" on fail).
_calc_qalc_eval() {
emulate -L zsh
setopt local_options extended_glob no_monitor no_notify
_CALC_QRESULT=
[[ -n $_CALC_QALC_BIN ]] || return 0
local expr=${1// of / * } # qalc mis-parses "A% of B"
(( _CALC_QCO_UP )) || _calc_qco_start
(( _CALC_QSEQ++ ))
local sentinel="909090909${_CALC_QSEQ}" # unique per call, echoes itself
if ! { print -p -- "$expr" && print -p -- "$sentinel" } 2>/dev/null; then
_calc_qco_stop; _calc_qco_start # pipe broke -> restart once
{ print -p -- "$expr" && print -p -- "$sentinel" } 2>/dev/null || return 0
fi
local line result='' saw=0
integer guard=0
while (( guard++ < 100 )) && read -rp -t 0.8 line 2>/dev/null; do
line=${line//$'\e'\[[0-9;?]##[a-zA-Z]/} # strip color escapes
line=${line##[[:space:]]##}; line=${line%%[[:space:]]##}
[[ -z $line || $line == '>'* ]] && continue # blank / echoed input
[[ $line == *$sentinel* ]] && { saw=1; break; } # our sentinel result
result=$line
done
(( saw )) || { _CALC_QCO_UP=0; return 0 } # timed out/wedged -> reset
# qalc echoes the input back when it cannot evaluate (e.g. "1 / 0").
[[ -n $result && ${result// /} != ${expr// /} ]] && _CALC_QRESULT=$result
return 0
}
# --- Decide whether a line is a pure math expression worth evaluating --------
# Conservative on purpose: must contain a digit and an operator (or a function