Skip to content

[WIP] Live Testing and Error Fixes: FreeBSD 15#3

Merged
orpheus497 merged 35 commits into
mainfrom
dev2
Apr 21, 2026
Merged

[WIP] Live Testing and Error Fixes: FreeBSD 15#3
orpheus497 merged 35 commits into
mainfrom
dev2

Conversation

@orpheus497
Copy link
Copy Markdown
Owner

@orpheus497 orpheus497 commented Apr 21, 2026

Summary by CodeRabbit

  • New Features

    • Added predictive autocomplete with ghost text suggestions from command history (accept with Right-Arrow).
    • Added Git branch detection in prompts (%g/%G expansion for branch/operation status).
    • Added interactive per-keystroke syntax highlighting (toggled via set syntax).
    • Added filetype-based color output support (set color with LSCOLORS/LS_COLORS).
  • Bug Fixes

    • Improved terminal emulation compatibility and build configuration accuracy.
    • Enhanced installation script robustness and option parsing.
  • Documentation

    • Updated README with feature overview and prompt reference.
    • Revised example startup configuration (dot.mcshrc) with modern defaults and aliases.

- Add vms.termcap.c (fetched from upstream tcsh-org/tcsh); compiles to
  an empty translation unit on BSD/Linux, but is required by Makefile.in
- Add dch-template.in (proper Debian changelog template, adapted from
  upstream tcsh with mcsh branding)
- configure.ac: remove erroneous '##0' strip from PACKAGE_PATCHLEVEL
  derivation; for version x.y.0 this produced an empty macro, causing
  compile errors in tc.vers.c
- configure.ac: fix TCSH_BASELINE_VERSION AC_DEFINE_UNQUOTED to expand
  the literal version string rather than the macro name
- Commit generated autoconf outputs (configure, aclocal.m4, m4/,
  acaux/, config.h.in, nls/catgen) so the repo is buildable without
  requiring autoconf/automake to be installed
- Add alacritty.toml (sets mcsh as the terminal shell for testing)

💘 Generated with Crush

Assisted-by: Claude Sonnet 4.5 via Crush <crush@charm.land>
The version string format included '[tcsh baseline N.N.N]'; the square
brackets are glob metacharacters.  set1() in sh.set.c always globs the
value vector unless VAR_NOGLOB is set, so on startup fix_version() was
calling setcopy(STRversion, ..., VAR_READWRITE) with a string containing
'[...]', tglob() flagged it as a pattern, globall() found no matching
files, and the shell immediately died with 'No match'.

Upstream tcsh's version string never contains glob metacharacters, so
this regression was introduced when mcsh added the '[tcsh baseline ...]'
annotation.  Replace the bracketed annotation with the brace-free form
'tcsh-baseline:N.N.N', consistent with the colon-separated option flags
that follow it.

💘 Generated with Crush

Assisted-by: Claude Sonnet 4.5 via Crush <crush@charm.land>
- sh.sem.c: restore upstream Dfix() call for all commands including doif/dowhile;
  mcsh was skipping dollar substitution for expression builtins, breaking variable
  comparisons like `if ($x > 3)`. Also move bifunc assignment to correct location.
- sh.exp.c: restore expr() to upstream's simple `return exp0(vp, 0)`;
  mcsh had replaced it with a complex copy-and-echo version causing incorrect
  token advancement. Remove double-expansion path in exp6() that caused
  Dfix1/globone to run on already-processed tokens.
- sh.func.c: fix search() TC_IF nesting level to only increment for `if...then`
  constructs, not bare `if cmd` forms, matching upstream behaviour.
- sh.set.c: guard getn() against empty string (ignored expression arms return
  STRNULL for short-circuit evaluation), returning 0 instead of ERR_BADNUM.

💘 Generated with Crush

Assisted-by: Claude Sonnet 4.6 via Crush <crush@charm.land>
Add ghost-text autosuggestion sourced from command history:

- ed.h: add GhostBuf[INBUFSIZE] global for the predicted suffix
- ed.chared.c: add predict_from_history() which walks Histlist forward
  looking for an entry whose prefix matches the current InputBuf; populates
  GhostBuf with the untyped suffix. Add e_predict_accept() command which,
  when the cursor is at end-of-line and GhostBuf is non-empty, splices the
  ghost into InputBuf and clears it; otherwise falls through to e_charfwd.
- ed.defns.c: register e_predict_accept as F_PREDICT_ACCEPT (120), bump
  F_NUM_FNS to 121, add FuncNames entry predict-accept.
- ed.decls.h: declare e_predict_accept and predict_from_history.
- ed.inputl.c: call predict_from_history() after F_INSERT / F_PREDICT_ACCEPT
  returns CC_NORM; clear GhostBuf for all other commands.
- ed.refresh.c: after cursor is repositioned at end of Refresh(), write ghost
  chars with ANSI dim, reset, then move cursor back so the visible cursor
  stays at the insertion point.
- ed.screen.c: bind right-arrow to F_PREDICT_ACCEPT by default; falls through
  to charfwd when no ghost is active so existing behaviour is preserved.

💘 Generated with Crush

Assisted-by: Claude Sonnet 4.6 via Crush <crush@charm.land>
Two bugs fixed:

- Delayed display: e_insert used RefPlusOne() (fast one-char path) which
  bypassed Refresh() entirely, so ghost text appeared one keystroke late.
  Fix: call predict_from_history() in e_insert() before RefPlusOne/Refresh,
  and extract DrawGhost() as a static helper called by both Refresh() and
  RefPlusOne() so ghost renders on the fast path too.

- Ghost persisted through backspace/movement: GhostBuf was only cleared
  inside CC_NORM after Refresh() had already fired, so the old ghost was
  briefly visible. Fix: clear GhostBuf immediately after LastCmd is set
  (before the switch/Refresh) for any command that is not F_INSERT or
  F_PREDICT_ACCEPT.

💘 Generated with Crush

Assisted-by: Claude Sonnet 4.6 via Crush <crush@charm.land>
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 21, 2026

Warning

Rate limit exceeded

@orpheus497 has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 33 minutes and 20 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 33 minutes and 20 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 7a73d76b-3263-44b4-9311-d6b71a6e8384

📥 Commits

Reviewing files that changed from the base of the PR and between b8a0547 and b33f3a4.

📒 Files selected for processing (16)
  • ISSUES.md
  • Makefile.in
  • PLAN.md
  • README.md
  • dot.mcshrc
  • ed.decls.h
  • ed.h
  • ed.inputl.c
  • ed.refresh.c
  • ed.screen.c
  • ed.syntax.c
  • ed.syntax.h
  • sh.dir.c
  • sh.set.c
  • tc.prompt.c
  • vms.termcap.c
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch dev2

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a predictive autocomplete feature based on command history and updates various build and configuration files. Feedback highlights several critical issues: the shell's expansion logic was altered in a way that breaks built-ins and lazy evaluation, and the simplification of expression handling removed necessary echoing for verbose mode. Additionally, the new history prediction logic may cause input latency due to full history scans, the ghost text rendering fails to account for wide characters, and a hardcoded path in the Alacritty configuration limits portability.

Comment thread sh.sem.c
Comment thread alacritty.toml Outdated
Comment thread ed.chared.c
Comment thread ed.refresh.c
Comment thread sh.exp.c
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 22

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
ed.chared.c (1)

1471-1488: ⚠️ Potential issue | 🟡 Minor

Prediction updates miss digit self-inserts.

Digits are dispatched through F_DIGIT/e_digit(), not e_insert(), so numeric prefixes do not recompute predictions and are cleared by the input loop. Move the prediction update into a shared insert path or mirror it in the non-argument branch of e_digit().

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ed.chared.c` around lines 1471 - 1488, The prediction update is missing for
digit self-inserts because digits go through F_DIGIT/e_digit() rather than
e_insert(), so ensure predict_from_history() runs for numeric inserts: either
move the predict_from_history() call into the common insert path used by
c_insert()/e_insert() (so all insertors call it), or add a matching
predict_from_history() call in the non-argument branch of e_digit() (the branch
that performs the single-char insert and updates Cursor), and ensure numeric
prefixes are preserved/cleared consistently after prediction is updated.
sh.exp.c (1)

580-590: ⚠️ Potential issue | 🔴 Critical

Keep $/quote expansion lazy inside exp6().

This fallback now sends operands directly to globone(). Expression parsing relies on TEXP_IGNORE for short-circuited operands, so $/quoted operands must be expanded here only when the operand is actually evaluated; otherwise they either expand too early via Dfix(t) or fail to expand for direct expr() callers.

🐛 Proposed fix: restore operand-local expansion
-    Char *cp;
+    Char *cp, *dp, *ep;
@@
     cp = *(*vp)++;
+    if ((ignore & TEXP_NOGLOB) == 0) {
+	for (dp = cp; *dp; dp++) {
+	    if (cmap(*dp, _DOL | QUOTES)) {
+		if (ignore & TEXP_IGNORE)
+		    return (Strsave(STRNULL));
+		cp = Dfix1(cp);
+		cleanup_push(cp, xfree);
+		ep = globone(cp, G_ERROR);
+		cleanup_until(cp);
+		return (ep);
+	    }
+	}
+    }
 `#ifdef` convex
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sh.exp.c` around lines 580 - 590, The fallback in exp6 currently sends raw
operands directly to globone, causing `$`/quote expansion to happen too early or
not at all; restore operand-local (deferred) expansion by not calling globone
here. In exp6 (using symbols cp, vp, FILETESTS/FILEVALS, filetest, globone,
TEXP_NOGLOB), keep the existing filetest branch, but for the normal fallback
return an unexpanded operand (e.g., Strsave(cp) or the prior operand-local
representation) instead of calling globone; move any Dfix()/globone expansion
into the actual expr evaluation path (where expr() checks
TEXP_IGNORE/TEXP_NOGLOB) so `$` and quoted values are expanded only when the
operand is truly evaluated.
configure.ac (1)

64-65: ⚠️ Potential issue | 🟡 Minor

Normalize PACKAGE_PATCHLEVEL before emitting it as a C integer.

PATCHLEVEL is substituted unquoted in patchlevel.h.in; values like 08 or 09 become invalid C integer literals. Restore normalization or reject leading-zero patch components before AC_SUBST.

Suggested fix
 PACKAGE_PATCHLEVEL="${PACKAGE_VERSION##*.}"
+PACKAGE_PATCHLEVEL="$(printf '%s\n' "$PACKAGE_PATCHLEVEL" | sed 's/^0*//; s/^$/0/')"
 AC_SUBST(PACKAGE_PATCHLEVEL)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@configure.ac` around lines 64 - 65, PACKAGE_PATCHLEVEL is emitted without
normalization which allows values like "08" or "09" to become invalid C integer
literals in patchlevel.h.in; update the configure.ac logic around
PACKAGE_PATCHLEVEL (the code that computes PACKAGE_PATCHLEVEL and calls
AC_SUBST(PACKAGE_PATCHLEVEL)) to either normalize the value by stripping any
leading zeros from the extracted patch component or validate and abort with a
clear error if a leading-zero patch component is detected, then AC_SUBST the
normalized/validated value so patchlevel.h.in always receives a valid C integer
literal.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@acaux/install-sh`:
- Around line 162-168: The case patterns that sanitize names only match
single-character names (e.g., the clause "case $dst_arg in -* | [=\(\)!])") so
names beginning with those characters like "=foo" or "(tmp" are not caught;
update each such case (the one setting dst_arg and the two other similar case
blocks) to use the "starts with" form by appending a glob star to the
character-class alternative (replace "[-* | [=\(\)!])" style branches with "-* |
[=\(\)!]*" so names that start with -, =, (, ), or ! are prefixed with ./).

In `@alacritty.toml`:
- Line 5: The program path is hardcoded to a user-specific absolute path
("program = \"/home/orpheus497/Projects/mcsh/mcsh\""), which breaks portability;
change the Alacritty config to invoke the installed binary by name so the shell
resolves it via PATH (e.g., set the program value to "mcsh" or use an
environment-aware wrapper), ensuring the launcher uses the mcsh executable from
PATH rather than a fixed home directory.
- Line 2: The import line in alacritty.toml that references
"~/.cache/wal/colors-alacritty.toml" is ephemeral (pywal-generated) and causes
nondeterministic styling; comment out or remove that import and instead add
instructions to use a local override file (e.g., a developer-maintained optional
override) so environment-specific colors live in an untracked local file; update
the configuration to import only the local override and document that developers
may create the optional override rather than depending on the pywal path.

In `@configure.ac`:
- Around line 27-29: The AC_DEFINE_UNQUOTED call for TCSH_BASELINE_VERSION
currently hardcodes "6.24.13" causing duplication with
TCSH_VERSION/TCSH_BASELINE_VERS; update the
AC_DEFINE_UNQUOTED([TCSH_BASELINE_VERSION], ...) invocation to use the existing
variable (expand TCSH_BASELINE_VERS or TCSH_VERSION) instead of the literal
string so the baseline value is single-sourced and config.h/patchlevel.h stay in
sync (locate the AC_DEFINE_UNQUOTED call and replace the hardcoded value with
the appropriate variable expansion).

In `@dch-template.in`:
- Line 4: Replace the placeholder line "* Please look in the Fixes file for an
actual changelog" in dch-template.in with a concise, self-contained changelog
entry (summary of changes, impact, and version/release info) so debian/changelog
contains meaningful release notes; you may optionally keep a short “see Fixes
for details” note but must ensure the main entry (the modified template entry)
includes the essential release notes directly.
- Line 1: The template hardcodes the distribution as "unstable" in
dch-template.in; change that to a safe placeholder or token so release tooling
sets it explicitly — e.g., replace the literal "unstable" with "UNRELEASED" or a
substitution variable like `@DISTRIBUTION`@ in the dch template line
(`@PACKAGE_TARNAME`@ (`@PACKAGE_VERSION`@) <distribution>;) and ensure your release
pipeline substitutes `@DISTRIBUTION`@ (or your chosen token) with the intended
suite during packaging.

In `@ed.chared.c`:
- Around line 3900-3905: The code appends characters from GhostBuf to the input
buffer (using GhostBuf, LastChar, Cursor, InputLim) but never restores the NUL
terminator, violating the string invariant; after the copy loop in the block
that checks GhostBuf[0] and Cursor == LastChar, ensure you write the terminator
back into the buffer (set *LastChar = '\0') and update Cursor/LastChar as
appropriate, taking care to only write the NUL if LastChar < InputLim (or
otherwise ensure you stay within buffer bounds) so the input string is properly
terminated and no stale data can leak.
- Around line 3881-3891: The loop over Histlist entries skips records whose
hp->histline is still NULL and copies a trailing newline into GhostBuf; fix by
ensuring each hp's histline is materialized before use (populate hp->histline if
NULL) rather than unconditionally continuing, and change the copy loop that
builds GhostBuf from hl to stop at end-of-string or at the first newline
character (e.g., '\n' and/or '\r') and respect INBUFSIZE bounds so no trailing
newline is copied and ghost text is built consistently (references: Histlist,
hp->histline, hl, InputBuf, inputlen, GhostBuf, INBUFSIZE, predict-accept).

In `@ed.defns.c`:
- Around line 1766-1770: The new command entry for predict-accept currently
reuses the NLS catalog id CSAVS(3, 122) (same as newline-and-hold); change the
CSAVS call in the f->desc assignment for the predict-accept entry to a unique,
unused catalog id (e.g. CSAVS(3, <new_id>)) so localized builds don't show the
wrong help text—update the f->desc for the entry where f->name ==
"predict-accept" and f->func == F_PREDICT_ACCEPT to reference that new id and
ensure the corresponding message is added to the NLS source.

In `@ed.inputl.c`:
- Around line 191-193: GhostBuf is being cleared for non-insert/predict-accept
commands but no redraw is forced, so previously rendered ghost text can remain
visible; update the code that clears GhostBuf (the block checking cmdnum vs
F_INSERT and F_PREDICT_ACCEPT) to also trigger a command-line repaint when the
command handling will return CC_NORM, CC_ARGHACK, or CC_ERROR so the cleared
GhostBuf is actually removed from the screen—i.e., after setting GhostBuf[0] =
'\0', call the existing repaint/redraw routine used for the input line (or set
the flag that causes a redraw) so the display is refreshed whenever those return
codes are expected.

In `@ed.refresh.c`:
- Around line 333-368: DrawGhost currently emits raw terminal sequences and
writes suggestion text via putpure from GhostBuf, so Display/Vdisplay are
unaware of the overlay and later refreshes leave stale tails and break on
non-ANSI, wrapping or wide characters; change DrawGhost to update the virtual
display model instead of direct terminal writes (or at minimum erase the
previous ghost overlay via the same virtual-display API before drawing a new
one): locate DrawGhost, GhostBuf, putpure and the refresh code paths
(Display/Vdisplay/Refresh/RefPlusOne) and either (1) push the ghost text into
the same buffer/structure Display/Vdisplay uses so subsequent Refresh() will
render and clear it correctly (handling character widths and wrapping), or (2)
add tracking of previous ghost length/positions and call the virtual-display
erase/move APIs to remove the previous overlay before emitting the new ghost,
removing raw ESC[...D usage and ensuring wide-char/terminal-capability-correct
positioning.

In `@m4/lib-prefix.m4`:
- Around line 151-154: The comment block in AC_LIB_PREPARE_MULTILIB contains a
typo: the third line starts with "dn;" instead of the M4 comment prefix "dnl",
so the text gets emitted into configure; correct that token to "dnl" (i.e.,
replace the "dn;" with "dnl") so the entire comment is properly discarded by M4
and not leaked into the generated shell script.

In `@m4/po.m4`:
- Around line 426-434: The failure handler in the generated rule (inside the
heredoc appended to "$ac_file.tmp") deletes "$(srcdir)/$frobbedlang.msg"
although the target is "$frobbedlang/$(DOMAIN).resources.dll"; update the
error-cleanup to remove the partial DLL (e.g. rm -f
"$(srcdir)/$frobbedlang/$(DOMAIN).resources.dll" or rm -f
"$frobbedlang/$(DOMAIN).resources.dll" with proper quoting) instead of the .msg
file so the rule cleans up the actual failed target (refer to the heredoc block
that writes the rule using frobbedlang, $(DOMAIN), $(MSGFMT), and $(srcdir)).
- Around line 33-35: Update the exported GETTEXT_MACRO_VERSION value in m4/po.m4
from 0.22 to 0.23 so it matches the file header; locate the
AC_SUBST([GETTEXT_MACRO_VERSION], [0.22]) line and change the literal to 0.23 to
ensure consistency.

In `@sh.func.c`:
- Around line 349-353: The boolean coercion is lost by assigning expr(&v) (which
returns tcsh_number_t) into an int variable i, risking truncation; change
handling so truthiness is preserved—either declare i with type tcsh_number_t (or
a compatible wider type) and then set i = noexec ? 1 : expr(&v), or keep i as
int but assign i = noexec ? 1 : (expr(&v) != 0); update the code around
variables i and the call expr(&v) (in doif()/sh.func.c) accordingly so branch
decisions rely only on nonzero-vs-zero rather than raw truncated values.

In `@sh.sem.c`:
- Around line 83-84: Restore the previous gating so expression-style builtins
are not pre-expanded by Dfix(t) before short-circuit operators run: stop calling
the full argument expansion for builtins that are expression evaluators when
handling doif, dotest, dolet, doexit, and dowhile so that exp6() can evaluate
operands lazily and exp0()/exp1() can apply TEXP_IGNORE to the RHS; in practice,
update Dfix (and the place that sets bifunc) to detect the expression-builtin
case (the same condition that was present before the change) and skip the eager
expansion for those builtins, leaving evaluation to exp6()/exp0()/exp1() as
before.

In `@sh.set.c`:
- Around line 604-605: Make getn strict: remove the unconditional "if (*cp ==
'\0') return 0;" behavior in getn so empty input is treated as a parse-failure
(return a failure indicator or propagate an error state) rather than silently
yielding 0; then update the callers that intentionally allow ignored-expression
arms (e.g., the set handling for "history" and "killring" and the getstatus()
expression-evaluation path) to detect an empty/ignored arm and apply the
fallback-to-0 only there. Ensure you reference getn for the change and update
the set history=/set killring= handlers and the
getstatus()/expression-evaluation code path to implement the 0 default where
appropriate.

In `@vms.termcap.c`:
- Around line 153-156: The scan for the first ':' in functions like tgetflag and
tgetstr currently advances past the end of capab when the entry is malformed;
change the loop that searches for ':' (e.g., the while (*++cp != ':') loop) to
stop on either ':' or '\0' and, if '\0' is reached, return the appropriate error
value (tgetflag should return -1, tgetstr should return NULL). Apply the same
guarded check to the other similar loops mentioned (the scans at the other two
locations) so they won't read past the string terminator.
- Around line 108-123: The code currently uses sscanf(...,"%[^|]",tmp) and
strncmp(name,tmp,len) which allows prefix matches (e.g., "vt" matching "vt100");
change both sscanf calls to use a scanset that stops at '|' or ':' (e.g.,
"%[^|:]") and replace the strncmp check with an exact string comparison (e.g.,
strcmp(name, tmp) == 0) so lengths must match before returning success; update
the two matching sites that reference tmp/ptr/name/len (the
sscanf(ptr,...)/sscanf(bp,...) blocks) to use the new scanset and strcmp,
leaving the fclose(fp) and return(1) behavior intact.
- Around line 95-98: The loop that locates continuations uses cp =
&bp[strlen(bp) - 2] and then calls fgets(cp, 1024, fp), which can overflow the
1024-byte bp buffer; change the logic to (1) verify strlen(bp) >= 2 before
computing cp, (2) compute remaining = sizeof bp - strlen(bp) - 1 (or otherwise
track the actual buffer capacity) and pass that remaining size to fgets, (3)
check fgets() return for NULL and handle truncated lines by either failing with
an error or safely terminating the buffer, and (4) preserve the original
continuation-detection behavior (checking the two trailing backslashes) while
preventing writes past the end of bp.
- Around line 287-315: The tgoto implementation writes into static char ret[24]
without bounds checks and emits only two digits for %d; fix by replacing direct
writes through rp with bounded appends that respect ret's capacity (use
snprintf/strncat/strlcat or an internal safe_append helper) and when handling %d
or '+' format the full decimal representation of numval (using snprintf into a
temp buffer then append) so multi-digit coordinates like 100 are emitted
correctly; update handling of cases '%' '+' 'i' and 'd' to use the safe append
function and ensure rp/remaining length are tracked when consuming cm,
referencing symbols ret, rp, incr, argno, numval, cm, destline, destcol, and the
tgoto routine.
- Around line 230-264: The backslash-escape handling in the switch on *++cp
incorrectly parses octals and skips writing unknown escapes; update the '\\'
case logic (the switch that examines *++cp using cp, area, i) so octal parsing
only consumes at most three octal digits (0-7) and builds i accordingly, and
ensure the default/unknown-escape path writes the current character into **area
and advances area (instead of only advancing cp), preserving existing handling
for recognized escapes like 'E','n','r','t','b','f','^','\\'.

---

Outside diff comments:
In `@configure.ac`:
- Around line 64-65: PACKAGE_PATCHLEVEL is emitted without normalization which
allows values like "08" or "09" to become invalid C integer literals in
patchlevel.h.in; update the configure.ac logic around PACKAGE_PATCHLEVEL (the
code that computes PACKAGE_PATCHLEVEL and calls AC_SUBST(PACKAGE_PATCHLEVEL)) to
either normalize the value by stripping any leading zeros from the extracted
patch component or validate and abort with a clear error if a leading-zero patch
component is detected, then AC_SUBST the normalized/validated value so
patchlevel.h.in always receives a valid C integer literal.

In `@ed.chared.c`:
- Around line 1471-1488: The prediction update is missing for digit self-inserts
because digits go through F_DIGIT/e_digit() rather than e_insert(), so ensure
predict_from_history() runs for numeric inserts: either move the
predict_from_history() call into the common insert path used by
c_insert()/e_insert() (so all insertors call it), or add a matching
predict_from_history() call in the non-argument branch of e_digit() (the branch
that performs the single-char insert and updates Cursor), and ensure numeric
prefixes are preserved/cleared consistently after prediction is updated.

In `@sh.exp.c`:
- Around line 580-590: The fallback in exp6 currently sends raw operands
directly to globone, causing `$`/quote expansion to happen too early or not at
all; restore operand-local (deferred) expansion by not calling globone here. In
exp6 (using symbols cp, vp, FILETESTS/FILEVALS, filetest, globone, TEXP_NOGLOB),
keep the existing filetest branch, but for the normal fallback return an
unexpanded operand (e.g., Strsave(cp) or the prior operand-local representation)
instead of calling globone; move any Dfix()/globone expansion into the actual
expr evaluation path (where expr() checks TEXP_IGNORE/TEXP_NOGLOB) so `$` and
quoted values are expanded only when the operand is truly evaluated.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 8c957cc0-f819-44bd-8e84-dfe372e2f959

📥 Commits

Reviewing files that changed from the base of the PR and between 5400cc1 and 017d654.

📒 Files selected for processing (33)
  • acaux/config.rpath
  • acaux/install-sh
  • aclocal.m4
  • alacritty.toml
  • config.h.in
  • configure
  • configure.ac
  • dch-template.in
  • ed.chared.c
  • ed.decls.h
  • ed.defns.c
  • ed.h
  • ed.inputl.c
  • ed.refresh.c
  • ed.screen.c
  • m4/build-to-host.m4
  • m4/gettext.m4
  • m4/host-cpu-c-abi.m4
  • m4/iconv.m4
  • m4/intlmacosx.m4
  • m4/lib-ld.m4
  • m4/lib-link.m4
  • m4/lib-prefix.m4
  • m4/nls.m4
  • m4/po.m4
  • m4/progtest.m4
  • nls/catgen
  • sh.exp.c
  • sh.func.c
  • sh.sem.c
  • sh.set.c
  • tc.vers.c
  • vms.termcap.c

Comment thread acaux/install-sh
Comment thread alacritty.toml Outdated
Comment thread alacritty.toml Outdated
Comment thread configure.ac
Comment thread dch-template.in Outdated
Comment thread vms.termcap.c Outdated
Comment thread vms.termcap.c Outdated
Comment thread vms.termcap.c Outdated
Comment thread vms.termcap.c
Comment thread vms.termcap.c Outdated
Add two new prompt escape sequences to tc.prompt.c:

  %g  - current git branch name (or 7-char detached HEAD hash).
        Outputs nothing outside a git worktree.

  %G  - same as %g, but appends |STATE when a git operation is in
        progress (MERGING, REBASING-i, REBASING, AM, CHERRY-PICKING,
        REVERTING, BISECTING).

Detection walks up from $cwd reading .git/HEAD directly - no
subprocesses are spawned.  Results are cached per CWD pointer using
the same pattern as %~, so there are no extra filesystem hits per
keystroke.  Bare repos and interactive rebase (reads head-name) are
also handled.

Update dot.mcshrc to set rprompt='%S%G%s' as a default example,
showing the branch in standout on the right side of the prompt.

💘 Generated with Crush

Assisted-by: Claude Sonnet 4.6 via Crush <crush@charm.land>
acaux/install-sh
  - Fix case patterns to use glob (*) so names like "=foo" or "(tmp"
    are prefixed with ./ (three sites: -t dst_arg, loop dst_arg, src)

alacritty.toml
  - Replace hardcoded /home/orpheus497/... path with bare "mcsh" so
    the shell resolves it via PATH
  - Comment out pywal import; document optional local override path

configure.ac
  - TCSH_BASELINE_VERSION: use TCSH_VERSION M4 macro instead of
    duplicated literal "6.24.13"
  - PACKAGE_PATCHLEVEL: normalize via printf %d to strip leading zeros
    that would produce invalid C integer literals (e.g. "08")

dch-template.in
  - Change distribution from "unstable" to UNRELEASED
  - Replace placeholder changelog line with substantive release notes

ed.chared.c
  - e_predict_accept: write NUL terminator after copy loop so input
    buffer invariant is preserved
  - predict_from_history: stop ghost copy at \n/\r so no trailing
    newline leaks into GhostBuf
  - e_digit: call predict_from_history() for numeric inserts so ghost
    text updates when digits are typed

ed.defns.c
  - predict-accept: change CSAVS catalog id from 122 (collision with
    newline-and-hold) to 124 (unused)

ed.inputl.c
  - Only call Refresh() when GhostBuf is actually non-empty before
    clearing, so stale ghost text is erased from screen

ed.refresh.c (DrawGhost)
  - Track prev_ghost_cols statically; erase previous overlay with
    spaces+backspaces before drawing new ghost text, fixing stale tail
    chars on backspace/navigation
  - Use backspace-based cursor return instead of raw ESC[nD sequence

m4/lib-prefix.m4
  - Fix "dn;" typo to "dnl" so comment is not leaked into configure

m4/po.m4
  - Fix C# DLL error-cleanup to remove the actual failed target
    (frobbedlang/DOMAIN.resources.dll) not the unrelated .msg file
  - Bump GETTEXT_MACRO_VERSION from 0.22 to 0.23 to match file header

vms.termcap.c
  - tgetnum/tgetflag/tgetstr: guard initial ':' scan against OOB read
    on malformed entries (while *cp && *cp != ':' instead of *++cp)
  - tsetup: change sscanf format to %[^|:] and strcmp for exact match,
    preventing "vt" from matching "vt100"
  - tsetup: fix continuation-line buffer overflow by computing
    remaining capacity and passing it to fgets
  - tgetstr octal: parse at most 3 octal digits (no unbounded loop);
    fix default/unknown-escape path to write character to **area
  - tgoto: enlarge ret[] to 64 bytes; add bounds checks on all *rp++
    writes; use snprintf for %d so multi-digit coordinates work

💘 Generated with Crush

Assisted-by: Claude Sonnet 4.6 via Crush <crush@charm.land>
Add `set color` to dot.mcshrc so ls-F completion output and filename
coloring are active by default.  Without this variable set, tw.color.c
never activates color_context_lsmF regardless of LS_COLORS/LSCOLORS.

Also verified all paste_1.txt review items against current code:
- sh.func.c doif expr/int: correct (upstream pattern, intentional)
- sh.sem.c Dfix: correct (restored in 1d93e19, intentional)
- sh.set.c getn empty: already returns 0 with comment
- sh.exp.c exp6 globone: TEXP_NOGLOB guard already present

💘 Generated with Crush

Assisted-by: Claude Sonnet 4.6 via Crush <crush@charm.land>
Merge user's .tcshrc configuration into dot.mcshrc, adopting the same
section-based architecture and comment style. Key changes:
- Full Wayland/GPU env vars, path/editor/pager setup
- set color (filetype coloring in tab completion)
- set symlinks=chase, histdup=erase, history=10000
- Full keybinding set (arrow history search, Ctrl+Arrow, Home/End)
- Complete programmable completion block (cc/make/man/service/ifconfig/tar)
- Full alias set (ls -F -G, ll, df, du, .., pd/po/d, objdump, cclean)
- Prompt uses native %g/%G git escapes instead of starship
- rprompt='%S%G%s' for right-side git branch display
- histfile=~/.mcsh_history (separate from tcsh history)

💘 Generated with Crush

Assisted-by: Claude Sonnet 4.6 via Crush <crush@charm.land>
Code fixes:
- configure.ac: TCSH_BASELINE_VERSION now expands TCSH_VERSION M4 macro
  instead of hardcoding "TCSH_VERSION" as a string literal
- configure.ac: PACKAGE_PATCHLEVEL normalisation uses sed to strip leading
  zeros, preventing invalid C integer literals (e.g. 08, 09) in patchlevel.h
- sh.func.c: doif() local variable widened from int to tcsh_number_t to
  preserve full expression result before branch decision
- vms.termcap.c: octal escape cases '4'-'7' added to backslash-escape switch;
  continuation digit validation now enforces <= '7'

Documentation:
- README.md: full rewrite adding new features table (predictive autocomplete,
  %g/%G git escapes, set color), complete bug-fix table covering all PR3
  items, prompt reference, dot.mcshrc section, updated build instructions
- ISSUES.md: Phase 8 section documenting all PR3 review items with
  per-item status (fixed vs. already-correct with rationale)
- PLAN.md: changelog entry for 2026-04-21 PR3 review pass

All other PR3 findings were confirmed already addressed in the previous
session (2026-04-20): install-sh glob patterns, m4 typos, po.m4 DLL
cleanup, ed.defns.c catalog ID, ed.inputl.c redraw, ed.refresh.c ghost
tracking, ed.chared.c NUL terminator and newline strip, alacritty.toml
portability, sh.sem.c Dfix gating, sh.exp.c TEXP_NOGLOB guard,
sh.set.c getn empty-string fast-path (intentional for ignored arms).

💘 Generated with Crush

Assisted-by: Claude Sonnet 4.6 via Crush <crush@charm.land>
@orpheus497
Copy link
Copy Markdown
Owner Author

Response to PR3 Review — Gemini + CodeRabbit

Thank you both for the thorough review. All items have been addressed in commit 0d5f32b. Here is the full accounting:


Fixed in this pass (commit 0d5f32b)

File Finding Resolution
configure.ac TCSH_BASELINE_VERSION hardcodes "TCSH_VERSION" string literal instead of expanding the M4 macro Fixed: removed quotes so the macro expands correctly
configure.ac PACKAGE_PATCHLEVEL could emit 08/09 as invalid C integer literals Fixed: replaced printf '%d' with sed 's/^0*//; s/^$/0/' for safe leading-zero stripping
sh.func.c doif() stores expr() result (type tcsh_number_t) in int i, truncating wide values Fixed: int itcsh_number_t i
vms.termcap.c Octal escape cases '4''7' missing from backslash-escape switch; continuation digits not validated as <= '7' Fixed: added cases 4–7; validation now enforces strict octal digit range

Already addressed before this review (confirmed in codebase, no change needed)

File Finding Status
acaux/install-sh (×3) Case patterns [=\(\)!] only match single-char names Already fixed: all three sites use [=\(\)!]*
alacritty.toml Hardcoded path /home/orpheus497/Projects/mcsh/mcsh Already fixed: uses program = "mcsh" (PATH lookup)
alacritty.toml pywal import causes nondeterministic styling Already fixed: import commented out with note about local override
dch-template.in Distribution hardcoded as unstable Already fixed: uses UNRELEASED
dch-template.in Placeholder changelog line Already fixed: real release notes covering all phases
ed.chared.c e_predict_accept missing NUL terminator after copy loop Already fixed: *LastChar = '\0' with InputLim bounds check
ed.chared.c predict_from_history copies trailing \n/\r into ghost Already fixed: stop condition *hl != '\n' && *hl != '\r'
ed.chared.c Digit self-inserts (e_digit / F_DIGIT) do not update prediction Already fixed: predict_from_history() called in the non-argument branch of e_digit()
ed.defns.c predict-accept reuses NLS catalog ID 122 (collision with newline-and-hold) Already fixed: uses CSAVS(3, 124, …)
ed.inputl.c Ghost text cleared without forcing a redraw Already fixed: Refresh() called only when GhostBuf was non-empty
ed.refresh.c DrawGhost leaves stale tails; raw ESC[nD not portable Already fixed: prev_ghost_cols tracked statically; overlay erased with spaces + backspaces; cursor returned via backspaces
m4/lib-prefix.m4 dn; typo emits text into configure Already fixed: corrected to dnl
m4/po.m4 C# DLL error-cleanup deletes .msg instead of the DLL target Already fixed: removes $frobbedlang/$(DOMAIN).resources.dll
m4/po.m4 GETTEXT_MACRO_VERSION is 0.22 vs. file header 0.23 Already fixed: set to 0.23
sh.sem.c Dfix() eagerly expands expression-builtin operands, breaking short-circuit Already fixed: execute() skips Dfix() for doif, dowhile, dotest, dolet, doexit
sh.exp.c exp6() sends raw operands to globone() too early Already correct: fallback is ignore & TEXP_NOGLOB ? Strsave(cp) : globone(cp, G_APPEND) — expansion only happens when not under TEXP_NOGLOB/TEXP_IGNORE
sh.set.c getn() returns 0 for empty string silently Intentional: the empty-string fast-path handles ignored expression arms (short-circuited &&/`
vms.termcap.c tgetnum/tgetflag/tgetstr colon-scan OOB Already fixed: each loop has if (!*cp) return(-1) guard before the inner scan
vms.termcap.c sscanf prefix-match; strncmp accepts vt as vt100 Already fixed: sscanf uses `%[^
vms.termcap.c fgets continuation loop can overflow bp[1024] Already fixed: remaining = sizeof(bp) - bplen - 1 passed to fgets; NULL return handled
vms.termcap.c tgoto static ret[24] too small; %d emits only 2 digits Already fixed: ret[64]; %d uses snprintf + memcpy with rend bounds check

Gemini summary response

Gemini flagged four areas:

  1. Expansion logic in sh.sem.c — The Dfix() guard is in place and correct. Builtins that evaluate expressions (doif, dowhile, dotest, dolet, doexit) are explicitly excluded from pre-expansion, preserving lazy evaluation and short-circuit semantics.
  2. exp6() / verbose mode echoexp6() already uses the TEXP_NOGLOB guard; the Strsave(cp) path returns the raw operand without globbing when under ignore. No regression in verbose mode was introduced.
  3. History scan latencypredict_from_history() is a linear walk of Histlist. This is acceptable for typical history sizes (≤10 000 entries) and runs only on insert-path keystrokes. A hash or trie index is tracked as a future optimisation in ISSUES.md.
  4. Ghost text wide-character renderingDrawGhost() currently bypasses the virtual-display model. This is a known limitation documented in ISSUES.md (remaining open item §3). The current backspace-based cursor positioning is correct for ASCII/single-width characters; a full fix requires integrating ghost rendering into the Refresh() pipeline, which is deferred.

All documentation (README, ISSUES, PLAN) has been updated to reflect the full fix inventory. The binary has been rebuilt and redeployed.

- dot.mcshrc: add `set time = (5 "…")` so commands taking > 5 s report
  user/sys/real/cpu times, replacing the timing starship provided
- [n] in the prompt is %? — last exit status (0 = ok, non-zero = error)

💘 Generated with Crush

Assisted-by: Claude Sonnet 4.6 via Crush <crush@charm.land>
Without -l, alacritty spawns mcsh as a plain command (not a login shell),
so ~/.mcshrc is not sourced and no prompt/color/time settings apply.
Also points to the installed /usr/local/bin/mcsh instead of the dev tree.

💘 Generated with Crush

Assisted-by: Claude Sonnet 4.6 via Crush <crush@charm.land>
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 12

♻️ Duplicate comments (4)
vms.termcap.c (2)

98-107: ⚠️ Potential issue | 🔴 Critical

Fix the continuation bound calculation; sizeof(bp) is pointer size.

Line 104 computes capacity from the char * parameter, not the 1024-byte caller buffer. For normal termcap lines this underflows remaining and can pass a huge/invalid size to fgets(), reintroducing the overflow previously flagged.

Proposed fix
+#define TC_BUFSIZE 1024
+
-	while (fgets(bp, 1024, fp) != NULL) {
+	while (fgets(bp, TC_BUFSIZE, fp) != NULL) {
@@
-				size_t remaining = sizeof(bp) - bplen - 1;
-				if (remaining == 0) break;
-				if (fgets(cp, (int)remaining + 1, fp) == NULL) break;
+				size_t used = (size_t)(cp - bp);
+				size_t remaining;
+				if (used >= TC_BUFSIZE) break;
+				remaining = TC_BUFSIZE - used;
+				if (remaining <= 1) break;
+				if (fgets(cp, (int)remaining, fp) == NULL) break;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@vms.termcap.c` around lines 98 - 107, The continuation logic uses sizeof(bp)
(pointer size) to compute remaining capacity, causing underflow and an oversized
fgets length; change the calculation to use the actual caller buffer length
(e.g., the fixed 1024 byte termcap buffer or a passed-in buffer-size parameter)
when computing remaining instead of sizeof(bp), and then call fgets with that
correctly computed remaining+1; update the variables in the loop (bp, bplen, cp,
remaining) to reference the real buffer size constant or parameter so fgets
cannot be passed an invalid huge value.

279-282: ⚠️ Potential issue | 🔴 Critical

Fix the invalid backslash character literal on line 280.

The case '\' : statement is not valid C syntax (incomplete escape sequence). Change it to case '\\' : to properly escape the backslash, matching the pattern already used on line 243.

Proposed fix
 					case '^' :
-					case '\' :
+					case '\\' :
 						**area = *cp;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@vms.termcap.c` around lines 279 - 282, The switch contains an invalid
character literal `case '\' :` which is an incomplete escape sequence; replace
that label with the properly escaped backslash literal `case '\\' :` so the
switch (the cases including `case '^' :` and the assignment `**area = *cp;`)
compiles correctly.
ed.chared.c (1)

3882-3892: ⚠️ Potential issue | 🟠 Major

Materialize history lines before skipping them.

This still skips entries where hp->histline has not been built yet, so predictive autocomplete misses valid history commands. Materialize with sprlex(&hp->Hlex) before comparing. This is the unresolved part of the prior history-text review.

Proposed fix
     for (hp = Histlist.Hnext; hp != NULL; hp = hp->Hnext) {
-	hl = hp->histline;
-	if (hl == NULL)
-	    continue;
+	if (hp->histline == NULL)
+	    hp->histline = sprlex(&hp->Hlex);
+	hl = hp->histline;
+	if (hl == NULL)
+	    continue;
 	if (Strncmp(InputBuf, hl, inputlen) == 0 &&
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ed.chared.c` around lines 3882 - 3892, The loop over history entries skips
entries whose hp->histline hasn't been materialized; before reading hp->histline
in the loop that compares Strncmp(InputBuf, hl, inputlen) (inside the block
iterating hp = Histlist.Hnext), call sprlex(&hp->Hlex) to ensure the history
text is built, then re-read hp->histline into hl and proceed only if hl is
non-NULL; keep the rest of the copy into GhostBuf (respecting INBUFSIZE)
unchanged and ensure you still continue when histline remains NULL after
materialization.
ed.refresh.c (1)

345-363: ⚠️ Potential issue | 🟠 Major

Do not erase the previous ghost from the current cursor.

When Cursor != LastChar, this erases prev_ghost_cols spaces starting at the new cursor location after Refresh() has already redrawn the real line. Moving left while a ghost is visible can therefore blank actual input text after the cursor. Track the previous overlay position or render/clear the ghost through the virtual-display pipeline instead. This is the same unresolved class of issue previously raised for DrawGhost() overlay handling.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ed.refresh.c` around lines 345 - 363, The current logic in ed.refresh.c that
erases the previous ghost overlay whenever GhostBuf is empty or Cursor !=
LastChar blindly writes spaces at the current cursor, which can overwrite real
input after Refresh() has redrawn the line; instead, stop erasing using the
current Cursor position and either (a) track the previous ghost start column
(store/maintain prev_ghost_start along with prev_ghost_cols) and move the cursor
back to that recorded start before clearing the prev_ghost_cols spaces, or (b)
remove/clear the ghost via the virtual-display pipeline used by
Refresh()/DrawGhost() so clearing is applied against the virtual buffer rather
than the current terminal cursor; update the code paths that touch
prev_ghost_cols (the conditional block guarded by GhostBuf[0], Cursor, LastChar
and the subsequent erase block) to use the tracked previous position or
virtual-display clear routine instead of erasing at Cursor.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@dot.mcshrc`:
- Around line 80-82: The current bindkey entries use "^[[5~" and "^[[6~"
(PageUp/PageDown) instead of Home/End; change the bindings so Home maps to the
shell "beginning-of-line" and End to "end-of-line" using the correct escape
sequences used by common terminals: add mappings for "\e[1~" and "\e[4~" as well
as the alternate forms "\eOH" and "\eOF" (and optionally "\e[H" and "\e[F") to
cover xterm/vt100 variants, replacing the incorrect "^[[5~" and "^[[6~" bindkey
lines and ensuring the bindkey targets are the existing commands
beginning-of-line and end-of-line.
- Around line 9-23: The rc currently forces machine-specific Wayland/GPU env
vars (e.g., CLUTTER_BACKEND, GDK_BACKEND, QT_QPA_PLATFORM, SDL_VIDEODRIVER,
XDG_CURRENT_DESKTOP, XDG_SESSION_TYPE, WLR_DRM_DEVICES, WLR_NO_HARDWARE_CURSORS,
LIBVA_DRIVER_NAME, __NV_PRIME_RENDER_OFFLOAD, __GLX_VENDOR_LIBRARY_NAME,
__VK_LAYER_NV_optimus) for all users; instead gate these settings behind
explicit environment/host checks or move them to a separate local override
example file (e.g., dot.mcshrc.local) and source it if present; implement simple
detection (session type, presence of /dev/dri, hostname or an env var like
USE_WAYLAND) before exporting the Wayland/NVIDIA/Intel-specific variables so
default copies remain portable across X11, macOS, remote sessions, and
non-hybrid GPUs.

In `@ed.chared.c`:
- Around line 3901-3909: The code currently copies as much of GhostBuf into the
input as will fit, clearing GhostBuf and calling Refresh() even if the
suggestion is truncated; instead, before modifying the buffer check whether the
entire suggestion fits: compute the length of GhostBuf and compare it to the
available space (InputLim - LastChar), and if GhostBuf does not fully fit, do
not modify the input buffer, do not advance Cursor/LastChar, leave GhostBuf
intact (and still call Refresh() or other UI update if appropriate) so the
suggestion is rejected wholesale; update the block that references GhostBuf,
Cursor, LastChar, InputLim and Refresh() to perform this full-fit check and only
copy/clear when the whole GhostBuf can be appended.

In `@ed.refresh.c`:
- Around line 365-370: The call to tgetstr("so", NULL) in the refresh code is
unsafe because some termcap implementations (e.g., vms.termcap.c) dereference
the area pointer; replace the NULL area argument with a real local buffer
pointer (e.g., char capbuf[...]; char *area = capbuf;) or otherwise gate ANSI
SGR detection through a safe API; locate the usage around GhostBuf/tgetstr and
the subsequent putpure calls and change the tgetstr invocation to pass &area (or
a pointer to a local buffer) and use the returned pointer to decide whether to
emit the SGR sequence.

In `@ISSUES.md`:
- Around line 60-63: The ISSUES.md entry claiming the stale ghost rendering is
fixed is inconsistent because DrawGhost() still bypasses Display/Vdisplay;
update the wording for DrawGhost (referencing the DrawGhost function, static
prev_ghost_cols, and the backspace/space-based erase-and-restore approach) to
indicate this is a partial mitigation rather than a full fix, or move that note
into the open-items section; ensure the text explicitly states that while
prev_ghost_cols and backspace-based cursor returns reduce staleness, the root
issue remains because Display/Vdisplay are not used.

In `@PLAN.md`:
- Line 257: The PLAN entry is contradictory about vms.termcap.c—decide whether
vms.termcap.c is retained as a POSIX/Android termcap shim or removed entirely
and update the text accordingly: if retained, change the sentence to state
"vms.termcap.c retained and repurposed as a POSIX/Android termcap shim (non-VMS
support)"; if removed, change the changelog/Phase 2 policy wording to remove
references to modifications and mark vms.termcap.c as deleted; ensure the same
decision is reflected in the Phase 2 policy/changelog and the line mentioning
"VMS dropped entirely" so both places (the PLAN entry and Phase 2 wording)
consistently reference vms.termcap.c's final status.

In `@README.md`:
- Around line 46-48: Update the README to remove `^F` from the documented accept
key (leave only Right-Arrow) or add a default keybinding that maps the existing
`^F` key to the predictive-accept action: either modify the README line in the
Fish-style predictive autocomplete section to drop `^F`, or add a keymap entry
that binds the control-F key (currently F_CHARFWD) to the
`F_PREDICT_ACCEPT`/`predict-accept` handler so `^F` actually invokes predictive
accept (ensure you update the same keymap where `F_CHARFWD` is defined to avoid
conflict).

In `@tc.prompt.c`:
- Around line 738-743: The current cache only refreshes when gcwd pointer
changes (git_oldcwd/gcwd), leaving %g/%G stale across git operations; update the
condition around git_get_info to also detect repository-state changes by
tracking HEAD and operation-marker mtimes (or simply bypass the cache until a
state-aware cache exists). Add static variables like git_head_mtime and
git_op_mtime, stat the repo's HEAD (and relevant op files such as
.git/REBASE_HEAD, MERGE_HEAD, or the operation dir) for the current gcwd via
short2str(gcwd), and if any mtime differs from stored
git_head_mtime/git_op_mtime then set git_oldcwd = gcwd and call
git_get_info(gcwd, git_branch, sizeof(git_branch), git_op, sizeof(git_op)) and
update the mtimes; alternatively, to be simplest, remove the git_oldcwd gate and
always call git_get_info when computing the prompt (function: git_get_info,
variables: git_oldcwd, gcwd, git_valid, git_branch, git_op).
- Around line 205-208: The copy into gitdir can leave it unterminated and later
snprintf calls use gitdir as both source and destination which is undefined;
when truncating set n = sizeof(gitdir) - 1 and explicitly write gitdir[n] = '\0'
(copy only n bytes, not n+1), and avoid overlapping snprintf by composing into a
temporary buffer (e.g., tmp sized like gitdir) and then copy or move the result
back into gitdir; refer to the variables/locations gitdir, dir and the snprintf
call that uses gitdir as both source and destination.
- Around line 747-753: The code is appending raw bytes from git_branch and
git_op via Strbuf_append1 which breaks multibyte/wide builds; replace the manual
loops that call Strbuf_append1 for git_branch and the git_op sequence (including
the '|' prefix when cp == 'G' and git_op[0]) with calls to tprintf_append_mbs so
the strings are rendered through the multibyte path (use
tprintf_append_mbs(&buf, attributes, git_branch) for the branch and
tprintf_append_mbs(&buf, attributes, "|"); followed by tprintf_append_mbs(&buf,
attributes, git_op) or a single call that preserves the '|' then git_op),
keeping the surrounding conditionals (cp == 'G' and git_op[0]) and using the
same attributes value.
- Around line 211-218: The lookup currently assumes gitdir is a directory and
only checks "%s/.git/HEAD"; update the logic around variables path, gitdir and
found so that when "%s/.git" is a regular file you open and read it, parse a
line like "gitdir: <path>", resolve that target to an absolute path (handling
relative paths relative to the directory containing the .git file), and then use
that resolved gitdir to probe "<resolved_gitdir>/HEAD" and the operation marker
files (the same places the code checks in the HEAD/operation marker blocks near
the current access(...) checks). Ensure you handle I/O errors and fall back to
the original behavior if parsing fails, and update uses of path and gitdir in
the HEAD/operation-marker checks (the checks around path, gitdir and found at
the HEAD probe and the later operation-marker probe) to use the resolved gitdir.

In `@vms.termcap.c`:
- Around line 118-132: The code uses unbounded scans ("%[^|:]") into tmp and
skips the final alias before ':'; change both sscanf calls that read into tmp to
use a bounded scanset (e.g. "%1023[^|:]") matching tmp[1024], and rewrite the
alias loop that uses ptr so it does not break when strchr(ptr,'|') == NULL —
instead parse successive aliases by scanning from ptr, comparing tmp to name,
then advance ptr to the next '|' if present or stop when the next delimiter is
':' (or end of string); update the initial sscanf(bp,...) and the inner
sscanf(ptr,...) and the loop termination logic around ptr to ensure the last
alias before ':' is checked.

---

Duplicate comments:
In `@ed.chared.c`:
- Around line 3882-3892: The loop over history entries skips entries whose
hp->histline hasn't been materialized; before reading hp->histline in the loop
that compares Strncmp(InputBuf, hl, inputlen) (inside the block iterating hp =
Histlist.Hnext), call sprlex(&hp->Hlex) to ensure the history text is built,
then re-read hp->histline into hl and proceed only if hl is non-NULL; keep the
rest of the copy into GhostBuf (respecting INBUFSIZE) unchanged and ensure you
still continue when histline remains NULL after materialization.

In `@ed.refresh.c`:
- Around line 345-363: The current logic in ed.refresh.c that erases the
previous ghost overlay whenever GhostBuf is empty or Cursor != LastChar blindly
writes spaces at the current cursor, which can overwrite real input after
Refresh() has redrawn the line; instead, stop erasing using the current Cursor
position and either (a) track the previous ghost start column (store/maintain
prev_ghost_start along with prev_ghost_cols) and move the cursor back to that
recorded start before clearing the prev_ghost_cols spaces, or (b) remove/clear
the ghost via the virtual-display pipeline used by Refresh()/DrawGhost() so
clearing is applied against the virtual buffer rather than the current terminal
cursor; update the code paths that touch prev_ghost_cols (the conditional block
guarded by GhostBuf[0], Cursor, LastChar and the subsequent erase block) to use
the tracked previous position or virtual-display clear routine instead of
erasing at Cursor.

In `@vms.termcap.c`:
- Around line 98-107: The continuation logic uses sizeof(bp) (pointer size) to
compute remaining capacity, causing underflow and an oversized fgets length;
change the calculation to use the actual caller buffer length (e.g., the fixed
1024 byte termcap buffer or a passed-in buffer-size parameter) when computing
remaining instead of sizeof(bp), and then call fgets with that correctly
computed remaining+1; update the variables in the loop (bp, bplen, cp,
remaining) to reference the real buffer size constant or parameter so fgets
cannot be passed an invalid huge value.
- Around line 279-282: The switch contains an invalid character literal `case
'\' :` which is an incomplete escape sequence; replace that label with the
properly escaped backslash literal `case '\\' :` so the switch (the cases
including `case '^' :` and the assignment `**area = *cp;`) compiles correctly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 0969feb4-4531-4a35-9c1e-7474c0766550

📥 Commits

Reviewing files that changed from the base of the PR and between 017d654 and 47612ef.

📒 Files selected for processing (17)
  • ISSUES.md
  • PLAN.md
  • README.md
  • acaux/install-sh
  • alacritty.toml
  • configure.ac
  • dch-template.in
  • dot.mcshrc
  • ed.chared.c
  • ed.defns.c
  • ed.inputl.c
  • ed.refresh.c
  • m4/lib-prefix.m4
  • m4/po.m4
  • sh.func.c
  • tc.prompt.c
  • vms.termcap.c

Comment thread dot.mcshrc Outdated
Comment thread dot.mcshrc Outdated
Comment thread ed.chared.c Outdated
Comment thread ed.refresh.c
Comment thread ISSUES.md Outdated
Comment thread tc.prompt.c Outdated
Comment thread tc.prompt.c Outdated
Comment thread tc.prompt.c Outdated
Comment thread tc.prompt.c Outdated
Comment thread vms.termcap.c Outdated
The threshold in set time is CPU time (user+sys seconds), not wall clock.
5 CPU seconds almost never fires. Changed to 1 so any command burning
>= 1 CPU second reports timing.

Also note: the shell only times forked commands (external programs), not
builtins. set time has no effect on cd, echo, etc.

💘 Generated with Crush

Assisted-by: Claude Sonnet 4.6 via Crush <crush@charm.land>
💘 Generated with Crush

Assisted-by: Claude Sonnet 4.6 via Crush <crush@charm.land>
The prusage format loop only did raw xputchar, so \n printed literally.
Added backslash-escape handling: \n=newline, \t=tab, \e=ESC, \=backslash.

dot.mcshrc: format now uses \n for newline and \e[...m ANSI color codes —
real (cyan), user (green), sys (yellow), cpu (magenta).

💘 Generated with Crush

Assisted-by: Claude Sonnet 4.6 via Crush <crush@charm.land>
💘 Generated with Crush

Assisted-by: Claude Sonnet 4.6 via Crush <crush@charm.land>
💘 Generated with Crush

Assisted-by: Claude Sonnet 4.6 via Crush <crush@charm.land>
💘 Generated with Crush

Assisted-by: Claude Sonnet 4.6 via Crush <crush@charm.land>
- dot.mcshrc: gate Wayland/GPU vars behind WAYLAND_DISPLAY+/dev/dri check
- dot.mcshrc: fix Home/End keybindings (\e[1~, \e[4~, \eOH, \eOF, \e[H, \e[F)
- dot.mcshrc: source ~/.mcshrc.local for machine-local overrides
- ed.chared.c: reject ghost wholesale if it won't fit in input buffer
- ed.chared.c: materialize histline via sprlex() before prefix comparison
- ed.refresh.c: tgetstr uses real capbuf; track prev_ghost_start_h for correct erase
- tc.prompt.c: gitdir NUL termination; worktree .git file parsing; bare repo snprintf fix
- tc.prompt.c: mtime-based cache invalidation (stat HEAD, invalidate on change)
- tc.prompt.c: replace raw Strbuf_append1 loops with tprintf_append_mbs for multibyte
- vms.termcap.c: bounded sscanf %1023[^|:]; fix last-alias loop; fix sizeof(bp) -> 1024
- vms.termcap.c: fix case '\' -> case '\\' (invalid escape literal)
- ISSUES.md: DrawGhost entry corrected to partial mitigation (Display/Vdisplay not used)
- PLAN.md: vms.termcap.c retained as POSIX/Android termcap shim (not removed); consistent
- README.md: remove ^F from autocomplete accept key (not bound by default)

💘 Generated with Crush

Assisted-by: Claude Sonnet 4.6 via Crush <crush@charm.land>
@orpheus497
Copy link
Copy Markdown
Owner Author

CodeRabbit Review Round 2 — All findings addressed

dot.mcshrc

  • Wayland/GPU vars gated behind if (-e /dev/dri/card0 && $?WAYLAND_DISPLAY) — safe on X11, macOS, SSH
  • Home/End bindings fixed: \e[1~/\e[4~, \eOH/\eOF, \e[H/\e[F (was PageUp/PageDown sequences)
  • ~/.mcshrc.local sourced if present for machine-local overrides

ed.chared.c

  • Full-fit check in e_predict_accept: ghost is rejected wholesale if Strlcat(GhostBuf) won't fit in InputLim - LastChar; no partial copies
  • sprlex() materialization: predict_from_history calls sprlex(&hp->Hlex) before reading hp->histline so unmaterialized history entries are not skipped silently

ed.refresh.c

  • tgetstr("so", NULL)tgetstr("so", &area) with real char capbuf[64] buffer — safe on vms.termcap.c and other implementations that dereference the area pointer
  • prev_ghost_start_h tracked at draw time so erase moves cursor back to the correct column before overwriting

tc.prompt.c

  • NUL termination of gitdir after memcpy (copy n bytes, write gitdir[n] = '\0')
  • Worktree .git file parsing: when .git is a regular file, gitdir: <path> is parsed and resolved to an absolute path
  • Bare repo snprintf uses a temporary buffer to avoid undefined overlapping source/destination
  • mtime-based cache invalidation: git_head_mtime static tracks HEAD mtime; cache is invalidated when it changes (not just when the gcwd pointer changes)
  • Multibyte append: raw Strbuf_append1 loops replaced with tprintf_append_mbs for git_branch and git_op

vms.termcap.c

  • sizeof(bp)1024: bp is a pointer parameter so sizeof(bp) was pointer-sized; now uses the correct buffer constant
  • Bounded sscanf: both sscanf calls changed from %[^|:] to %1023[^|:]; alias loop rewritten so the last alias before : is always checked (was skipped when strchr(ptr,'|') == NULL)
  • case '\' :case '\' :: invalid incomplete escape literal replaced with properly escaped backslash

Docs

  • ISSUES.md: DrawGhost entry updated to "partial mitigation" — explicitly states that Display/Vdisplay are not used and full fix is deferred
  • PLAN.md: Phase 2b now states "vms.termcap.c retained and repurposed as POSIX/Android termcap shim" consistently in both the policy section and changelog
  • README.md: ^F removed from predictive autocomplete accept key (it is not bound by default; Right-Arrow is the only accept key)

Deployment note

New binary built cleanly (make — zero errors). Deployed via sudo cp mcsh /usr/local/bin/mcsh. All existing features verified: %g/%G prompt, predictive autocomplete (Right-Arrow accept, ghost display/erase), fish-style history, set color, set time with ANSI colours, Home/End keys.

The prev_ghost_start_h approach was wrong: it stored vcursor_h (the
virtual-draw offset) not the physical terminal cursor column, so the
backspace count to "return to ghost start" was far too large, sending
the cursor to column 0.

New logic:
- When GhostBuf is cleared and Cursor==LastChar (accept path): Refresh
  already painted the new real content over the ghost area via
  update_line; no erase needed, just reset prev_ghost_cols.
- When Cursor!=LastChar (move-away path): emit spaces then backspaces
  from the current cursor position to overwrite trailing ghost chars;
  Refresh will repaint correct content on the next redraw.
- Normal redraw with ghost still active: erase from current position
  then redraw ghost (unchanged).

Drops prev_ghost_start_h entirely.

💘 Generated with Crush

Assisted-by: Claude Sonnet 4.6 via Crush <crush@charm.land>
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

♻️ Duplicate comments (8)
alacritty.toml (1)

2-5: ⚠️ Potential issue | 🟡 Minor

Align the checked-in config with the documented portable defaults.

The pywal cache import is still active, and the shell is still pinned to /usr/local/bin/mcsh; both contradict the documented “local override + PATH” setup and can break non-default installs.

🛠️ Suggested fix
 [general]
-import = ["~/.cache/wal/colors-alacritty.toml"]
+# Optional local overrides, e.g. pywal colors, can be imported from:
+# import = ["~/.config/alacritty/local.toml"]
 
 [terminal.shell]
-program = "/usr/local/bin/mcsh"
+program = "mcsh"

Verify the config and docs stay aligned:

#!/bin/bash
rg -n -C2 'colors-alacritty|program = "/usr/local/bin/mcsh"|program = "mcsh"|pywal import|resolved via `PATH`|via PATH' alacritty.toml README.md ISSUES.md
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@alacritty.toml` around lines 2 - 5, The checked-in config currently imports
pywal cache via the line import = ["~/.cache/wal/colors-alacritty.toml"] and
pins the shell program to an absolute path program = "/usr/local/bin/mcsh",
which contradicts the documented “local override + PATH” setup; remove or
disable the pywal import line and change the [terminal.shell] program entry to
use the unqualified executable name (e.g., program = "mcsh") or remove the
program key so the shell is resolved via PATH, thereby keeping the config
portable and matching the docs.
PLAN.md (1)

77-78: ⚠️ Potential issue | 🟡 Minor

Clarify that VMS platform support is dropped, not every VMS-named file.

This now says vms.termcap.c is retained, but the policy section still says “VMS is dropped entirely.” Tighten the policy wording so readers do not interpret the retained shim as a contradiction.

📝 Suggested wording
-- VMS is **dropped** entirely.
+- VMS platform support is **dropped**; the legacy-named `vms.termcap.c`
+  file is retained only as a non-VMS POSIX/Android termcap shim.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@PLAN.md` around lines 77 - 78, Update the policy wording to clarify that
platform support for VMS is removed while some VMS-legacy files (e.g., the
retained vms.termcap.c shim) are kept for portability; specifically, change the
phrase "VMS is dropped entirely" to something like "VMS platform support is
discontinued; certain VMS‑named legacy source files (for example vms.termcap.c)
are retained and repurposed as portable shims" so readers understand the
difference between dropping VMS support and keeping legacy files.
tc.prompt.c (2)

775-795: ⚠️ Potential issue | 🟠 Major

Invalidate %g/%G using the resolved gitdir state, not $cwd/.git/HEAD.

The mtime probe only checks short2str(gcwd)/.git/HEAD, so prompts in repo subdirectories, worktrees/submodules where .git is a file, and bare repos never see HEAD changes. Operation-state changes like MERGE_HEAD also leave %G stale because HEAD mtime may not change.

🐛 Correctness-first fix
-			int need_refresh = (git_oldcwd != gcwd || git_valid < 0);
-			if (!need_refresh) {
-			    char _hp[MAXPATHLEN];
-			    struct stat _st;
-			    snprintf(_hp, sizeof(_hp), "%s/.git/HEAD",
-				short2str(gcwd));
-			    if (stat(_hp, &_st) == 0 &&
-				_st.st_mtime != git_head_mtime)
-				need_refresh = 1;
-			}
-			if (need_refresh) {
-			    char _hp[MAXPATHLEN];
-			    struct stat _st;
-			    git_oldcwd = gcwd;
-			    git_valid = git_get_info(short2str(gcwd),
-				git_branch, sizeof(git_branch),
-				git_op, sizeof(git_op));
-			    snprintf(_hp, sizeof(_hp), "%s/.git/HEAD",
-				short2str(gcwd));
-			    git_head_mtime = (stat(_hp, &_st) == 0)
-				? _st.st_mtime : 0;
-			}
+			git_oldcwd = gcwd;
+			git_valid = git_get_info(short2str(gcwd),
+			    git_branch, sizeof(git_branch),
+			    git_op, sizeof(git_op));

If the cache stays, have git_get_info() return or fill the resolved gitdir plus HEAD/op marker mtimes, then invalidate against those paths.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tc.prompt.c` around lines 775 - 795, The current mtime invalidation uses
short2str(gcwd)/.git/HEAD which misses worktrees/submodules/bare repos and
op-state files; modify git_get_info (and its callers: the block using git_valid,
git_oldcwd, git_branch, git_op) to return or populate the resolved gitdir path
plus HEAD and operation-marker mtimes (e.g., HEAD mtime and
MERGE_HEAD/REBASE_HEAD mtimes), then replace the direct stat() of
short2str(gcwd)/.git/HEAD with comparisons against those resolved mtimes (update
git_head_mtime and any op-state mtime variables from git_get_info) so
need_refresh is computed from the resolved gitdir state rather than
cwd/.git/HEAD.

270-271: ⚠️ Potential issue | 🔴 Critical

Remove the self-copying snprintf().

Line 271 still passes gitdir as both source and destination, which is undefined behavior and has no useful effect here. The bare-repo path is already in gitdir.

🐛 Proposed fix
-			    /* Rewrite path to be used below without /.git prefix */
-			    snprintf(gitdir, sizeof(gitdir), "%s", gitdir);
-			    /* Adjust: bare repo uses gitdir itself as git dir */
+			    /* Bare repo uses gitdir itself as git dir. */

Verify the remaining self-copy pattern is gone:

#!/bin/bash
rg -n -C2 'snprintf\(gitdir,\s*sizeof\(gitdir\),\s*"%s",\s*gitdir\)' tc.prompt.c
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tc.prompt.c` around lines 270 - 271, The snprintf call that writes gitdir
into itself is undefined and unnecessary; remove the line containing
snprintf(gitdir, sizeof(gitdir), "%s", gitdir) (referencing the gitdir buffer
and its self-copy via snprintf) and ensure the surrounding code uses the
existing gitdir value unchanged; also search for other occurrences of the same
self-copy pattern to delete or replace them with a proper copy from a different
source if needed.
vms.termcap.c (2)

118-140: ⚠️ Potential issue | 🟠 Major

Stop alias parsing at the first capability delimiter and check sscanf().

If a malformed entry starts with a delimiter, tmp can be compared uninitialized. The loop also keeps searching for | after the first :, so a | inside a capability value can be treated as an alias.

🐛 Proposed fix
-		sscanf(bp,"%1023[^|:]",tmp);
-		if (strcmp(name, tmp) == 0) {
+		if (sscanf(bp, "%1023[^|:]", tmp) == 1 &&
+		    strcmp(name, tmp) == 0) {
@@
-		ptr = bp;
-		while ((ptr = strchr(ptr,'|')) != NULL) {
+		ptr = bp;
+		while ((ptr = strchr(ptr, '|')) != NULL &&
+		    strchr(bp, ':') != NULL && ptr < strchr(bp, ':')) {
 			ptr++;
-			sscanf(ptr,"%1023[^|:]",tmp);
-			if (strcmp(name, tmp) == 0) {
+			if (sscanf(ptr, "%1023[^|:]", tmp) == 1 &&
+			    strcmp(name, tmp) == 0) {
 				fclose(fp);
@@
-			if (strchr(ptr,'|') == NULL) break;
 		}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@vms.termcap.c` around lines 118 - 140, The alias-parsing loop can read
uninitialized tmp when an entry begins with a delimiter and incorrectly treats
'|' inside capability values as aliases; modify the parsing in the block that
uses bp/ptr/tmp so that you first locate the first capability delimiter (':' or
end-of-entry) and limit alias scanning to the substring before that delimiter,
use a bounded sscanf or safer function return check (check sscanf returns 1)
before calling strcmp, and stop advancing ptr past the first ':' (i.e., stop on
':' or end-of-line) so strchr(ptr,'|') is only searched within the alias
portion; update the logic around sscanf(bp,"%1023[^|:]",tmp), the ptr loop that
uses strchr(ptr,'|'), and the strcmp checks to enforce these guards.

333-342: ⚠️ Potential issue | 🟠 Major

Avoid out-of-bounds pointer arithmetic in the tgoto() length check.

rp + tlen can be formed past the end of ret before the comparison, which is undefined behavior. Compare lengths using integer offsets instead.

🐛 Proposed fix
-				if (tlen > 0 && rp + tlen <= rend) {
+				if (tlen > 0 &&
+				    (size_t)tlen <= (size_t)(rend - rp)) {
 					memcpy(rp, tmp, (size_t)tlen);
 					rp += tlen;
 				}

Verify the unsafe pointer-add check is removed:

#!/bin/bash
rg -n -C2 'rp \+ tlen <= rend|rend - rp|snprintf\(tmp' vms.termcap.c
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@vms.termcap.c` around lines 333 - 342, The length check uses pointer
arithmetic that can form an out-of-bounds pointer (rp + tlen <= rend); instead,
after calling snprintf into tmp and storing the return in tlen, validate tlen is
non-negative and compare sizes using integer offsets: ensure (size_t)tlen <=
(size_t)(rend - rp) (and also ensure tlen < sizeof(tmp) or handle truncation
from snprintf). Update the check in the case 'd' block (variables: rp, rend,
tlen, tmp, numval, snprintf) to use the integer difference rend - rp and only
memcpy when the integer-size check passes.
ed.refresh.c (1)

380-397: ⚠️ Potential issue | 🟠 Major

Use termcap output and display widths for ghost rendering.

This only uses so as a boolean, then emits hardcoded ANSI ESC[2m/ESC[0m and counts every Char as one column. That still breaks non-ANSI terminals and wide-character suggestions.

Prefer emitting the actual so/se capabilities and using NLSWidth()/putwraw() so cursor restoration matches rendered columns.

Verify all ghost rendering still bypasses termcap strings and width accounting:

#!/bin/bash
sed -n '333,399p' ed.refresh.c
rg -n -C2 'DrawGhost|tgetstr\("so"|ESC|NLSWidth|putwraw|putpure' ed.refresh.c
dot.mcshrc (1)

142-142: ⚠️ Potential issue | 🟠 Major

Rename the objdump alias to dis to preserve normal objdump behavior.

With alias objdump 'objdump -M intel -d \!*', commands like objdump -h libfoo.so, objdump -t a.out, or objdump -x /bin/ls all become objdump -M intel -d -h …, forcing disassembly and mixing it with the requested output, breaking user intent. The -M intel flag is supported on FreeBSD 15's llvm-objdump (available since FreeBSD 13.3), so the fix is straightforward:

-alias objdump 'objdump -M intel -d \!*'
+alias dis 'objdump -M intel -d \!*'

This preserves the useful -M intel -d disassembly functionality under a dedicated alias while keeping plain objdump available with its standard behavior.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@dot.mcshrc` at line 142, The alias `objdump 'objdump -M intel -d \!*'`
overrides the real objdump and forces disassembly for all invocations; change it
to create a new alias name (e.g., `dis`) that runs `objdump -M intel -d` so
users can still call `objdump` normally; update the alias definition in the file
(replace the `alias objdump ...` entry with an alias named `dis` or similar) so
the symbol `objdump` remains the original tool and `dis` provides the
Intel-syntax disassembly shortcut.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@dot.mcshrc`:
- Line 215: The timing threshold is currently set to 0 in the set time = (0
"\n\e[1;36mreal\e[0m %E  \e[1;32muser\e[0m %U  \e[1;33msys\e[0m %S 
\e[1;35mcpu\e[0m %P") setting which causes every command to print a timing
banner; change the threshold from 0 to a higher value (e.g. 2 or 5) to avoid
noisy interactive output, or add a short comment documenting that 0 is
intentional for high-precision build timing so reviewers/users understand the
choice.
- Line 103: The completion definition for make (the line using complete make and
the backticked make -pn invocation) runs make -pn on every tab press; change it
so the command only runs when a Makefile exists (e.g., test for files like
Makefile, makefile, GNUmakefile) or at minimum redirect stderr to suppress "no
target" noise, and update the backticked expression inside the complete make
call accordingly so the guard or redirection is applied to the make -pn
invocation used for completions.
- Around line 29-30: The local override file (~/.mcshrc.local) is being sourced
too early via the if ( -r ~/.mcshrc.local ) source ~/.mcshrc.local statement so
later sections (Environment, Engine, Keybindings, Aliases, Prompt, System and
the set time block) overwrite user customizations; move that source invocation
to the very end of the file (after the set time block) so user overrides take
precedence, and only keep a top-of-file source if you intentionally need
pre-GPU/Wayland hooks (in which case document and/or source it twice as pre/post
hooks).
- Around line 207-212: Replace the incorrect use of VIMINFO with VIMINIT/EXINIT
in the root-only block: instead of setenv VIMINFO ':set viminfo=', set the
environment variable VIMINIT (or EXINIT) to the literal command set viminfo= (no
extra quotes around the empty value) so Vim executes "set viminfo=" on startup
and thus disables writing a .viminfo for root; update the block containing the
$uid == 0 check where unset savehist and setenv LESSHISTFILE are set to use
setenv VIMINIT 'set viminfo=' (or setenv EXINIT 'set viminfo=') accordingly.

In `@ed.refresh.c`:
- Around line 347-361: The code erases the old ghost starting at the current
Cursor, which can overwrite real input when the cursor moved; modify the logic
around GhostBuf handling (the block using prev_ghost_cols, putpure and
putpure('\b')) to move the terminal cursor to the previous ghost start stored in
prev_ghost_h and prev_ghost_v, write spaces for prev_ghost_cols there, then
restore the cursor to the current position (instead of writing spaces at
Cursor); also ensure you update prev_ghost_h and prev_ghost_v before drawing the
new ghost so future erases use the correct coordinates.

In `@ISSUES.md`:
- Around line 75-77: The ISSUES.md entry for alacritty.toml is out of sync with
the actual config: update the bullet for `alacritty.toml` to reflect whether the
file currently imports `~/.cache/wal/colors-alacritty.toml`, whether the pywal
import is commented out, and whether `program` uses `"/usr/local/bin/mcsh"`,
`"/home/.../mcsh"` or just `"mcsh"` (PATH-resolved). Edit the text to accurately
describe the present state (e.g., "imports ~/.cache/wal/colors-alacritty.toml
present" or "pywal import commented out; program set to 'mcsh' resolved via
PATH") and remove contradictory claims so `alacritty.toml`, `pywal` and
`program` descriptions match the repository config.

In `@sh.time.c`:
- Around line 385-397: The for loop and its nested branches around the escape
handling are missing braces and create an ambiguous if/else-if shape; locate the
loop iterating over cp (the for (; *cp; cp++) block) and add explicit braces
around the loop body and around the inner if (*cp != '%') branch and its inner
else so the switch/escape handling and the trailing else if (cp[1]) are clearly
scoped; ensure braces wrap the block containing the backslash handling (the
switch on *++cp), the xputchar(*cp) path, and the subsequent else-if so Clang's
static analysis no longer flags the nested branches.

---

Duplicate comments:
In `@alacritty.toml`:
- Around line 2-5: The checked-in config currently imports pywal cache via the
line import = ["~/.cache/wal/colors-alacritty.toml"] and pins the shell program
to an absolute path program = "/usr/local/bin/mcsh", which contradicts the
documented “local override + PATH” setup; remove or disable the pywal import
line and change the [terminal.shell] program entry to use the unqualified
executable name (e.g., program = "mcsh") or remove the program key so the shell
is resolved via PATH, thereby keeping the config portable and matching the docs.

In `@dot.mcshrc`:
- Line 142: The alias `objdump 'objdump -M intel -d \!*'` overrides the real
objdump and forces disassembly for all invocations; change it to create a new
alias name (e.g., `dis`) that runs `objdump -M intel -d` so users can still call
`objdump` normally; update the alias definition in the file (replace the `alias
objdump ...` entry with an alias named `dis` or similar) so the symbol `objdump`
remains the original tool and `dis` provides the Intel-syntax disassembly
shortcut.

In `@PLAN.md`:
- Around line 77-78: Update the policy wording to clarify that platform support
for VMS is removed while some VMS-legacy files (e.g., the retained vms.termcap.c
shim) are kept for portability; specifically, change the phrase "VMS is dropped
entirely" to something like "VMS platform support is discontinued; certain
VMS‑named legacy source files (for example vms.termcap.c) are retained and
repurposed as portable shims" so readers understand the difference between
dropping VMS support and keeping legacy files.

In `@tc.prompt.c`:
- Around line 775-795: The current mtime invalidation uses
short2str(gcwd)/.git/HEAD which misses worktrees/submodules/bare repos and
op-state files; modify git_get_info (and its callers: the block using git_valid,
git_oldcwd, git_branch, git_op) to return or populate the resolved gitdir path
plus HEAD and operation-marker mtimes (e.g., HEAD mtime and
MERGE_HEAD/REBASE_HEAD mtimes), then replace the direct stat() of
short2str(gcwd)/.git/HEAD with comparisons against those resolved mtimes (update
git_head_mtime and any op-state mtime variables from git_get_info) so
need_refresh is computed from the resolved gitdir state rather than
cwd/.git/HEAD.
- Around line 270-271: The snprintf call that writes gitdir into itself is
undefined and unnecessary; remove the line containing snprintf(gitdir,
sizeof(gitdir), "%s", gitdir) (referencing the gitdir buffer and its self-copy
via snprintf) and ensure the surrounding code uses the existing gitdir value
unchanged; also search for other occurrences of the same self-copy pattern to
delete or replace them with a proper copy from a different source if needed.

In `@vms.termcap.c`:
- Around line 118-140: The alias-parsing loop can read uninitialized tmp when an
entry begins with a delimiter and incorrectly treats '|' inside capability
values as aliases; modify the parsing in the block that uses bp/ptr/tmp so that
you first locate the first capability delimiter (':' or end-of-entry) and limit
alias scanning to the substring before that delimiter, use a bounded sscanf or
safer function return check (check sscanf returns 1) before calling strcmp, and
stop advancing ptr past the first ':' (i.e., stop on ':' or end-of-line) so
strchr(ptr,'|') is only searched within the alias portion; update the logic
around sscanf(bp,"%1023[^|:]",tmp), the ptr loop that uses strchr(ptr,'|'), and
the strcmp checks to enforce these guards.
- Around line 333-342: The length check uses pointer arithmetic that can form an
out-of-bounds pointer (rp + tlen <= rend); instead, after calling snprintf into
tmp and storing the return in tlen, validate tlen is non-negative and compare
sizes using integer offsets: ensure (size_t)tlen <= (size_t)(rend - rp) (and
also ensure tlen < sizeof(tmp) or handle truncation from snprintf). Update the
check in the case 'd' block (variables: rp, rend, tlen, tmp, numval, snprintf)
to use the integer difference rend - rp and only memcpy when the integer-size
check passes.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 9aef1552-89e5-436b-a7f9-017514c49457

📥 Commits

Reviewing files that changed from the base of the PR and between 47612ef and 84f3811.

📒 Files selected for processing (10)
  • ISSUES.md
  • PLAN.md
  • README.md
  • alacritty.toml
  • dot.mcshrc
  • ed.chared.c
  • ed.refresh.c
  • sh.time.c
  • tc.prompt.c
  • vms.termcap.c

Comment thread dot.mcshrc Outdated
Comment thread dot.mcshrc Outdated
Comment thread dot.mcshrc
Comment thread dot.mcshrc Outdated
Comment thread ed.refresh.c Outdated
Comment thread ISSUES.md Outdated
Comment thread sh.time.c Outdated
dot.mcshrc:
- move ~/.mcshrc.local sourcing to end-of-file so user overrides take precedence
- guard make completion behind Makefile existence check; redirect stderr
- rename alias objdump -> dis to preserve original objdump for normal use
- fix VIMINFO -> VIMINIT (correct env var to suppress .viminfo for root)
- add comment documenting set time = (0 ...) threshold is intentional

ed.refresh.c:
- DrawGhost: replace raw backspace-based cursor return with MoveToLine/MoveToChar
  using tracked prev_ghost_h/prev_ghost_v (physical CursorH/CursorV coordinates)
  so ghost erase always targets the correct terminal position

sh.time.c:
- add explicit braces around for-loop body and inner if/else branches to fix
  ambiguous if/else shape flagged by static analysis

tc.prompt.c:
- remove no-op snprintf(gitdir, sizeof(gitdir), "%s", gitdir) self-copy

vms.termcap.c:
- check sscanf return == 1 before strcmp in alias loop
- fix rp+tlen pointer arithmetic in case 'd': use integer (rend-rp) comparison

alacritty.toml:
- comment out pywal import line; change program to "mcsh" (PATH-resolved)

ISSUES.md: update alacritty.toml entry to reflect actual current state
PLAN.md: clarify VMS section heading — platform discontinued, shim files retained

💘 Generated with Crush

Assisted-by: Claude Sonnet 4.6 via Crush <crush@charm.land>
💘 Generated with Crush

Assisted-by: Claude Sonnet 4.6 via Crush <crush@charm.land>
@orpheus497
Copy link
Copy Markdown
Owner Author

CodeRabbit Review Round 3 — All findings addressed

dot.mcshrc

  • ~/.mcshrc.local sourced last — moved to end-of-file so all later sections (env, keybindings, aliases, prompt, set time) are set before user overrides run
  • make completion guarded — now checks for Makefile/makefile/GNUmakefile existence before running make -pn; stderr redirected to suppress "no target" noise
  • objdump alias renamed to dis — original objdump is no longer shadowed; dis provides the Intel-syntax disassembly shortcut
  • VIMINFOVIMINIT — root block now sets setenv VIMINIT 'set viminfo=' (correct env var that Vim actually reads on startup)
  • set time = (0 ...) comment added — threshold 0 is intentional (times every command); documented inline

ed.refresh.c

  • DrawGhost uses MoveToLine/MoveToChar for eraseprev_ghost_h/prev_ghost_v now track the physical terminal cursor (CursorH/CursorV) at draw time; erase navigates to that saved position rather than emitting raw backspaces from an unknown column

sh.time.c

  • Explicit braces added around the for-loop body and the inner if/else if branches — removes the ambiguous dangling-else shape flagged by static analysis

tc.prompt.c

  • No-op snprintf self-copy removedsnprintf(gitdir, sizeof(gitdir), "%s", gitdir) (bare-repo path, line ~271) deleted; surrounding code uses gitdir unchanged

vms.termcap.c

  • sscanf return checked before strcmp — both alias-loop sscanf calls now guard strcmp behind == 1
  • rp + tlen <= rend pointer arithmetic fixedcase 'd' now uses (size_t)tlen <= (size_t)(rend - rp) (integer difference) and also validates tlen < sizeof(tmp) to handle truncation

alacritty.toml

  • pywal import line commented out — config is now portable; colour theming delegated to a machine-local override
  • program changed to "mcsh" — resolved via PATH, no absolute path

ISSUES.md

  • alacritty.toml entry updated to reflect actual current state (pywal commented out, program = "mcsh")

PLAN.md

  • Phase 2b heading clarified — "VMS — Complete Removal" → "VMS — Platform Support Discontinued"; note added that VMS-named source files (e.g. vms.termcap.c) are retained as portable shims

Also fixed this session (ghost cursor UI bug reported separately)

  • Right-Arrow autocomplete accept no longer jumps cursor to column 0 — root cause was prev_ghost_start_h storing vcursor_h (virtual draw offset) instead of physical CursorH; replaced with MoveToLine/MoveToChar approach above

Implements Option B: full virtual-display pipeline integration.
No raw ESC bypass — colour flows through the same Display/Vdisplay
model used by every other rendering path.

Architecture end-to-end:
  1. syntax_colorize() (ed.syntax.c) runs after every keystroke in
     ed.inputl.c — single-pass state machine fills SyntaxColor[]
     with SynToken values for each InputBuf character.
  2. Draw() (ed.refresh.c) reads SyntaxColor[cp-InputBuf] and sets
     the vcurrent_color global (SYN_NORMAL for prompt characters).
  3. Vdraw() writes vcurrent_color into VcolorDisplay[v][h] in
     parallel with the character write into Vdisplay[v][h].
  4. Refresh() main loop copies VcolorDisplay -> ColorDisplay after
     the Vdisplay->Display diff, keeping colour arrays in sync.
  5. so_write() (ed.screen.c) reads ColorDisplay[CursorV][CursorH]
     per character and calls SetSGRColor() before putwraw(). Resets
     with SetSGRColor(-1) at end of write.
  6. SetSGRColor() tracks cur_sgr to suppress redundant SGR emissions;
     resets to -1 whenever SetAttributes() clears all attributes.

Token types: keyword, builtin, cmd_ok, cmd_bad, operator, variable,
dquote, squote, backtick, comment, error (unmatched quote).

LRU command cache (CMD_CACHE_SIZE=32) avoids per-keystroke stat(2)
calls on $PATH for command existence checks.

New files:
  ed.syntax.h  — SynToken enum, SynColor/SynPalette, SyntaxColor[],
                 extern declarations
  ed.syntax.c  — tokeniser, LRU cache, cmd_on_path(), syntax_colorize(),
                 syntax_clear()

Modified files:
  ed.h         — VcolorDisplay, ColorDisplay, vcurrent_color externals
  ed.screen.c  — ReBufferDisplay() allocs colour arrays; SetSGRColor()
                 new static fn; so_write() reads ColorDisplay per char;
                 SetAttributes() resets cur_sgr on full attribute clear
  ed.refresh.c — Draw() sets vcurrent_color; Vdraw() writes VcolorDisplay;
                 ClearDisp() zeroes ColorDisplay; Refresh() copies arrays
  ed.inputl.c  — calls syntax_colorize() after every command dispatch
  ed.decls.h   — extern syntax_colorize(), syntax_clear()
  sh.set.c     — update_vars() and dounset hook STRsyntax
  tc.const.c   — STRsyntax[] constant
  Makefile.in  — ed.syntax.${SUF} added to EDOBJS
  dot.mcshrc   — set syntax added after set color

Feature is fully gated on adrof(STRsyntax); no overhead when unset.
Also removes alacritty.toml from tracking (machine-local config).
Docs: PLAN.md Phase 9, ISSUES.md completed section, README feature table.

💘 Generated with Crush

Assisted-by: Claude Sonnet 4.6 via Crush <crush@charm.land>
@orpheus497
Copy link
Copy Markdown
Owner Author

Phase 9 complete — Native Interactive Syntax Highlighting (set syntax)

Just pushed commit 8180a32 to dev2. This closes out the last major feature item for this PR branch.


What landed

Option B: full virtual-display pipeline integration. Colour flows through the same Display/Vdisplay model used by every other rendering path — no raw ESC bypass, no out-of-band terminal writes.

New files

File Purpose
ed.syntax.h SynToken enum (12 token types), SynColor/SynPalette[] defaults, SyntaxColor[INBUFSIZE] parallel array, extern declarations
ed.syntax.c Single-pass tokeniser state machine, LRU command cache (CMD_CACHE_SIZE=32), cmd_on_path() via stat(2) + $PATH walk, syntax_colorize(), syntax_clear()

Modified files

File Change
ed.h VcolorDisplay, ColorDisplay, vcurrent_color EXTERN declarations
ed.screen.c ReBufferDisplay() allocates parallel colour arrays; SetSGRColor() new static fn (tracks cur_sgr, emits ESC[bold;fg m); so_write() reads ColorDisplay[CursorV][CursorH] per char; SetAttributes() resets cur_sgr on full attribute clear
ed.refresh.c Draw() sets vcurrent_color from SyntaxColor; Vdraw() writes VcolorDisplay; ClearDisp() zeroes ColorDisplay; Refresh() copies VcolorDisplayColorDisplay after line diff
ed.inputl.c syntax_colorize() called after every command dispatch
ed.decls.h extern declarations for syntax_colorize() / syntax_clear()
sh.set.c update_vars() + dounset hook STRsyntax → colorize/clear
tc.const.c / tc.const.h STRsyntax[] constant
Makefile.in / Makefile ed.syntax.${SUF} added to EDOBJS
dot.mcshrc set syntax added after set color

Token palette (defaults, all overridable via SynPalette[])

Token Colour
keyword (if while foreach …) bold cyan
builtin (set alias cd …) bold green
command — found on $PATH green
command — not found bold red
operator (| ; && || > …) yellow
variable ($foo ${foo}) magenta
double-quoted string yellow
single-quoted string yellow
backtick substitution cyan
comment (#…) bright black
unmatched quote bold red

Architecture summary

keystroke → syntax_colorize() → SyntaxColor[]
          → Draw(cp)          → vcurrent_color = SyntaxColor[cp-InputBuf]
          → Vdraw(c,w)        → VcolorDisplay[v][h] = vcurrent_color
          → Refresh()         → ColorDisplay[line] ← VcolorDisplay[line]
          → so_write()        → SetSGRColor(ColorDisplay[CursorV][CursorH])
                              → putwraw(c)
                              → SetSGRColor(-1)   ← reset after write

Feature is fully gated on adrof(STRsyntax) — zero overhead when unset.


Also in this commit

  • alacritty.toml removed from tracking (machine-local config, now in .gitignore)
  • PLAN.md — Phase 9 section added; changelog entry
  • ISSUES.md — Phase 9 completed section with full architecture breakdown
  • README.mdset syntax row in features table; dot.mcshrc key settings updated

All completed work on this branch (PR #3 full summary)

Phase 9 — Native syntax highlighting (this commit)

See above.

Phase 8 — Code review fixes (PR3 Gemini + CodeRabbit)

  • configure.ac: TCSH_BASELINE_VERSION uses M4 macro; PACKAGE_PATCHLEVEL strips leading zeros via sed
  • sh.func.c doif(): int itcsh_number_t (wide expression result truncation fix)
  • vms.termcap.c: octal cases '4''7' added; sscanf uses %[^|:] + strcmp; fgets tracks remaining buffer; tgoto buffer enlarged to 64 bytes with snprintf for %d
  • ed.defns.c: predict-accept NLS catalog ID corrected (124, was 122 — collision with newline-and-hold)
  • acaux/install-sh: name-guard case patterns use * suffix for multi-char names
  • m4/lib-prefix.m4: dn;dnl; m4/po.m4: C# DLL cleanup removes correct target; GETTEXT_MACRO_VERSION bumped

Phase 7b — Native mcsh features

  • Fish-style predictive autocomplete: predict_from_history() scans Histlist for prefix match; ghost text rendered dimmed after cursor; Right-Arrow / ^F accepts; cleared on any non-insert command
  • Git branch prompt escapes %g / %G: git_get_info() walks upward for .git/HEAD; cached per-CWD; %G appends operation state (MERGING, REBASING-i, BISECTING, etc.)
  • dot.mcshrc rewrite: full reference startup file with keybindings, completions, aliases, no starship dependency

Phase 5 — Upstream feature PRs

  • PR #89 Interactive comments: # acts as comment in interactive mode
  • PR #107 Expression short-circuit: $?a && "$a" != "" no longer throws when a is unset
  • PR #105 Pipe-to-variable: echo foo | set x and set x < file
  • PR #77 function builtin: named shell function definitions
  • Issue #113 Redirection in { } blocks: if ( { cmd >& /dev/null } ) works

Phase 4 — Bug fixes

  • %j prompt token: counts live job leaders only (upstream #110)
  • getn() overflow: strtoll with proper overflow/errno (upstream #101)
  • Shift operator UB: unsigned arithmetic for <</>>
  • crypt link failure: AC_SEARCH_LIBS([crypt], [crypt xcrypt]) (upstream #99)
  • Ghost text rendering fixes: stale erase, cursor jump on accept, NUL terminator

Phases 1–3, 6 — Branding, hygiene, build system

  • Full Windows/VMS/dead platform purge; system/ pruned to active POSIX platforms
  • sh.types.h collapsed to <stdint.h>/<stddef.h>; bundled malloc removed; K&R guards removed
  • Build system: configure.ac modernised; dead branches removed; autoconf ≥ 2.72 targeted

…Display

Two bugs causing colours on wrong/random characters:

1. so_write() was reading ColorDisplay[CursorV][CursorH] — the
   *previous* frame's colour map — while rendering characters from
   the current Vdisplay. Every character got last keystroke's token,
   shifted by the display diff. Changed to read VcolorDisplay which
   is filled by Vdraw() in the current Refresh() pass.

2. VcolorDisplay was never zeroed before each repaint, so deleted
   characters left stale SynToken values that bled onto the padding
   spaces written by cpy_pad_spaces(). Added a memset loop at the
   top of Refresh() before the prompt/input draw pass.

💘 Generated with Crush

Assisted-by: Claude Sonnet 4.6 via Crush <crush@charm.land>
CC_NORM falls through without calling Refresh() — it relies on
RefPlusOne() for the incremental single-character append path, which
never goes through Draw()/Vdraw() and therefore never picks up the
new SyntaxColor[] values from syntax_colorize().

When set syntax is active, promote CC_NORM to CC_REFRESH after
syntax_colorize() runs so a full Refresh() is always issued and
the new token colours reach the screen immediately on every keypress.

💘 Generated with Crush

Assisted-by: Claude Sonnet 4.6 via Crush <crush@charm.land>
…f bypass)

The core problem: update_line() does a glyph-only diff (*o == *n).
When you type a character that changes a word's token class (e.g.
'i' -> 'if' becoming a keyword), the *existing* letters didn't change
glyph so the diff skips them entirely -- so_write() never fires for
those cells and the new SGR colour never reaches the terminal.

Two fixes together:

1. ed.refresh.c: before update_line() on each line, compare
   ColorDisplay (previous colours) vs VcolorDisplay (new colours).
   If they differ, memset Display[cur_line] to zero to poison the
   diff -- forcing update_line() to see every cell as changed and
   redraw the whole line via so_write() with the new colours.

2. ed.inputl.c: keep CC_NORM -> CC_REFRESH promotion when set syntax
   is active, so RefPlusOne() (which bypasses Draw/Vdraw entirely)
   is replaced with a full Refresh() that goes through the colour
   pipeline. Without this, single-char inserts never reach Refresh()
   at all.

💘 Generated with Crush

Assisted-by: Claude Sonnet 4.6 via Crush <crush@charm.land>
…correctly)

Replace the parallel VcolorDisplay/ColorDisplay arrays and Display[]
poisoning with the correct approach: pack the 4-bit SynToken directly
into bits 0xF0000000 of each Char written to Vdisplay.

ATTRIBUTES uses 0x0F000000; QUOTE (0x80000000) is only ever set by
the lexer and never written into Vdisplay/Display. So bits 0xF0000000
are completely free in the display layer -- 4 bits covers all 12 tokens.

Result: update_line()'s glyph diff (*o == *n) now naturally detects
colour-only changes because the packed token makes the Char values
differ. Deleted characters become '\0' in the new Vdisplay which
differs from the old coloured Char -- backspace correctly erases.
No poisoning, no parallel arrays, no stale state, no off-by-one.

so_write() extracts the token with SYN_TOK(), calls SetSGRColor(),
then strips the token bits with SYN_GLYPH() before putwraw() so
the terminal sees only the actual glyph.

New macros in ed.syntax.h:
  SYN_SHIFT / SYN_MASK   -- bit field definition
  SYN_PACK(c, t)         -- embed token t into display Char c
  SYN_TOK(c)             -- extract token from display Char c
  SYN_GLYPH(c)           -- strip token bits, get raw glyph

Removed: VcolorDisplay, ColorDisplay parallel arrays and all
allocation, zeroing, memcmp/memcpy, and poisoning code.

💘 Generated with Crush

Assisted-by: Claude Sonnet 4.6 via Crush <crush@charm.land>
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 9

♻️ Duplicate comments (7)
tc.prompt.c (1)

414-420: ⚠️ Potential issue | 🟠 Major

Refresh %G when the resolved Git state changes, not only $cwd/.git/HEAD.

Line 777 only checks $cwd/.git/HEAD, which does not exist for worktrees/submodules and does not change when merge/rebase/cherry-pick marker files appear. %G can still stay stale in the same directory.

🐛 Correctness-first fix: recompute git info for prompt rendering
 		/* git info cache */
     static Char *git_oldcwd = NULL;
     static char git_branch[256];
     static char git_op[64];
     static int  git_valid = -1;
-    static time_t git_head_mtime = 0;
@@
 		    {
-			int need_refresh = (git_oldcwd != gcwd || git_valid < 0);
-			if (!need_refresh) {
-			    char _hp[MAXPATHLEN];
-			    struct stat _st;
-			    snprintf(_hp, sizeof(_hp), "%s/.git/HEAD",
-				short2str(gcwd));
-			    if (stat(_hp, &_st) == 0 &&
-				_st.st_mtime != git_head_mtime)
-				need_refresh = 1;
-			}
-			if (need_refresh) {
-			    char _hp[MAXPATHLEN];
-			    struct stat _st;
-			    git_oldcwd = gcwd;
-			    git_valid = git_get_info(short2str(gcwd),
-				git_branch, sizeof(git_branch),
-				git_op, sizeof(git_op));
-			    snprintf(_hp, sizeof(_hp), "%s/.git/HEAD",
-				short2str(gcwd));
-			    git_head_mtime = (stat(_hp, &_st) == 0)
-				? _st.st_mtime : 0;
-			}
+			git_oldcwd = gcwd;
+			git_valid = git_get_info(short2str(gcwd),
+			    git_branch, sizeof(git_branch),
+			    git_op, sizeof(git_op));
 		    }

Also applies to: 773-793

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tc.prompt.c` around lines 414 - 420, The prompt's git cache (git_oldcwd,
git_branch, git_op, git_valid, git_head_mtime) is only refreshed by checking
$cwd/.git/HEAD and thus misses worktrees/submodules and marker-file state
changes; update the refresh logic to detect changes in the resolved git
directory and any state marker files instead of only .git/HEAD: resolve the
actual git dir for the current cwd (follow worktree file if present), compute a
composite state signature (e.g., HEAD target path + mtimes of HEAD and marker
files like MERGE_HEAD, CHERRY_PICK_HEAD, REBASE_HEAD and refs/* that affect
branch name), compare it to the cached signature, and if it differs (or cwd !=
git_oldcwd) recompute git_branch/git_op, set git_valid accordingly and update
git_head_mtime and git_oldcwd so %G is always recomputed when the resolved git
state changes.
ISSUES.md (1)

91-95: ⚠️ Potential issue | 🟡 Minor

Remove ^F from the predictive-accept status note unless it is actually bound.

This still says Right-Arrow / ^F, while the current documented behavior in README.md is Right-Arrow only and the provided keymap change only binds the right arrow to F_PREDICT_ACCEPT.

Suggested fix
-  via backspaces. `e_predict_accept` (Right-Arrow / `^F`) copies `GhostBuf` into
+  via backspaces. `e_predict_accept` (Right-Arrow) copies `GhostBuf` into
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ISSUES.md` around lines 91 - 95, Update the predictive-accept status note to
remove the "^F" token so it matches the actual binding; specifically edit the
descriptive line that currently reads "Right-Arrow / `^F`" (referencing
predict_from_history(), e_predict_accept, and GhostBuf) to mention only
"Right-Arrow" (and/or `F_PREDICT_ACCEPT` if you prefer symbolic name) so the
docs align with README.md and the provided keymap that binds only the right
arrow to F_PREDICT_ACCEPT.
vms.termcap.c (1)

126-138: ⚠️ Potential issue | 🟡 Minor

Limit alias scanning to the name field before the first :.

The loop still searches for | across the full capability string, so a pipe character in a later capability value can be parsed as another alias. Stop at the first colon and only scan aliases inside that name field.

Suggested fix
-		ptr = bp;
-		while ((ptr = strchr(ptr,'|')) != NULL) {
+		{
+		    char *name_end = strchr(bp, ':');
+		    ptr = bp;
+		    while ((ptr = strchr(ptr, '|')) != NULL &&
+			   (name_end == NULL || ptr < name_end)) {
 			ptr++;
 			if (sscanf(ptr,"%1023[^|:]",tmp) == 1 && strcmp(name, tmp) == 0) {
 				fclose(fp);
 `#ifdef` DEBUG
 	fprintf(stderr,CGETS(31, 3, "Found %s in %s.\n"), name, termfile);
 	sleep(1);
 `#endif` /* DEBUG */
 				return(1);
 			}
-			if (strchr(ptr,'|') == NULL) break;
+		    }
 		}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@vms.termcap.c` around lines 126 - 138, The alias-scanning loop (using ptr,
bp, strchr and sscanf(tmp)) can cross the first ':' and treat pipes in
capability values as additional aliases; change the loop to stop at the first
':' by locating the name-field terminator (e.g., char *colon = strchr(bp, ':'))
and only consider '|' positions that occur before that colon, updating ptr
forward but breaking when the next '|' is at or past the colon, and only call
sscanf on the substring within that name field when comparing tmp to name.
sh.set.c (1)

611-612: ⚠️ Potential issue | 🟠 Major

Keep getn() strict; scope the empty-arm fallback to expression evaluation.

This still makes every empty numeric string parse as 0, so callers outside ignored short-circuit arms can silently accept invalid input. The provided cross-file snippets show getn() results flowing into array arithmetic (sh.func.c), completion range checks (tw.comp.c), and history line selection (sh.hist.c).

Proposed direction
-    if (*cp == '\0')		/* empty string from ignored expression arms */
-	return 0;
+    if (*cp == '\0')
+	stderror(ERR_NAME | ERR_BADNUM);

Then apply the 0 fallback only where the expression evaluator can prove the empty value came from an ignored/short-circuited arm.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sh.set.c` around lines 611 - 612, getn() should remain strict and must not
treat an empty string as numeric 0; remove or disable the unconditional fallback
in getn() that returns 0 when *cp == '\0' and instead let callers explicitly
provide a 0 for ignored/short-circuited expression arms. Update the expression
evaluator code paths that perform short-circuit/ignored-arm evaluation (the
caller(s) in the expression evaluator that invoke getn(), not getn() itself) to
detect an empty-arm result and substitute 0 only when the evaluator can prove
the value came from an ignored/short-circuited arm; leave other callers (e.g.,
sh.func.c, tw.comp.c, sh.hist.c) to continue receiving strict getn() errors for
empty numeric input. Ensure getn() returns an error/indicator for empty input so
callers can decide whether to substitute 0.
dot.mcshrc (2)

11-27: ⚠️ Potential issue | 🟠 Major

Do not enable Sway/NVIDIA hybrid settings for every Wayland session.

The guard is better, but /dev/dri/card0 && WAYLAND_DISPLAY also matches non-Sway, AMD-only, Intel-only, remote, and nested Wayland sessions. Keep generic Wayland variables here if desired, but move compositor/GPU-specific exports behind an explicit local opt-in or into ~/.mcshrc.local.

Suggested direction
 if ( -e /dev/dri/card0 && $?WAYLAND_DISPLAY ) then
     setenv CLUTTER_BACKEND wayland
     setenv GDK_BACKEND wayland
     setenv QT_QPA_PLATFORM wayland
     setenv SDL_VIDEODRIVER wayland
-    setenv XDG_CURRENT_DESKTOP sway
-    setenv XDG_SESSION_TYPE wayland
-    setenv XDG_SESSION_DESKTOP sway
-
-    # Intel + Nvidia Hybrid
-    setenv WLR_DRM_DEVICES /dev/dri/card0
-    setenv WLR_NO_HARDWARE_CURSORS 1
-    setenv LIBVA_DRIVER_NAME iHD
-    setenv __NV_PRIME_RENDER_OFFLOAD 1
-    setenv __GLX_VENDOR_LIBRARY_NAME nvidia
-    setenv __VK_LAYER_NV_optimus NVIDIA_only
 endif
+
+# Put compositor/GPU-specific settings, e.g. Sway/NVIDIA offload, in:
+#   ~/.mcshrc.local
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@dot.mcshrc` around lines 11 - 27, The current guard (checking /dev/dri/card0
and $?WAYLAND_DISPLAY) sets both generic Wayland envs and
compositor/GPU-specific NVIDIA/Hybrid variables; split them: keep only generic
Wayland exports (CLUTTER_BACKEND, GDK_BACKEND, QT_QPA_PLATFORM, SDL_VIDEODRIVER,
XDG_CURRENT_DESKTOP, XDG_SESSION_TYPE, XDG_SESSION_DESKTOP) under the existing
Wayland check, and move the hybrid-specific exports (WLR_DRM_DEVICES,
WLR_NO_HARDWARE_CURSORS, LIBVA_DRIVER_NAME, __NV_PRIME_RENDER_OFFLOAD,
__GLX_VENDOR_LIBRARY_NAME, __VK_LAYER_NV_optimus) into an explicit opt-in block
(e.g., sourced from ~/.mcshrc.local or gated by a specific flag like
ENABLE_NV_HYBRID or a compositor-specific check such as XDG_CURRENT_DESKTOP =
"sway" plus NVIDIA device detection) so they are only enabled for intended
Sway/NVIDIA hybrid sessions.

69-217: ⚠️ Potential issue | 🟠 Major

Restore an interactive guard around keybindings, completions, prompts, and stty.

This reference rc now runs interactive-only setup unconditionally. If .mcshrc is sourced by a non-interactive shell, bindkey, complete, stty, prompt mutation, alias resets, and timing settings can change script behavior or fail on non-tty stdin.

Suggested structure
 # ------------------------------------------------------------------------------
 # 4. KEYBINDINGS (TERMINAL I/O MAPPING)
 # ------------------------------------------------------------------------------
+if ( $?prompt ) then
 bindkey -e
 ...
 # Threshold 0 = time every command; raise to 2 for less noisy sessions
 set time = (0 "\n\e[1;36mreal\e[0m %E  \e[1;32muser\e[0m %U  \e[1;33msys\e[0m %S  \e[1;35mcpu\e[0m %P")
+endif
 
 # Source machine-local overrides last so they take full precedence
 if ( -r ~/.mcshrc.local ) source ~/.mcshrc.local
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@dot.mcshrc` around lines 69 - 217, The file runs interactive-only commands
unguarded; wrap all interactive setup (blocks containing bindkey, complete
invocations, unalias/alias lines, the PROMPT ENGINE where set prompt/set
prompt2/set rprompt occur, the stty status / bindkey ^G switch, set time, and
the HOST COMPLETION loop using foreach) in an interactive-check such as:
surround those sections with if ( $?prompt ) then ... endif (or an equivalent
interactive test) so non-interactive shells skip bindkey/complete/stty/prompt
changes and avoid failures.
ed.refresh.c (1)

348-408: ⚠️ Potential issue | 🟠 Major

Ghost rendering still bypasses Vdisplay/Display and emits hardcoded ANSI.

Same root-cause as the prior review: DrawGhost() writes directly with putpure() plus raw ESC[2m / ESC[0m, so the refresh model has no record of the overlay. Consequences that still apply:

  • The tgetstr("so", &caparea) check probes for standout, but the code then emits \033[2m (SGR dim/faint) — a different attribute. A terminal that advertises so but not SGR 2 will render incorrectly, and one that supports dim but not so will render unstyled. Either gate on a real dim capability or drop the probe and always emit nothing when TermCanUL/SGR support is unknown.
  • The trailing \033[0m unconditionally resets SGR state regardless of whether the probe succeeded, which can clobber attributes the renderer just set (e.g. in RefPlusOne after PutPlusOne on a literal).
  • putpure((int)c) narrows a Char to a byte; any multibyte/wide ghost glyph (common for history completions with non-ASCII) is silently mangled, and ghost_cols tracks bytes written, not terminal columns — so the backspace restore and prev_ghost_cols erase will be wrong for anything non-ASCII. The objectives note this is a documented limitation, but the packing/tracking path into Vdisplay is what would fix it for real.
Minimal correctness tweak for the termcap/dim mismatch (behavior fix, not the structural one)
-    char capbuf[64];
-    char *caparea = capbuf;
-
     /*
      * On a full repaint Refresh() already redrew the whole line cleanly —
@@
-    gp = GhostBuf;
-    if (tgetstr("so", &caparea) != NULL) {
-	(void) putpure('\033'); (void) putpure('[');
-	(void) putpure('2'); (void) putpure('m');
-    }
+    gp = GhostBuf;
+    /* Emit SGR dim only; do not conflate with termcap "so" (standout). */
+    (void) putpure('\033'); (void) putpure('[');
+    (void) putpure('2'); (void) putpure('m');
     while (*gp) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ed.refresh.c` around lines 348 - 408, DrawGhost currently bypasses the
refresh/display pipeline by writing directly with putpure(), probes termcap "so"
but emits SGR 2, unconditionally emits ESC[0m, and counts bytes not display
columns which mangles wide/multibyte ghosts; fix by routing ghost output through
the existing display layer (call Vdisplay/Display or the same output path used
by PutPlusOne) instead of putpure(), use the correct terminal capability (query
true SGR/dim support or a TermCanSGR flag rather than tgetstr("so",...)) and
only emit/reset SGR when you actually set it, and compute column width using the
project's character-width utility (the Char/width helpers) so prev_ghost_cols
and backspace/erase logic track terminal columns correctly; update references in
DrawGhost, GhostBuf, prev_ghost_cols, and any SGR/termcap checks to use the
shared display and width helpers.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@ed.h`:
- Around line 181-187: The comment above vcurrent_color is stale: it still
describes separate parallel colour arrays for Vdisplay/Display, but the renderer
now packs syntax tokens into the display Char struct and extracts them via
SYN_TOK() / SYN_GLYPH() in ed.screen.c; update the comment in ed.h (near
vcurrent_color and ReBufferDisplay()) to remove references to parallel colour
arrays, state that syntax tokens are stored in Char, mention
SYN_TOK()/SYN_GLYPH() as the accessors used by the rendering path, and keep a
short note about vcurrent_color being the SynToken being written into those Char
entries.

In `@ed.inputl.c`:
- Around line 189-193: The current call to syntax_colorize() runs before
dispatch, so later mutations to InputBuf by tenematch() in completion/expansion
cases (CC_COMPLETE*, CC_LIST_GLOB, CC_EXPAND_GLOB, CC_NORMALIZE_PATH,
CC_EXPAND_VARS, CC_NORMALIZE_COMMAND) leave syntax tokens stale when Refresh()
is called; fix this by moving syntax_colorize() to occur immediately before each
Refresh() that follows a path which can modify InputBuf, or introduce a helper
(e.g., RefreshSyntax) that calls syntax_colorize() if adrof(STRsyntax) and then
calls Refresh(), and replace those Refresh() calls in the listed switch cases
with the helper so syntax is recolored after InputBuf mutations.

In `@ed.refresh.c`:
- Line 45: Remove the unused file-scope static variable vcurrent_color_local:
delete the declaration "static int vcurrent_color_local;" (it shadows nothing
and is never referenced); ensure code uses the existing global vcurrent_color
from ed.h and run a build to confirm no remaining references to
vcurrent_color_local remain.

In `@ed.screen.c`:
- Around line 1006-1031: SetSGRColor currently emits ESC[0m when fg < 0 which
resets all terminal attributes but does not update cur_atr, and StopHighlight
emits T_me without clearing cur_sgr; fix by treating syntax SGR as a separate
layer: change SetSGRColor to reset only the color attributes (e.g. use SGR 39)
instead of ESC[0m or, if emitting a full reset, immediately set cur_atr = 0 so
terminal attribute state stays consistent; likewise ensure StopHighlight clears
cur_sgr (or otherwise re-applies cur_atr) so later calls to SetSGRColor don't
get incorrectly skipped—update references in SetSGRColor, StopHighlight, cur_sgr
and cur_atr handling to keep color-layer and attribute-layer state synchronized.

In `@ed.syntax.c`:
- Around line 365-385: The tokenizer incorrectly ends variable state when the
first character after '$' is a special prefix like '?' or '#', causing `$?path`
to color only `$?` as a variable; update the logic in the ST_VARIABLE handling
so that if the initial character (buf[i] & CHAR) is '?' or '#' you keep state =
ST_VARIABLE and allow the subsequent alphanumeric/underscore characters to
remain part of the variable name instead of switching to ST_NORMAL; modify the
conditional that checks ((buf[i] & CHAR) >= 'a' ... '0' ... '_') to also accept
an initial '?' or '#' (or special-case that prefix) so SyntaxColor[i] stays
SYN_VARIABLE and state is not set to ST_NORMAL prematurely (referencing
ST_VARIABLE, ST_NORMAL, SyntaxColor, SYN_VARIABLE, state, buf and CHAR).
- Around line 202-241: The syntax-command cache in ed.syntax.c must be
invalidated when PATH or cwd changes and must check executability correctly:
update the executable test in the search loop and the direct-path branch to use
S_ISREG(st.st_mode) && access(path_or_word, X_OK) instead of (st.st_mode &
S_IXUSR), and ensure directories are excluded; add a cache_clear() (or
timestamp-based invalidation) routine used by update_vars() (the caller that
invokes dohash() on PATH changes) so cache_lookup/cache_store do not return
stale results when PATH or CWD changes.

In `@ed.syntax.h`:
- Around line 39-60: Include <stdint.h> (for uint8_t) and ensure INBUFSIZE is
defined or the proper header that provides it is included; then guard the
token-packing macros by checking whether Char is wide enough (use existing build
macros like WIDE_STRINGS or a compile-time sizeof(Char) / CHAR_BIT test) before
defining SYN_SHIFT/SYN_MASK/SYN_PACK/SYN_TOK/SYN_GLYPH, and provide a clear
fallback when Char is narrow (SHORT_STRINGS): either disable bit-packing (set
SYN_MASK to 0 and make SYN_PACK a no-op) and use a parallel token array (e.g.,
syn_tokens[] referenced where syntax tokens are needed) or switch to alternate
implementations that do not overflow; update all uses of
SYN_PACK/SYN_TOK/SYN_GLYPH and references to Char accordingly so narrow-Char
builds do not rely on 32-bit packing.

In `@ISSUES.md`:
- Around line 21-35: Update the architecture notes to remove references to
separate VcolorDisplay/ColorDisplay arrays and instead describe the packed Char
implementation: explain that syntax_colorize() emits packed token/char info into
the display buffer as Char values, that Draw/ Vdraw write these packed Char
values (not parallel color arrays), and that ed.screen.c extracts token and
glyph with SYN_TOK()/SYN_GLYPH() when deciding colors in so_write(); also note
ed.h no longer declares VcolorDisplay/ColorDisplay and keep
SetSGRColor()/cur_sgr behavior description as-is. Ensure the same changes are
applied to the other affected paragraph(s) that still mention the old
VcolorDisplay/ColorDisplay model.

In `@Makefile.in`:
- Around line 443-445: EDOBJS currently lists ed.syntax.${SUF} but the header
ed.syntax.h is not added to the dependency graph, so changes to that header
won't trigger rebuilds; update the Makefile rules so the object/target that
produces ed.syntax.${SUF} (e.g., ed.syntax.o / the rule that builds
ed.syntax.${SUF}) depends on ed.syntax.h and ensure any display/input targets
that include ed.syntax.h also list ed.syntax.h as a prerequisite so edits to
ed.syntax.h cause those targets (ed.syntax.${SUF}, ed.screen.${SUF},
ed.inputl.${SUF}, etc.) to rebuild.

---

Duplicate comments:
In `@dot.mcshrc`:
- Around line 11-27: The current guard (checking /dev/dri/card0 and
$?WAYLAND_DISPLAY) sets both generic Wayland envs and compositor/GPU-specific
NVIDIA/Hybrid variables; split them: keep only generic Wayland exports
(CLUTTER_BACKEND, GDK_BACKEND, QT_QPA_PLATFORM, SDL_VIDEODRIVER,
XDG_CURRENT_DESKTOP, XDG_SESSION_TYPE, XDG_SESSION_DESKTOP) under the existing
Wayland check, and move the hybrid-specific exports (WLR_DRM_DEVICES,
WLR_NO_HARDWARE_CURSORS, LIBVA_DRIVER_NAME, __NV_PRIME_RENDER_OFFLOAD,
__GLX_VENDOR_LIBRARY_NAME, __VK_LAYER_NV_optimus) into an explicit opt-in block
(e.g., sourced from ~/.mcshrc.local or gated by a specific flag like
ENABLE_NV_HYBRID or a compositor-specific check such as XDG_CURRENT_DESKTOP =
"sway" plus NVIDIA device detection) so they are only enabled for intended
Sway/NVIDIA hybrid sessions.
- Around line 69-217: The file runs interactive-only commands unguarded; wrap
all interactive setup (blocks containing bindkey, complete invocations,
unalias/alias lines, the PROMPT ENGINE where set prompt/set prompt2/set rprompt
occur, the stty status / bindkey ^G switch, set time, and the HOST COMPLETION
loop using foreach) in an interactive-check such as: surround those sections
with if ( $?prompt ) then ... endif (or an equivalent interactive test) so
non-interactive shells skip bindkey/complete/stty/prompt changes and avoid
failures.

In `@ed.refresh.c`:
- Around line 348-408: DrawGhost currently bypasses the refresh/display pipeline
by writing directly with putpure(), probes termcap "so" but emits SGR 2,
unconditionally emits ESC[0m, and counts bytes not display columns which mangles
wide/multibyte ghosts; fix by routing ghost output through the existing display
layer (call Vdisplay/Display or the same output path used by PutPlusOne) instead
of putpure(), use the correct terminal capability (query true SGR/dim support or
a TermCanSGR flag rather than tgetstr("so",...)) and only emit/reset SGR when
you actually set it, and compute column width using the project's
character-width utility (the Char/width helpers) so prev_ghost_cols and
backspace/erase logic track terminal columns correctly; update references in
DrawGhost, GhostBuf, prev_ghost_cols, and any SGR/termcap checks to use the
shared display and width helpers.

In `@ISSUES.md`:
- Around line 91-95: Update the predictive-accept status note to remove the "^F"
token so it matches the actual binding; specifically edit the descriptive line
that currently reads "Right-Arrow / `^F`" (referencing predict_from_history(),
e_predict_accept, and GhostBuf) to mention only "Right-Arrow" (and/or
`F_PREDICT_ACCEPT` if you prefer symbolic name) so the docs align with README.md
and the provided keymap that binds only the right arrow to F_PREDICT_ACCEPT.

In `@sh.set.c`:
- Around line 611-612: getn() should remain strict and must not treat an empty
string as numeric 0; remove or disable the unconditional fallback in getn() that
returns 0 when *cp == '\0' and instead let callers explicitly provide a 0 for
ignored/short-circuited expression arms. Update the expression evaluator code
paths that perform short-circuit/ignored-arm evaluation (the caller(s) in the
expression evaluator that invoke getn(), not getn() itself) to detect an
empty-arm result and substitute 0 only when the evaluator can prove the value
came from an ignored/short-circuited arm; leave other callers (e.g., sh.func.c,
tw.comp.c, sh.hist.c) to continue receiving strict getn() errors for empty
numeric input. Ensure getn() returns an error/indicator for empty input so
callers can decide whether to substitute 0.

In `@tc.prompt.c`:
- Around line 414-420: The prompt's git cache (git_oldcwd, git_branch, git_op,
git_valid, git_head_mtime) is only refreshed by checking $cwd/.git/HEAD and thus
misses worktrees/submodules and marker-file state changes; update the refresh
logic to detect changes in the resolved git directory and any state marker files
instead of only .git/HEAD: resolve the actual git dir for the current cwd
(follow worktree file if present), compute a composite state signature (e.g.,
HEAD target path + mtimes of HEAD and marker files like MERGE_HEAD,
CHERRY_PICK_HEAD, REBASE_HEAD and refs/* that affect branch name), compare it to
the cached signature, and if it differs (or cwd != git_oldcwd) recompute
git_branch/git_op, set git_valid accordingly and update git_head_mtime and
git_oldcwd so %G is always recomputed when the resolved git state changes.

In `@vms.termcap.c`:
- Around line 126-138: The alias-scanning loop (using ptr, bp, strchr and
sscanf(tmp)) can cross the first ':' and treat pipes in capability values as
additional aliases; change the loop to stop at the first ':' by locating the
name-field terminator (e.g., char *colon = strchr(bp, ':')) and only consider
'|' positions that occur before that colon, updating ptr forward but breaking
when the next '|' is at or past the colon, and only call sscanf on the substring
within that name field when comparing tmp to name.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: c016f88e-864d-47da-92c3-b436fd0a6ad7

📥 Commits

Reviewing files that changed from the base of the PR and between 84f3811 and b8a0547.

📒 Files selected for processing (18)
  • .gitignore
  • ISSUES.md
  • Makefile.in
  • PLAN.md
  • README.md
  • dot.mcshrc
  • ed.decls.h
  • ed.h
  • ed.inputl.c
  • ed.refresh.c
  • ed.screen.c
  • ed.syntax.c
  • ed.syntax.h
  • sh.set.c
  • sh.time.c
  • tc.const.c
  • tc.prompt.c
  • vms.termcap.c

Comment thread ed.h Outdated
Comment thread ed.inputl.c Outdated
Comment thread ed.refresh.c Outdated
Comment thread ed.screen.c
Comment thread ed.syntax.c
Comment thread ed.syntax.c
Comment thread ed.syntax.h Outdated
Comment thread ISSUES.md Outdated
Comment thread Makefile.in
- ed.h: update stale comment; replace parallel-colour-array description with
  accurate packed-Char / SYN_TOK()/SYN_GLYPH() architecture note
- ed.inputl.c: call syntax_colorize() inside Repair() (covers all
  buffer-mutation CC_* cases) and in the CC_REFRESH path so tokens are
  always fresh when Refresh() is called after completion/expansion
- ed.refresh.c: remove unused vcurrent_color_local; fix DrawGhost to only
  emit/reset SGR dim when actually set (eliminate unconditional ESC[0m)
- ed.screen.c: SetSGRColor() sets cur_atr=0 on full ESC[0m reset;
  StopHighlight() clears cur_sgr so later SetSGRColor() calls are not skipped
- ed.syntax.c: fix ST_VARIABLE to keep state=ST_VARIABLE when first char is
  '?' or '#' so $?path/$#arr colour the full name; use S_ISREG+access(X_OK)
  in cmd_on_path(); add syntax_cache_clear() called by update_vars(STRpath)
- ed.syntax.h: add <stdint.h>; guard SYN_PACK/SYN_TOK/SYN_GLYPH macros with
  WIDE_STRINGS check; provide narrow-Char fallback (no-op); document header
- ed.decls.h: add extern for syntax_cache_clear()
- sh.set.c: remove getn() empty-string-returns-0 fallback; PATH change now
  calls syntax_cache_clear() to invalidate stale cmd_on_path() results
- tc.prompt.c: git cache refresh now checks composite state (HEAD +
  MERGE_HEAD, CHERRY_PICK_HEAD, REBASE_HEAD, rebase-merge/head-name) so
  merge/rebase/cherry-pick operations are detected without cwd pointer change
- vms.termcap.c: alias-scanning loop stops at first ':' so capability values
  containing '|' are not mistaken for additional terminal aliases
- dot.mcshrc: split generic Wayland exports from NVIDIA/hybrid GPU block
  (hybrid vars moved to ~/.mcshrc.local opt-in); wrap all interactive-only
  setup (bindkey, complete, alias, prompt, host-completion loop, stty/time)
  in if ($?prompt) so non-interactive shells skip them cleanly
- ISSUES.md: update Phase 9 architecture notes to reflect packed-Char model
  (no VcolorDisplay/ColorDisplay arrays); remove '^F' from predict-accept docs;
  update files-changed list with accurate descriptions

💘 Generated with Crush

Assisted-by: Claude Sonnet 4.6 via Crush <crush@charm.land>
@orpheus497
Copy link
Copy Markdown
Owner Author

Review Response — All Inline & Duplicate Comment Findings Addressed

Commit 85103e3 implements every finding from the review. Build confirmed clean on FreeBSD 15 (make → link succeeds, zero warnings on changed files).


Inline Comments

File Finding Action
ed.h (L181–187) Stale comment describes separate parallel colour arrays Fixed — comment now accurately describes the packed-Char model: tokens stored in Char bits, accessed via SYN_TOK()/SYN_GLYPH() in ed.screen.c; vcurrent_color described as the token being packed into Vdisplay cells
ed.inputl.c (L189–193) syntax_colorize() runs before the mutation switch; completion/expansion cases leave tokens stale when Refresh() is called Fixedsyntax_colorize() moved inside Repair() (called by all mutation cases: CC_COMPLETE*, CC_LIST_GLOB, CC_EXPAND_GLOB, CC_NORMALIZE_PATH, CC_EXPAND_VARS, CC_NORMALIZE_COMMAND) and also called in the CC_REFRESH path; post-dispatch block simplified to only set retval = CC_REFRESH when CC_NORM
ed.refresh.c (L45) Unused static int vcurrent_color_local Removed — declaration deleted; build confirmed no remaining references
ed.refresh.c (L348–408) DrawGhost probes tgetstr("so",…) but emits SGR 2; unconditional ESC[0m even when no SGR was set; byte-count not column-count Fixed — SGR dim is only reset when it was actually emitted (sgr_set flag); dim check uses GoodStr(T_me) proxy; unconditional reset removed; capbuf/caparea dead vars removed
ed.screen.c (L1006–1031) SetSGRColor emits ESC[0m but does not update cur_atr; StopHighlight emits T_me without clearing cur_sgr FixedSetSGRColor now sets cur_atr = 0 when emitting a full reset; StopHighlight clears cur_sgr = -1 so subsequent colour calls are not incorrectly skipped
ed.syntax.c (L365–385) $?path / $#arr — tokenizer exits ST_VARIABLE after the ?/# prefix, leaving the rest uncoloured Fixed — when ch == '?' or ch == '#' in ST_VARIABLE, state is kept as ST_VARIABLE; subsequent alphanumeric/underscore chars continue as part of the variable name
ed.syntax.c (L202–241) cmd_on_path() uses st.st_mode & S_IXUSR (misses group/other-exec, includes dirs); cache not invalidated on PATH/cwd changes Fixed — both the direct-path branch and the $PATH search loop now use S_ISREG(st.st_mode) && access(path, X_OK) == 0; added syntax_cache_clear() public function; wired into update_vars(STRpath) in sh.set.c
ed.syntax.h (L39–60) Missing <stdint.h>; no narrow-Char guard on bit-packing macros Fixed#include <stdint.h> added; SYN_SHIFT/SYN_MASK/SYN_PACK/SYN_TOK/SYN_GLYPH guarded by WIDE_STRINGS / sizeof(Char) check; narrow-Char fallback: SYN_PACK is identity, SYN_TOK returns 0, SYN_GLYPH is identity
ISSUES.md (L21–35) Architecture notes describe old VcolorDisplay/ColorDisplay parallel arrays Fixed — rewritten to describe packed-Char model end-to-end; SYN_TOK()/SYN_GLYPH() mentioned as accessors; so_write() / SetSGRColor() / cur_atr / StopHighlight behaviour updated; files-changed list updated
Makefile.in (L443–445) ed.syntax.h not in dependency graph; changes to it don't trigger rebuilds Fixed — added ed.syntax.${SUF} ed.screen.${SUF} ed.inputl.${SUF}: ed.syntax.h rule

Duplicate Comments

File Finding Action
dot.mcshrc (L11–27) Generic Wayland exports and NVIDIA/hybrid GPU variables mixed in one block Fixed — generic Wayland exports (CLUTTER_BACKEND, GDK_BACKEND, QT_QPA_PLATFORM, SDL_VIDEODRIVER, XDG_*) stay under the Wayland check; NVIDIA/hybrid exports (WLR_DRM_DEVICES, WLR_NO_HARDWARE_CURSORS, LIBVA_DRIVER_NAME, __NV_PRIME_RENDER_OFFLOAD, __GLX_VENDOR_LIBRARY_NAME, __VK_LAYER_NV_optimus) removed from main file with a comment directing users to opt-in via ~/.mcshrc.local
dot.mcshrc (L69–217) Interactive-only commands run unguarded in non-interactive shells Fixed — all of sections 4–9 (bindkey, complete, unalias/alias, prompt vars, host-completion foreach, stty/bindkey ^G, set time) wrapped in if ($?prompt) then … endif
ed.refresh.c (L348–408) DrawGhost bypasses display pipeline (addressed above under inline) ✓ addressed above
ISSUES.md (L91–95) e_predict_accept docs say "Right-Arrow / ^F" but only Right-Arrow is bound Fixed^F token removed; now reads "Right-Arrow" only
sh.set.c (L611–612) getn() returns 0 for empty string, hiding errors from callers Fixed — the if (*cp == '\0') return 0 fallback removed; getn() now issues ERR_BADNUM for empty input like any other invalid numeric string
tc.prompt.c (L414–420) Git cache only checks $cwd/.git/HEAD mtime; misses worktrees, merges, rebases Fixed — cache invalidation now iterates a marker-file list (.git/HEAD, .git/MERGE_HEAD, .git/CHERRY_PICK_HEAD, .git/REBASE_HEAD, .git/rebase-merge/head-name) and refreshes when any mtime differs
vms.termcap.c (L126–138) Alias-scanning loop can cross the first : and match capability values containing | Fixedcolon = strchr(bp, ':') computed once; inner loop breaks (and skips sscanf) when ptr >= colon; aliases from the name field only are matched

All 13 affected files committed, build passes cleanly.

@orpheus497 orpheus497 marked this pull request as ready for review April 21, 2026 13:13
Copilot AI review requested due to automatic review settings April 21, 2026 13:13
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR is a broad WIP update to mcsh that adds new interactive UX features (git-aware prompts, predictive autocomplete, per-keystroke syntax highlighting) and includes a set of portability/build-system updates aimed at improving FreeBSD 15 and general POSIX compatibility.

Changes:

  • Add interactive syntax highlighting (set syntax) by tokenizing the input buffer and emitting ANSI SGR during line rendering.
  • Add fish-style predictive autocomplete ghost text + accept-on-Right-Arrow behavior.
  • Add git branch/operation prompt escapes (%g / %G) and expand various portability/build fixes and docs.

Reviewed changes

Copilot reviewed 40 out of 43 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
vms.termcap.c Adds/repurposes a portable termcap implementation and fixes/bounds checks for various termcap routines.
tc.vers.c Adjusts the reported version string format.
tc.prompt.c Implements %g/%G git prompt expansions with caching and repo-state detection.
tc.const.c Adds STRsyntax constant to support set syntax.
sh.time.c Adds backslash escape handling in time format output.
sh.set.c Wires set/unset syntax behavior and clears syntax cache on PATH changes; adds an empty-string guard in getn().
sh.sem.c Adjusts when Dfix() runs and preserves builtin detection.
sh.func.c Updates doif() parsing/handling and changes reexecute() behavior to use F_REPEAT.
sh.exp.c Simplifies expr() to call exp0() directly; removes a glob/quote expansion block in exp6().
nls/catgen Adds a wrapper script for generating catalogs via gencat.
m4/progtest.m4 Adds gettext/autotools macro support file.
m4/po.m4 Adds gettext po-directory support macros.
m4/nls.m4 Adds NLS enable/disable macro.
m4/lib-prefix.m4 Adds library prefix detection macro.
m4/lib-ld.m4 Adds linker detection macro.
m4/intlmacosx.m4 Adds macOS CoreFoundation/CoreServices intl linking detection.
m4/iconv.m4 Adds iconv detection/linking macros.
m4/host-cpu-c-abi.m4 Adds host CPU/ABI detection macros.
m4/gettext.m4 Adds gettext discovery/selection macros.
m4/build-to-host.m4 Adds build-to-host path conversion helpers.
ed.syntax.h Introduces syntax token types, palette, and token packing macros into display Char.
ed.syntax.c Implements tokenizer, $PATH command lookup + cache, and public syntax APIs.
ed.screen.c Emits per-cell SGR colors in so_write() and tracks current SGR state.
ed.refresh.c Packs syntax tokens into Vdisplay and adds ghost text rendering routine.
ed.inputl.c Hooks syntax_colorize() into interactive input loop and clears ghost text on non-insert actions.
ed.h Adds GhostBuf and vcurrent_color globals for new UI features.
ed.defns.c Adds predict-accept editor command and binds Right-Arrow to accept ghost text.
ed.decls.h Declares new editor/syntax functions.
ed.chared.c Implements history-based ghost suggestion generation + accept behavior.
dot.mcshrc Rewrites the reference startup config and enables new features (set syntax, prompt updates, bindings, completions).
dch-template.in Adds Debian changelog template for releases.
configure.ac Adjusts baseline version define and patchlevel normalization.
config.h.in Adds TCSH_BASELINE_VERSION placeholder and relocates HAVE_LIBC_GLOB.
acaux/install-sh Updates Automake install-sh to a newer revision with improved option handling.
acaux/config.rpath Updates rpath helper with CLI help/version handling and newer gnulib content.
README.md Documents new features, prompt escapes, build instructions, and dotfile usage.
PLAN.md Updates plan status/details for VMS, Phase 9, and new features.
Makefile.in Adds ed.syntax to editor object list and updates dependencies.
ISSUES.md Adds detailed design notes and implementation summary for new features.
.gitignore Ignores local config/build artifacts and generated files.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread configure.ac
Comment thread tc.prompt.c
Comment thread ed.screen.c
Comment thread ed.refresh.c
Comment thread ed.inputl.c Outdated
…sh-style pushd/popd

configure.ac
- AC_DEFINE_UNQUOTED TCSH_BASELINE_VERSION now wraps the value as a
  quoted string literal so config.h emits a valid C string instead of
  an undefined identifier.

tc.prompt.c
- Git prompt cache now tracks git_head_mtime (for .git/HEAD) and a
  separate git_marker_mtime (max mtime across MERGE_HEAD,
  CHERRY_PICK_HEAD, REBASE_HEAD, rebase-merge/head-name).  Previously
  all marker files were compared against git_head_mtime, so a live
  MERGE_HEAD whose mtime differs from HEAD's would force a refresh on
  every prompt render.

ed.screen.c (SetSGRColor)
- The default-fg / no-bold branch now emits ESC[22;39m instead of
  ESC[0m.  ESC[0m was clobbering all SGR state (underline, standout)
  while cur_atr was not reset, causing desynchronisation.  cur_atr is
  now updated to clear the bold bit when bold is not requested.

ed.refresh.c (DrawGhost)
- SGR reset after ghost text now emits ESC[22;39m instead of ESC[0m,
  preserving cur_atr synchronisation on the incremental RefPlusOne path.

ed.inputl.c
- CC_NORM with `set syntax` active now calls syntax_colorize() directly
  rather than promoting the return code to CC_REFRESH.  Commands that
  already call Refresh() internally return CC_NORM, so the previous
  approach was triggering an extra full Refresh() per keystroke.

sh.dir.c + dot.mcshrc — zsh-style pushd/popd tree navigation
- dirs -v marks the current directory (index 0) with a → arrow.
- pushd/popd default to the vertical numbered display (dirs -v style)
  when no explicit format flag is given — replaces the flat horizontal
  output with a legible stack tree after every navigation.
- dfind() extended to handle -N arguments (counting from the bottom of
  the stack / oldest entry), mirroring zsh's cd -N semantics.
- dochngd() (cd built-in) detects cd -N before skipargs so the digit
  is not rejected as an unknown flag.

💘 Generated with Crush

Assisted-by: Claude Sonnet 4.5 via Crush <crush@charm.land>
@orpheus497
Copy link
Copy Markdown
Owner Author

Response to PR #3 review findings

Thanks to @coderabbitai, @google-gemini, and @Copilot for the thorough reviews across all rounds. This comment addresses everything raised — the latest commit f4821c9 closes all remaining findings.


Copilot (final round — 5 unresolved comments)

configure.acTCSH_BASELINE_VERSION macro expansion

AC_DEFINE_UNQUOTED([TCSH_BASELINE_VERSION], [TCSH_VERSION], …) was passing the M4 identifier TCSH_VERSION unquoted, which would generate #define TCSH_BASELINE_VERSION TCSH_VERSION — an undefined C identifier rather than a string literal. Fixed: the value is now ["TCSH_VERSION"] so config.h emits #define TCSH_BASELINE_VERSION "6.24.13".

tc.prompt.c — git cache marker-file mtime comparison bug

The previous code compared every marker file's mtime against git_head_mtime, so a live MERGE_HEAD (whose mtime is independent of HEAD) would always differ from git_head_mtime and force a full git-info refresh on every single prompt render while in merge state. Fixed: HEAD mtime is now tracked in git_head_mtime; the max mtime of all state-marker files (MERGE_HEAD, CHERRY_PICK_HEAD, REBASE_HEAD, rebase-merge/head-name) is tracked separately in git_marker_mtime. Both are compared independently and both are updated after a refresh.

ed.screen.cSetSGRColor ESC[0m desynchronises cur_atr

When sc->fg == 0 (default colour, no bold), the code emitted ESC[0m which resets all SGR attributes including underline and standout — but cur_atr was not cleared, causing the editor's attribute tracking to drift. Fixed: the branch now emits ESC[22;39m (cancel bold, reset fg only) and clears the BOLD bit in cur_atr to stay synchronised.

ed.refresh.cDrawGhost ESC[0m on the incremental path

DrawGhost was resetting all SGR with ESC[0m after writing ghost text, which on the RefPlusOne incremental path could clobber real attribute state without cur_atr being updated. Fixed: now emits ESC[22;39m to only undo bold/dim and reset the foreground, leaving other tracked attributes intact.

ed.inputl.c — redundant full Refresh() per keystroke with set syntax

The previous fix converted CC_NORM → CC_REFRESH whenever set syntax was active. Many commands (e.g. e_insert) already call RefPlusOne() or Refresh() internally and then return CC_NORM, so this caused a double refresh per keystroke. Fixed: syntax_colorize() is now called directly for CC_NORM without altering the return value, leaving the command's own screen-update path undisturbed.


pushd / popd — zsh-style directory stack navigation

Beyond the review fixes, pushd/popd have been upgraded to behave like zsh's directory stack:

  • dirs -v marks the current directory with a arrow at index 0 so it is immediately obvious where you are in the stack.
  • pushd / popd default to the vertical numbered display (equivalent to dirs -v) after every navigation instead of the previous flat horizontal output, making the stack readable at a glance.
  • cd -N is now supported: counts from the bottom of the stack (oldest entry), mirroring zsh's cd -N semantics. The existing cd +N (forward from current) continues to work unchanged. A bare cd - still switches to $owd.
  • dfind() extended to handle -N in addition to the existing +N, with dochngd() pre-screening digit-only -N args before skipargs so they are never mistaken for unknown flags.

Previously addressed (rounds 1–3, CodeRabbit / Gemini)

The following were resolved in earlier commits and are recorded here for completeness:

Area Fix
vms.termcap.c Buffer overflows (fgets capacity, sscanf bounds, tgoto snprintf, octal parsing, continuation-line guard, backslash literal '\')
ed.chared.c e_predict_accept NUL termination; predict_from_history trailing newline strip and INBUFSIZE bound; history-line materialisation before skip
ed.refresh.c Ghost erase logic overshoot (only erase when Cursor == LastChar); tgetstr safe buffer pass-through
ed.defns.c predict-accept NLS catalog ID collision (was 122, now 124)
sh.func.c doif() stores expr() result in tcsh_number_t instead of truncating int
sh.sem.c Dfix() short-circuits expansion for expression builtins
sh.set.c getn() strict empty-input handling
m4/lib-prefix.m4 dn;dnl typo
m4/po.m4 DLL error cleanup deletes correct target; GETTEXT_MACRO_VERSION updated to 0.23
dot.mcshrc Home/End bindings corrected to \e[1~/\e[4~/\eOH/\eOF; machine-specific GPU vars gated behind local override; set time moved to end; ^F accept binding removed from docs
tc.prompt.c gitdir NUL termination; worktree .git file parsing; tprintf_append_mbs for multibyte branch/op names
alacritty.toml Hardcoded shell path → program = "mcsh"; pywal import commented out as optional
PLAN.md / ISSUES.md / README.md Wording clarifications, DrawGhost status updated, ^F binding note removed

All builds pass clean on FreeBSD 15 (cc -g -O2, no new warnings introduced).

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 21, 2026

@orpheus497 — thank you for the thorough and well-organized fix summary across all review rounds. Here's a quick acknowledgment of the final-round fixes:

Copilot round — noted and confirmed sound:

  • configure.ac — The revert to ["TCSH_VERSION"] is correct: in AC_DEFINE_UNQUOTED, when TCSH_VERSION is an M4 macro, the bracketed-quoted form produces a C string literal ("6.24.13") in config.h, which is exactly what's needed. The earlier "remove quotes" guidance (round 1) was a mistake on my end — the Copilot finding correctly diagnosed the regression.

  • tc.prompt.c — Splitting into git_head_mtime + git_marker_mtime is the right model. Comparing state-marker mtimes against git_head_mtime was always semantically wrong since those files have independent lifetimes.

  • ed.screen.c / ed.refresh.c — Using ESC[22;39m (bold-off + fg-reset) instead of ESC[0m is the correct targeted reset. The cur_atr synchronisation fix in SetSGRColor closes the attribute-drift path cleanly.

  • ed.inputl.c — Calling syntax_colorize() for CC_NORM without promoting to CC_REFRESH is the right call; commands that update the display already do so via their own RefPlusOne()/Refresh() paths.

pushd/popd enhancement — The cd -N bottom-of-stack semantics and dfind() extension are a clean addition; the dochngd() pre-screening of digit-only -N args to avoid skipargs misinterpretation is a good guard.

Triggering a fresh review pass to cover commit f4821c9 and the new pushd/popd changes.

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

…ject state

README.md
- Complete rewrite of the Features section: language additions (function,
  short-circuit, pipe-to-variable, redirect in blocks, interactive comments),
  editor/interactive table (predictive autocomplete, syntax highlighting with
  token colour table, filetype colouring), prompt reference table, and a
  full directory-stack navigation section with worked example.
- Bug-fixes table extended to cover all PR3 findings: SGR desync fixes,
  git cache mtime, TCSH_BASELINE_VERSION, ed.inputl double-refresh, and
  all vms.termcap.c, m4, configure.ac, and acaux items.
- dot.mcshrc section rewritten as a section table covering all 9 sections
  and the root guard / local-override pattern.
- Source layout table extended to include ed.syntax.c/h.
- All doc cross-links and status notes corrected.

PLAN.md
- Phase 4 Completed table now includes #110 (%j fix) and #107 (short-circuit).
- Phase 7 (Documentation) updated to "complete (first pass)" with accurate
  per-file status.
- Phase 9 (native features) extended with zsh-style pushd/popd entry.
- Phase 8 added as its own phase (code review fixes) with status complete.
- Risk Register pruned of obsolete entries.
- Changelog updated with all 2026-04-21 round-2 work.

ISSUES.md
- New top section documents Phase 8 round-2 fixes (configure.ac string
  literal, git cache, SetSGRColor/DrawGhost ESC[22;39m, inputl double
  refresh) and Phase 9 zsh pushd/popd extension (arrow marker, default
  tree display, cd -N, dfind extension).
- Remaining open items section rewritten: deferred man page sections
  explicitly list set syntax, %g/%G, cd -N, and pushd display; test-suite
  section lists cd -N as a required case.
- All historical completed work sections preserved and accurately dated.

💘 Generated with Crush

Assisted-by: Claude Sonnet 4.5 via Crush <crush@charm.land>
@orpheus497
Copy link
Copy Markdown
Owner Author

Documentation update — 5337037

All three primary documentation files have been comprehensively rewritten to accurately reflect the current state of the project.

README.md

  • Features section split into Language, Editor/Interactive (with full syntax-highlight token colour table), Prompt, and Directory Stack subsections — every landed feature documented.
  • Bug fixes table extended to cover all PR3 findings: SGR desync (ESC[22;39m fix), git cache mtime independence, TCSH_BASELINE_VERSION string literal, ed.inputl.c double-refresh, and all vms.termcap.c, m4/, configure.ac, and acaux/ items.
  • Prompt reference table — all standard tcsh % escapes plus %g/%G.
  • Directory stack navigation section — worked example showing zsh-style numbered output with marker, cd -N usage, and the full alias table.
  • dot.mcshrc reference rewritten as a section table covering all 9 sections, root guard, and local-override pattern.
  • Source layout table extended to include ed.syntax.c/h.

PLAN.md

  • Phase 4 completed table now includes #110 (%j fix) and #107 (short-circuit fix).
  • Phase 7 (Documentation) status updated to complete (first pass) with per-file status.
  • Phase 9 (native features) extended with the zsh-style pushd/popd tree display entry.
  • Phase 8 added as its own phase (code review fixes), status complete.
  • Risk register pruned; changelog updated with all 2026-04-21 round-2 work.

ISSUES.md

  • New top section accurately documents Phase 8 round-2 fixes and the Phase 9 pushd/popd extension.
  • Remaining open items rewritten: deferred tcsh.man.in sections explicitly call out set syntax, %g/%G, cd -N, and pushd tree display as needing man-page entries. Test suite section lists cd -N as a required test case.
  • All historical completed-work sections preserved and accurately dated.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 41 out of 44 changed files in this pull request and generated 7 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread tc.prompt.c
Comment on lines +792 to +796
snprintf(_hp, sizeof(_hp), "%s/.git/HEAD",
short2str(gcwd));
if (stat(_hp, &_st) == 0 &&
_st.st_mtime != git_head_mtime)
need_refresh = 1;
Copy link

Copilot AI Apr 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The git prompt cache invalidation checks ${cwd}/.git/HEAD mtime (and .git/* markers). In worktrees/submodules where .git is a file pointing to the real gitdir, these paths often don’t exist, so git_head_mtime can stay 0 and the cache may not refresh after checkouts/merges. Consider caching the resolved gitdir/HEAD path from git_get_info() and stat’ing that instead.

Copilot uses AI. Check for mistakes.
Comment thread ed.syntax.c
Comment on lines +165 to +168
for (i = 0; i < CMD_CACHE_SIZE; i++) {
if (cmd_cache[i].found >= 0 &&
strncmp(cmd_cache[i].name, name, CMD_CACHE_NAMELEN - 1) == 0)
return cmd_cache[i].found;
Copy link

Copilot AI Apr 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cache_store() truncates command names to CMD_CACHE_NAMELEN-1, but cache_lookup() uses strncmp(..., CMD_CACHE_NAMELEN-1) without checking the original length. For names ≥ 63 chars this can cause false cache hits between distinct commands sharing a prefix. Consider not caching overlong names, or store/compare the full length (or a hash) to avoid collisions.

Copilot uses AI. Check for mistakes.
Comment thread ed.syntax.c
Comment on lines +592 to +601
/* Mark unterminated quotes as errors */
if (state == ST_SQUOTE || state == ST_DQUOTE ||
state == ST_BACKTICK || state == ST_BRACE_VAR) {
for (i = 0; i < len; i++) {
if ((SynToken)SyntaxColor[i] == SYN_SQUOTE ||
(SynToken)SyntaxColor[i] == SYN_DQUOTE ||
(SynToken)SyntaxColor[i] == SYN_BACKTICK ||
(SynToken)SyntaxColor[i] == SYN_VARIABLE)
SyntaxColor[i] = SYN_ERROR;
}
Copy link

Copilot AI Apr 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The unterminated-quote error pass treats any SYN_VARIABLE as SYN_ERROR, which can incorrectly mark $var occurrences that appear before the unmatched quote began. Consider tracking the start offset of the active unterminated region and only marking that region (or only tokens belonging to the active quote state) as SYN_ERROR.

Suggested change
/* Mark unterminated quotes as errors */
if (state == ST_SQUOTE || state == ST_DQUOTE ||
state == ST_BACKTICK || state == ST_BRACE_VAR) {
for (i = 0; i < len; i++) {
if ((SynToken)SyntaxColor[i] == SYN_SQUOTE ||
(SynToken)SyntaxColor[i] == SYN_DQUOTE ||
(SynToken)SyntaxColor[i] == SYN_BACKTICK ||
(SynToken)SyntaxColor[i] == SYN_VARIABLE)
SyntaxColor[i] = SYN_ERROR;
}
/* Mark only the trailing unterminated quote/brace region as errors. */
if (state == ST_SQUOTE || state == ST_DQUOTE ||
state == ST_BACKTICK || state == ST_BRACE_VAR) {
ptrdiff_t mark_start = len;
while (mark_start > 0) {
SynToken tok = (SynToken)SyntaxColor[mark_start - 1];
if (tok != SYN_SQUOTE &&
tok != SYN_DQUOTE &&
tok != SYN_BACKTICK &&
tok != SYN_VARIABLE)
break;
mark_start--;
}
for (i = mark_start; i < len; i++)
SyntaxColor[i] = SYN_ERROR;

Copilot uses AI. Check for mistakes.
Comment thread tc.prompt.c
Comment on lines +183 to +186
* op is empty string when no special operation is in progress, or one of:
* MERGING, REBASING, REBASING-i, REBASING-m, CHERRY-PICKING, REVERTING,
* BISECTING.
*
Copy link

Copilot AI Apr 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The git_get_info() comment documents operation states including REBASING-m, but the rebase-apply path sets op to "AM" (line 366). Either document AM as a possible value or rename it to match the documented set so %G has stable, documented output values.

Copilot uses AI. Check for mistakes.
Comment thread tc.vers.c

version = xasprintf(
"mcsh %d.%.2d.%.2d (%s) %s (%" TCSH_S "-%" TCSH_S "-%" TCSH_S ") [tcsh baseline %s] options %s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s",
"mcsh %d.%.2d.%.2d (%s) %s (%" TCSH_S "-%" TCSH_S "-%" TCSH_S ") tcsh-baseline:%s options %s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s",
Copy link

Copilot AI Apr 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The $version banner format changed from [tcsh baseline %s] to tcsh-baseline:%s. README.md still documents the bracketed form, and any external tooling that greps for the old marker may break. Consider keeping the previous bracketed marker (or updating all docs/consumers consistently).

Copilot uses AI. Check for mistakes.
Comment thread sh.func.c
execute(t, (tpgrp > 0 ? tpgrp : -1), NULL, NULL, TRUE);
cleanup_until(t);
cleanup_until(&paraml);
execute(kp, (tpgrp > 0 ? tpgrp : -1), NULL, NULL, TRUE);
Copy link

Copilot AI Apr 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

reexecute() now directly calls execute(kp, ...) on the same struct command (line 394). This bypasses the previous re-lex/alias/syntax rebuild, so reexecuted tails may not be parsed (pipes/redirections/metachars become plain args) and aliases may not expand. Consider restoring the prior wordent+alias()+syntax() path (or equivalent) before executing.

Copilot uses AI. Check for mistakes.
Comment thread sh.sem.c
Comment on lines +171 to 172
if ((t->t_dflg & F_REPEAT) == 0)
Dfix(t); /* $ " ' \ */
Copy link

Copilot AI Apr 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dfix(t) is now gated only on F_REPEAT. Expression-evaluating builtins (e.g. if, while, test, let) need to skip Dfix() so $/quote expansion doesn’t happen eagerly and break short-circuit evaluation. Consider computing bifunc = isbfunc(t) before this point and skipping Dfix() for those builtins (as the previous code did), rather than keying off F_REPEAT.

Copilot uses AI. Check for mistakes.
- configure.ac: remove extra M4 quoting brackets around TCSH_VERSION in
  AC_DEFINE_UNQUOTED so the macro is expanded correctly and config.h emits
  the literal version string "6.24.13" instead of the identifier TCSH_VERSION.

- sh.sem.c: skip Dfix() for expression-evaluating builtins (doif, dowhile,
  dotest, dolet, doexit) to preserve lazy evaluation semantics.  Previously
  all NODE_COMMAND paths called Dfix() unconditionally, expanding $, ", ', \
  before operands reached expr(), defeating TEXP_IGNORE / TEXP_NOGLOB guards.
  isbfunc() is now called early, before Dfix(), to detect the exempted builtins.

- ISSUES.md: document both fixes in the completed-work log.

All items raised in the CodeRabbit PR3 round-1 and round-2 reviews are now
resolved; remaining notes (acaux/install-sh patterns, dch-template.in, m4
files, alacritty.toml, dot.mcshrc) were either already correct or addressed
in prior rounds.

💘 Generated with Crush

Assisted-by: Claude Sonnet 4.5 via Crush <crush@charm.land>
@orpheus497
Copy link
Copy Markdown
Owner Author

Response to CodeRabbit PR3 round-2 review (review 4148466671)

Thank you for the thorough second-pass review. Below is a full accounting of every comment with disposition and commit reference.


✅ Fixed in this round (commit 543b61f)

configure.acTCSH_BASELINE_VERSION M4 macro expansion

Lines 27-29: Use TCSH_VERSION M4 macro instead of literal string

Fixed. The extra inner M4 quoting brackets ["TCSH_VERSION"] prevented macro expansion, emitting the literal identifier TCSH_VERSION into config.h instead of the version string. Changed to [TCSH_VERSION] so M4 expands the macro correctly; config.h now emits #define TCSH_BASELINE_VERSION "6.24.13".

sh.sem.c – Skip Dfix() for expression-evaluating builtins

Lines 83-84: Skip Dfix() for doif, dowhile, dotest, dolet, doexit to preserve lazy evaluation

Fixed. execute() now calls isbfunc() before Dfix() and skips expansion for the five expression-evaluating builtins (doif, dowhile, dotest, dolet/@, doexit). This restores proper TEXP_IGNORE/TEXP_NOGLOB semantics inside expr().


✅ Already fixed in prior rounds (addressed but re-confirmed here)

Comment File Status
Case patterns only match single-char names ([=\(\)!][=\(\)!]*) acaux/install-sh lines 162-168, 211, 270 Fixed in round 1 — all three instances already have * suffix
alacritty.toml hardcoded path alacritty.toml line 6 Fixed in round 1 — uses "mcsh" (PATH-resolved)
alacritty.toml pywal nondeterminism alacritty.toml line 2 Fixed in round 1 — import commented out with explanation
dch-template.in placeholder changelog line dch-template.in line 4 Fixed in round 1 — now contains substantive release notes
dch-template.in distribution hardcoded "unstable" dch-template.in line 1 Fixed in round 1 — changed to UNRELEASED
ed.chared.c predict-accept missing NUL terminator ed.chared.c lines 3900-3905 Fixed in round 1 — bounds check + *LastChar = '\0'
ed.chared.c history materialization / trailing newline strip ed.chared.c lines 3881-3891 Fixed in round 1 — sprlex() called before histline access; loop stops at \n/\r
ed.chared.c truncated suggestion rejection ed.chared.c lines 3901-3909 Fixed in round 1 — wholesale reject if ghost doesn't fit
ed.chared.c digit self-insert misses prediction update ed.chared.c lines 1471-1488 Fixed in round 1 — predict_from_history() called in e_digit() non-argument branch
ed.defns.c duplicate NLS catalog ID 122 ed.defns.c lines 1766-1770 Fixed in round 1 — changed to ID 124
ed.inputl.c GhostBuf cleared without Refresh() ed.inputl.c lines 191-193 Fixed in round 1 — Refresh() called when GhostBuf becomes empty
ed.refresh.c stale ghost tail / prev_ghost_cols ed.refresh.c lines 333-368 Fixed in round 1 — static prev_ghost_cols tracks and erases previous overlay
ed.refresh.c tgetstr with NULL area unsafe ed.refresh.c lines 365-370 Fixed in round 1 — DrawGhost does not use tgetstr; emits SGR directly
m4/lib-prefix.m4 dn; typo m4/lib-prefix.m4 lines 151-154 Fixed — corrected to dnl
m4/po.m4 error-cleanup removes wrong file m4/po.m4 lines 426-434 Fixed — cleanup now removes $frobbedlang/$(DOMAIN).resources.dll
m4/po.m4 GETTEXT_MACRO_VERSION 0.22 vs 0.23 m4/po.m4 lines 33-35 Fixed — bumped to 0.23
sh.exp.c lazy evaluation in exp6() sh.exp.c lines 580-590 Already correct — fallback path uses Strsave(cp) when TEXP_NOGLOB is set
sh.func.c boolean coercion tcsh_number_t vs int sh.func.c lines 349-353 Fixed in round 1 — i declared as tcsh_number_t
sh.set.c getn() empty input silently returns 0 sh.set.c lines 604-605 Fixed in round 1 — empty string triggers ERR_BADNUM
tc.prompt.c %g/%G stale cache across git ops tc.prompt.c lines 738-743 Fixed in round 2 — HEAD mtime and marker mtime tracked independently
tc.prompt.c gitdir NUL termination / snprintf overlap tc.prompt.c lines 205-208 Fixed in round 1 — explicit NUL termination after memcpy; temp buffer used
tc.prompt.c raw Strbuf_append1 loops break multibyte tc.prompt.c lines 747-753 Fixed in round 1 — tprintf_append_mbs() used for git_branch and git_op
tc.prompt.c .git worktree file not handled tc.prompt.c lines 211-218 Fixed in round 2 — parses gitdir: pointer, resolves relative paths, checks resolved location
vms.termcap.c multiple buffer safety issues vms.termcap.c all Fixed in round 1 — bounded sscanf, strcmp for exact match, snprintf in tgoto, octal parsing, sizeof(bp) pointer-size bug
dot.mcshrc forces machine-specific Wayland/GPU env vars dot.mcshrc lines 9-23 Fixed in round 1 — gated behind /dev/dri + $?WAYLAND_DISPLAY check; GPU overrides documented as local-override only
dot.mcshrc wrong PageUp/PageDown sequences dot.mcshrc lines 80-82 Already correct — Home/End bindings use \e[1~, \e[4~, \eOH, \eOF, \e[H, \e[F

All review comments from both rounds are now addressed. Build is clean (make -j4, no warnings or errors). Pushed as commit 543b61f on dev2.

Skipping Dfix() for doif/dowhile/dotest/dolet/doexit causes $?VAR and
other variable references inside if-condition operands to never be
expanded, producing "No match." errors at runtime (e.g. the $?WAYLAND_DISPLAY
guard in dot.mcshrc).

Dfix() *must* run before expr() — it is the sole mechanism that expands
$var, "...", '...' and \ in command words.  The reviewer's concern about
lazy evaluation applies inside expr()/exp6() itself, not at the Dfix layer.
The pre-existing TEXP_NOGLOB/TEXP_IGNORE handling in exp6() is correct and
sufficient; no Dfix change is needed.

Document the regression and rationale in ISSUES.md.

💘 Generated with Crush

Assisted-by: Claude Sonnet 4.5 via Crush <crush@charm.land>
@orpheus497 orpheus497 merged commit e94df12 into main Apr 21, 2026
1 check passed
@orpheus497 orpheus497 deleted the dev2 branch April 21, 2026 15:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants