Skip to content

feat: terminal sanitizing, wrapping and diff painting - #12

Merged
dgrieser merged 3 commits into
mainfrom
feat/terminal-helpers
Nov 12, 2026
Merged

dgrieser merged 3 commits into
mainfrom
feat/terminal-helpers

Conversation

@dgrieser

Copy link
Copy Markdown
Collaborator

The terminal code swctl still did by hand, generalized.

term

  • Sanitize(s) / SanitizeLines(s): make untrusted text (CI job logs, mail, server-provided strings) safe to write to a terminal. Colours (16, 256, 24-bit) and safe SGR attributes stay; every other escape sequence (OSC 52 clipboard writes, OSC 8 links, title changes, cursor movement) and every C0/C1 control character except newline goes; tabs expand to 8-column tab stops; on each line only the text after the last carriage return stays, as a terminal shows it; invalid UTF-8 becomes U+FFFD. SanitizeLines makes every line stand alone (colours carried over, reset at the end). Fuzzed.
  • Wrap(s, width, indent): word wrap at spaces with a hanging indent; escape sequences have no width.
  • EscapeLen fix (also StripANSI, VisibleWidth, Truncate, live): ESC ( B (what tput sgr0 emits) is one sequence, ESC X strings are recognised, and a lone ESC no longer swallows half a UTF-8 character.

theme

  • Roles Added, Removed, DiffHeader, DiffHunk (Default: 114, 203, bold, 117 — Success and Error's tones, laid out like git).
  • Theme.For(w): the theme where w takes colour, Plain otherwise.
  • DiffScanner + DiffKind: reads a unified diff the way patch does (hunk line counts decide whether --- x is a removed line or a file header).
  • Theme.PaintDiff(text) / PaintDiffLine(kind, line).

Compatibility

No API removed or changed. Theme has four more fields (an unkeyed Theme{…} literal would no longer compile; none exist in our repos). EscapeLen results change only for the sequences it mis-measured.

swctl, atoss-cli and mittwald-outlook-bridge build and pass their tests against this branch.

🤖 Generated with Claude Code

dgrieser and others added 3 commits November 12, 2026 14:27
What swctl still did by hand, generalized:

- term.Sanitize and SanitizeLines make untrusted text, such as CI job
  logs or mail, safe to write to a terminal: colours and a safe set of
  SGR attributes stay, every other escape sequence and control
  character goes (no clipboard writes, disguised links or title
  changes), tabs expand to tab stops, and a carriage return keeps what
  a terminal would show.
- term.Wrap word-wraps to a width with a hanging indent, ignoring the
  width of escape sequences.
- EscapeLen counts ESC ( B (tput sgr0) and ESC X strings whole and
  never splits a UTF-8 character after a lone ESC, so StripANSI,
  VisibleWidth and Truncate no longer leave stray bytes.
- theme: Added, Removed, DiffHeader and DiffHunk roles, Theme.For,
  DiffScanner, which reads a unified diff the way patch does, and
  PaintDiff / PaintDiffLine.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Sanitize expanded tabs counting cells per rune, so a joined emoji
  such as a ZWJ sequence counted four cells instead of the two a
  terminal draws, and the tab stop came out short. The column is now
  measured with VisibleWidth.
- Wrap treated an escape sequence standing alone, such as a trailing
  reset, as a word whose preceding spaces could break the line, leaving
  the reset on a line of its own. It now stays on the line it follows,
  and its spaces go before the next visible word.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…apes in Wrap

- Sanitize and SanitizeLines split into lines before reading escape
  sequences, so an OSC, DCS, SOS, PM or APC string containing a newline
  left the rest of its payload behind as text. Escape strings are now
  dropped from the whole text first, to their terminator or the end.
- Every tab measured the line so far again, which made a long line of
  tabs quadratic. The column is now kept as the tabs go, still counting
  grapheme clusters.
- A style sequence opening a line took the first word's place in Wrap,
  so the line could break before any visible word.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@dgrieser
dgrieser merged commit 0ed0552 into main Nov 12, 2026
13 checks passed
@dgrieser
dgrieser deleted the feat/terminal-helpers branch November 12, 2026 13:40
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.

1 participant