Skip to content

dtterm: modern enhancements#1

Open
runlevel5 wants to merge 16 commits into
masterfrom
feat/dtterm-wide-color-storage
Open

dtterm: modern enhancements#1
runlevel5 wants to merge 16 commits into
masterfrom
feat/dtterm-wide-color-storage

Conversation

@runlevel5

@runlevel5 runlevel5 commented May 19, 2026

Copy link
Copy Markdown
Owner

To make dtterm a 1st class terminal

TODO:

Tier 1 — "you'll notice immediately"

  1. Alternate screen buffer (DECSET 1049 / 1047)
    - Currently when you run vim, htop, less, etc., they paint over your shell history and destroy it on exit.
    - With alt-screen, full-screen apps switch to a secondary buffer and the original shell content is preserved when they quit.
    - Effort: medium. Needs a second TermBuffer + DECSET branches. Files: TermParse.c (1049 / 1047 / 47 cases) + a buffer-swap function in TermBuffer.c.
  2. Mouse reporting (DECSET 1000 / 1002 / 1003 / 1006)
    - The stub case 1000: /* don't send mouse x and y */ has been sitting in TermParse.c:1822 waiting to be implemented for thirty years.
    - Without it: tmux / vim / mc mouse modes don't work, no click-to-position-cursor in vim, no scrollwheel-paging in less.
    - Effort: medium. Needs Xt event handlers on press/motion/release + protocol formatter for the four mouse modes (1006 SGR-encoding is the modern one).
  3. Bracketed paste (DECSET 2004)
    - When enabled, dtterm wraps pasted text in \e[200~ … \e[201~ so shells / editors can distinguish a paste from typing.
    - Mitigates "paste-bomb" attacks where pasted text containing a newline triggers immediate execution.
    - Effort: small. One DECSET branch + a flag consulted in the paste / selection-insert path.

Tier 2 — "modern shell integrations want them"

  1. Focus reporting (DECSET 1004) — emits \e[I / \e[O on focus in/out. Used by vim autosave-on-blur, tmux for activity tracking. Tiny commit.
  2. OSC 7 — current directory hint (\e]7;file://host/path\e\). Shells emit this; terminals use it to label tabs or open new tabs at the same cwd. Tiny commit.
  3. OSC 133 semantic prompts — \e]133;A\e\ start prompt, B end prompt, C start command, D end-with-exit-status. Used by VS Code / Wezterm / Kitty for "jump to previous prompt" navigation. Small commit.
  4. OSC 8 click-to-open — finish what we started. Wire a Button1 event handler that looks up the URL of the clicked cell (currently we don't even track URLs per cell) and invokes xdg-open. Medium — needs per-cell
    URL id storage which is real work.

Tier 3 — "spec compliance / niche"

  1. REP (CSI Pn b) — "repeat previous character Pn times". Some apps emit this for efficient encoding of long runs. One-handler commit.
  2. SGR 4:N underline-style sub-parameter — 4:0 none, 4:1 single, 4:2 double, 4:3 curly, 4:4 dotted, 4:5 dashed. Needs the strict ECMA-48 sub-parameter parsing I deliberately skipped. Medium — touches the parser
    plus four new renderer line styles.
  3. DECRQM / DECRPM mode-report responses — let apps query which DECSETs the terminal honours. Useful for compatibility detection. Small.
  4. REP / DECCRA / DECCARA / DECRQSS — various less-common CSI completers. Each is a small handler.

Tier 4 — major work, major payoff

  1. Xft / Pango text rendering — replace the bitmap-X11-font drawing pipeline with antialiased / hinted vector rendering. Major rewrite (the renderer is built on XDrawImageString / XmbDrawImageString); but the
    visual quality jump is the biggest cosmetic change available.
  2. Sixel graphics (\eP q ... \e\) — inline raster images. gnuplot, feh, chafa use it. Major: needs an image-rendering pipeline parallel to the text grid.
  3. Kitty graphics protocol (\e_G...\e\) — newer, richer alternative to Sixel. Even bigger.
  4. Per-cell wide-char support cleanup — dtterm already has TermPrimBufferWc.c but its handling of CJK double-width and combining marks has known rough edges. Polish would help East Asian terminal users a lot.

runlevel5 and others added 16 commits May 19, 2026 01:26
Promote dtterm's per-cell colour fields, the enhValue transport typedef,
and the parser-side colour state variables from the original 4-bit /
8-bit shape to a full 32-bit packed encoding.

The encoding carries a one-byte mode flag in the high bits and a 24-bit
payload in the low bits:

  bits 24-31  mode  -- 0=DEFAULT, 1=INDEXED, 2=DIRECT_RGB
  bits  0-23  payload  -- palette slot or 0xRRGGBB

DEFAULT is all-zero, so the zero-initialised state of fresh cells
continues to mean 'use the widget default fg / bg' without any per-cell
fix-up at allocation time.  SGR 39 / 49 round-trip back to DEFAULT
instead of getting confused with INDEXED 0.

User-visible behaviour is unchanged: the SGR handler now stores
ENH_MAKE_INDEXED(1..8) for 30-37 / 40-47, and the renderer decodes that
back to the existing colorPairs[1..8] slots.  No new escape sequences
are recognised yet; this change just lifts the type ceiling that
previously capped colour at 16 indexed slots, so the 256-colour and
24-bit truecolour additions can land as small follow-ups.

Per-cell enhancement memory grows from ~4 to ~12 bytes (roughly 640 KB
extra for an 80x24 window with 1000-line scrollback at the defaults).

Files touched:

  cde/lib/DtTerm/TermPrim/TermPrimBuffer.h
        - enhValue: unsigned char -> uint32_t
        - ENH_MODE_*, ENH_COLOR_MODE, ENH_COLOR_PAYLOAD, ENH_IS_DEFAULT,
          ENH_MAKE_DEFAULT, ENH_MAKE_INDEXED, ENH_MAKE_RGB helpers
  cde/lib/DtTerm/Term/TermBufferP.h
        - DtTermEnhPart: fgColor:4 / bgColor:4 bit-fields replaced by
          full uint32_t fields; video / field / font kept as bit-fields
  cde/lib/DtTerm/Term/TermBuffer.h
        - COLOR_MASK widened from 0x0F to 0xFFFFFFFFU
  cde/lib/DtTerm/Term/TermData.h
        - enhFgColorState / enhBgColorState widened to uint32_t in both
          DtTermDataRec and VtSaveCursorRec
  cde/lib/DtTerm/Term/TermFunction.c
        - _DtTermVideoEnhancement uses ENH_MAKE_DEFAULT / ENH_MAKE_INDEXED
          for SGR 0 / 30-37 / 39 / 40-47 / 49
  cde/lib/DtTerm/Term/TermEnhance.c
        - _DtTermEnhProc decodes the packed colour value through a small
          helper and indexes colorPairs[] with the resolved slot

Co-Authored-By: Trung Le <trung.le@ruby-journal.com>
Extend the indexed-colour palette from 8 ANSI slots to the full 16
slots used by xterm-compatible bright-colour SGR sequences, and apply
the standard bold-brightens-fg convention so a bold ANSI 30-37 fg
renders with the brighter variant.

  SGR 90-97   -> bright fg, stored as ENH_MAKE_INDEXED(9..16)
  SGR 100-107 -> bright bg, stored as ENH_MAKE_INDEXED(9..16)
  SGR 1 + 30-37 fg -> bright variant at render time (xterm convention)

The widget's colorPairs[] array grows from 9 to 17 entries (default +
16 ANSI slots).  Slots 9..16 are seeded with xterm-style RGB defaults:

  slot  9: bright black   #808080
  slot 10: bright red     #ff0000
  slot 11: bright green   #00ff00
  slot 12: bright yellow  #ffff00
  slot 13: bright blue    #5c5cff
  slot 14: bright magenta #ff00ff
  slot 15: bright cyan    #00ffff
  slot 16: bright white   #ffffff

The destroy loop's upper bound is updated to cover all 17 slots, which
also fixes a pre-existing off-by-one that left the slot-8 (white)
pixels unfreed when the widget was destroyed.

The bold-brightens-fg promotion lives in _DtTermEnhProc rather than
the SGR handler so SGR 22 (bold off) restores the original colour
without requiring the parser to remember whether the explicit colour
was a 30-37 or a 90-97 sequence.  Promotion only fires for
ENH_MODE_INDEXED payloads 1..8 -- ENH_MODE_DEFAULT and future
ENH_MODE_RGB values pass through untouched.

Files touched:

  cde/lib/DtTerm/Term/TermData.h
        - VtColorPairRec colorPairs[9] -> colorPairs[17]
  cde/lib/DtTerm/Term/TermColor.c
        - _DtTermColorInit seeds slots 9..16 with bright RGB defaults
        - _DtTermColorDestroy iterates all 17 slots (was 8)
  cde/lib/DtTerm/Term/TermFunction.c
        - _DtTermVideoEnhancement recognises SGR 90-97 / 100-107
  cde/lib/DtTerm/Term/TermEnhance.c
        - _DtTermResolveColorPair accepts indexed payloads 1..16
        - _DtTermEnhProc applies bold-brightens-fg for INDEXED 1..8
Add support for the xterm 256-colour palette and rewrite the SGR
parameter loop into a peek-ahead state machine that consumes the
compound colour sub-sequences atomically:

  38;5;N        indexed 256-colour foreground
  48;5;N        indexed 256-colour background
  38;2;R;G;B    direct-RGB foreground (parser only; renderer in a
                follow-up change)
  48;2;R;G;B    direct-RGB background (parser only; renderer in a
                follow-up change)

The maximum number of parameters per CSI sequence is raised from 16
to 32 in both NPARAM (Term/TermParse.c) and NUM_PARMS
(TermPrim/TermPrimParserP.h) so compound sequences like
'0;1;38;2;255;128;0;48;2;64;32;16;4' fit with headroom.

The render-side resolver is reorganised.  _DtTermResolveColorPair
returned a colorPairs[] index, which can only express the static
0..16 slots.  In its place are two Pixel-returning helpers,
_DtTermResolveFgPixel and _DtTermResolveBgPixel, that handle DEFAULT,
the 16 named slots and -- when the payload is 17..256 -- delegate to
the new lazy 256-palette resolver.  This keeps the rest of
_DtTermEnhProc agnostic to the colour mode and gives Phase D
(direct-RGB rendering) a single hook to extend.

The xterm 256 palette is allocated lazily as colours appear on screen:

  TermData.h        Pixel pal256[256]; Boolean pal256Allocated[256];
  TermColor.c       _DtTermColor256RGB returns the canonical xterm
                    RGB triple (16..231: 6x6x6 cube using xterm's
                    {0x00,0x5f,0x87,0xaf,0xd7,0xff} step table;
                    232..255: 24 grey steps from 8 to 238).
                    _DtTermResolve256Pixel allocates each entry on
                    first use via XAllocColor and caches the Pixel;
                    falls back to the widget's default fg if the
                    server's colormap is exhausted.

_DtTermColorDestroy now batches the populated 256-palette slots into a
single XFreeColors call alongside the existing colorPairs cleanup.

A new helper _DtTermSetColor (TermFunction.c) writes a packed enhValue
to fg or bg and stamps it on the cell at the cursor.  The parser uses
it for the 38;5 / 38;2 / 48;5 / 48;2 paths; the legacy SGR 30-37 /
90-97 paths continue to go through _DtTermVideoEnhancement, which is
re-entered for any non-colour parameter in a chained sequence.

Files touched:

  cde/lib/DtTerm/TermPrim/TermPrimParserP.h
        - NUM_PARMS 20 -> 32 (sized for parms[NUM_PARMS])
  cde/lib/DtTerm/Term/TermParse.c
        - NPARAM 16 -> 32
        - _DtTermCharAttributes rewritten as a peek-ahead loop
  cde/lib/DtTerm/Term/TermData.h
        - Pixel pal256[256] + Boolean pal256Allocated[256]
  cde/lib/DtTerm/Term/TermColor.h
        - _DtTermResolve256Pixel extern
  cde/lib/DtTerm/Term/TermColor.c
        - _DtTermCube6 cube-step table
        - _DtTermColor256RGB (static helper)
        - _DtTermResolve256Pixel (lazy allocation + cache)
        - _DtTermColorDestroy frees the populated 256-palette slots
  cde/lib/DtTerm/Term/TermEnhance.c
        - _DtTermResolveColorPair (int) replaced with
          _DtTermResolveFgPixel / _DtTermResolveBgPixel (Pixel)
        - _DtTermEnhProc simplified
  cde/lib/DtTerm/Term/TermFunction.h
        - _DtTermSetColor extern
  cde/lib/DtTerm/Term/TermFunction.c
        - _DtTermSetColor (packed-enhValue setter)

Direct-RGB rendering (ENH_MODE_RGB) is wired through the parser but
the resolver still drops to the default for that mode -- the next
change adds visual detection and the masks-and-shifts pixel synthesis.
Wire up the rendering side of the direct-RGB SGR sub-sequences whose
parsing landed in the previous change.  Pixel synthesis uses the X
visual's RGB masks on TrueColor / DirectColor displays, with no
XAllocColor round-trip and nothing to free at destroy time.  On
visuals that can't represent arbitrary 24-bit colour, the request
rounds to the nearest of the 16 indexed defaults so the experience
degrades gracefully rather than failing or crashing.

Visual detection runs once per widget at _DtTermColorInit time and
caches the masks / shifts / bit-counts on DtTermData.  The shift for
each channel is derived from the position of the lowest set bit in
the mask; the bit-count from popcount of the mask.  This means the
synthesizer also handles non-default visuals such as 15-bit RGB555,
16-bit RGB565, and 30-bit deep-colour without special-casing.

The 8-bit-channel scaler maps cleanly in both directions:
  bits >= 8 (e.g. 8 or 10): v << (bits - 8) << shift
  bits <  8 (e.g. 5 or 6):  v >> (8 - bits) << shift

So r=0xff on a 5-bit channel collapses to 0x1f (max), and on a
10-bit channel expands to 0x3fc (just below max).  The result is then
ANDed with the channel's mask to guarantee no stray bits.

When the screen isn't TrueColor / DirectColor, the resolver picks the
nearest match from the 16 indexed defaults using straight Euclidean
distance in 8-bit RGB and routes through the existing
colorPairs[] / _DtTermColorInitializeColorPair path -- the same path
the SGR 30-37 / 90-97 sequences already use.  No colormap exhaustion
risk, no separate pixel-tracking required.

The fg / bg resolvers in TermEnhance.c both gained an ENH_MODE_RGB
arm that decodes the packed 0xRRGGBB payload and hands off to
_DtTermResolveRGBPixel.  The isBg flag is forwarded so the
nearest-of-16 fallback picks .fg.pixel for foreground requests and
.bg.pixel for background requests, matching what the indexed path
does.

Direct-RGB Pixels are not tracked on the destroy path: they were never
allocated, so there is nothing for XFreeColors to release.  The
existing colorPairs[] and pal256 free loops are unchanged.

Files touched:

  cde/lib/DtTerm/Term/TermData.h
        - isTrueColor + red/green/blue mask, shift, bits triples
  cde/lib/DtTerm/Term/TermColor.h
        - _DtTermResolveRGBPixel extern
  cde/lib/DtTerm/Term/TermColor.c
        - _DtTermVisualLowBit, _DtTermVisualPopCount helpers
        - _DtTermDetectVisual (one-shot at _DtTermColorInit)
        - _DtTermScaleChannel (8-bit -> mask-width channel scaler)
        - _DtTermNearestOf16 (Euclidean nearest indexed colour)
        - _DtTermResolveRGBPixel (TrueColor synth + fallback)
  cde/lib/DtTerm/Term/TermEnhance.c
        - _DtTermResolveFgPixel / _DtTermResolveBgPixel ENH_MODE_RGB
          arms
Ship two new terminfo entries alongside the existing dtterm one, and
document the new SGR codes recognised by the terminal emulator.

  dtterm-256color   colors#256, setaf/setab use SGR 30-37, 90-97, and
                    38;5;N depending on the requested index.
  dtterm-direct     use=dtterm-256color, RGB boolean, colors#0x1000000,
                    setrgbf/setrgbb emit the SGR 38;2;R;G;B / 48;2;R;G;B
                    direct-RGB sequences.

Both are opt-in -- TERM=dtterm continues to mean the historical 8-colour
behaviour.  Users set TERM=dtterm-256color or TERM=dtterm-direct (the
latter implies the former) so terminfo-aware applications such as vim,
neovim, tmux, htop and less detect the extra capabilities.

Compilation runs through `tic -x` at install time so unknown extension
booleans (e.g. RGB on older ncurses) don't cause failures.  The .ti
source is shipped verbatim alongside dtterm.ti via dist_cfg_DATA so
distributors who don't have tic at install time can still vendor the
file.

The dtterm(5) man page gets new entries for SGR 90-97 / 100-107 (bright
fg / bg), 38;5;N / 48;5;N (256-colour palette) and 38;2;R;G;B /
48;2;R;G;B (direct-RGB), plus a paragraph pointing readers at the new
TERM names.

Files touched:

  cde/programs/dtterm/dtterm-extra.ti      (new)
        - dtterm-256color and dtterm-direct entries (use=dtterm and
          use=dtterm-256color respectively)
  cde/programs/dtterm/Makefile.am
        - dist_cfg_DATA installs the new .ti
        - install-exec-hook runs tic -x on it
  cde/programs/dtterm/dtterm.5
        - SGR 90-97, 100-107, 38;5;N, 48;5;N, 38;2;R;G;B, 48;2;R;G;B
        - reference to the new TERM names
The original dtterm defaults for SGR 30-37 / 40-47 used full-intensity
channels (0xffff) for every colour, which left no headroom for the
SGR 90-97 / 100-107 bright variants -- bright-red rendered the same
pixel as normal red, bright-green the same as normal green, and so on.
The only visible distinctions were bright-black (grey) and bright-blue
(#5c5cff); everything else looked identical to its plain counterpart,
which made vim, htop and other apps that rely on bold-brightens-fg
look washed out compared to the xterm rendering.

Drop the 0/1 InitColor() macro for slots 1-8 and seed each slot with
xterm's 'colour3' family RGB values instead:

  slot 1: black                #000000
  slot 2: red       red3       #cd0000
  slot 3: green     green3     #00cd00
  slot 4: yellow    yellow3    #cdcd00
  slot 5: blue      blue3      #0000ee
  slot 6: magenta   magenta3   #cd00cd
  slot 7: cyan      cyan3      #00cdcd
  slot 8: white     gray90     #e5e5e5

The bright variants in colorPairs[9..16] keep their full-intensity
defaults, so the SGR 1 bold-brightens-fg promotion and the SGR 90-97
sequences now render distinctly brighter than the normal palette --
matching xterm's out-of-box appearance.

Black stays at #000000 (any further dimming would shift it towards a
grey that no terminal user expects).  White drops to #e5e5e5 so the
bright-white (#ffffff) variant is visibly different.

This is a small visual tweak: applications continue to receive the
same Pixel for the same SGR code, and any user who wants the previous
full-intensity look can override via the *color0..*color15 resources
once those land.
Expose the 16-entry ANSI colour palette and the xterm bold-brightens-fg
toggle as standard Xt resources so users can theme dtterm via their
.Xdefaults / app-defaults without rebuilding.

  *color0  .. *color7    SGR 30-37 / 40-47 fg / bg     (dim ANSI)
  *color8  .. *color15   SGR 90-97 / 100-107 fg / bg   (bright ANSI)
  *boldColors            Boolean; True enables bold-brightens-fg

Resource defaults match the dimmed palette this branch landed earlier
(red3, green3, yellow3, blue3, magenta3, cyan3, gray90 for 0-7, and
gray40 / pure primaries / #5c5cff / white for 8-15), so behaviour is
unchanged out of the box.

Implementation notes:

  DtTermPart (cde/include/Dt/TermP.h) gains 16 Pixel fields and one
  Boolean.  The String -> Pixel converter is the standard XmRPixel /
  XtRString combination Xt already runs for *foreground / *background,
  so we don't ship a custom converter -- by the time
  _DtTermColorInit() runs, vt.colorN holds a usable Pixel on the
  widget's colormap.

  _DtTermColorInit() (TermColor.c) replaces the static RGB seeding
  loop with a copy from vt.color0..15 into colorPairs[1..16] and sets
  fgCommon = bgCommon = True for those slots.  That skips the lazy
  XAllocColor in _DtTermColorInitializeColorPair (the resource
  subsystem already owns the Pixels) and lets the existing XQueryColor
  path derive the half-bright derivative from .pixel.

  _DtTermEnhProc (TermEnhance.c) consults tw->vt.boldColors before
  promoting an SGR 30-37 INDEXED fg to its 90-97 bright twin when
  SGR 1 (bold) is set.  Setting *boldColors: False keeps the dim fg
  and renders bold via the bold font / overstrike path only.

Documentation:

  cde/programs/dtterm/Dtterm.ad.src grows a commented block that lists
  the 16 resources with their default colour specs and explains the
  bright-vs-dim semantics.  Names follow the xterm convention so a
  user moving from xterm can lift their existing palette config
  unchanged.

Files touched:

  cde/include/Dt/Term.h           - DtN / DtC string defines (17 new)
  cde/include/Dt/TermP.h          - Pixel color0..15 + Boolean boldColors
  cde/lib/DtTerm/Term/Term.c      - XtResource entries (17 new)
  cde/lib/DtTerm/Term/TermColor.c - resource Pixels into colorPairs
  cde/lib/DtTerm/Term/TermEnhance.c - bold-brighten gated by boldColors
  cde/programs/dtterm/Dtterm.ad.src - commented resource examples
The dtterm widget exports TERM=<termName> into the child shell's
environment (TermPrim.c:2539+) using the *termName resource.  Until
now that resource defaulted to "dtterm", which advertises only 8
colours -- so curses-aware applications such as vim, neovim, tmux,
htop and less ran in 8-colour mode unless the user remembered to
'export TERM=dtterm-256color' themselves.

Flip the default to dtterm-256color now that the corresponding
terminfo entry ships with dtterm and is compiled at 'make install'
time via the existing 'tic -x dtterm-extra.ti' hook.  dtterm-256color
uses use=dtterm, so every capability the old TERM=dtterm provided
remains available -- callers strictly gain colors#256 and the
multi-step setaf / setab encoding that targets SGR 30-37, 90-97 and
38;5;N depending on the requested index.

Users who deliberately want the 8-colour TERM keep that as a
one-line override:

    Dtterm*termName: dtterm

Users who want to advertise 24-bit truecolour to truecolor-aware apps
(vim with ':set termguicolors', etc.) can opt in to the heaviest of
the three terminfo entries:

    Dtterm*termName: dtterm-direct

Only DtTerm's resource default changes; DtTermPrimitive keeps its
base default of "dumb" since standalone DtTermPrimitive users (rare)
should set TERM explicitly.

Falling back when the terminfo database doesn't carry
dtterm-256color is left to the user: if /usr/share/terminfo/d (or
~/.terminfo/d) lacks the entry, curses applications start in dumb
mode and the user either runs 'tic -x cde/programs/dtterm/dtterm-extra.ti'
once or overrides termName back to "dtterm".  Dtterm.ad.src grows a
commented block describing both paths.

Files touched:

  cde/lib/DtTerm/Term/Term.c        - default for DtNtermName
  cde/programs/dtterm/Dtterm.ad.src - documentation comment
Recognise five new SGR families on the wire and stamp them onto the
per-cell enhancement record.  The renderer changes that turn the new
attributes into pixels live in a follow-up; this commit is
behaviour-neutral on screen and only widens / wires storage:

  SGR 21         doubly underlined
  SGR 53 / 55    overline on / off
  SGR 58;5;N     indexed underline colour
  SGR 58;2;R;G;B direct-RGB underline colour
  SGR 59         default underline colour
  SGR 73 / 74    superscript / subscript
  SGR 75         super / subscript off

Existing SGR 24 (underline off) is extended to clear both UNDERLINE
and the new DOUBLE_UNDERLINE bit, matching xterm semantics.

The new state is recorded in the existing video bit-field (widened
from 6 to 10 bits) and in a brand-new enhUlColor slot on the cell.
The slot lives at the previously-reserved enum value 2 in dtEnhID --
NUM_ENHANCEMENT_FIELDS was already 6, so the values[] array always
had room.

Storage changes:
  cde/lib/DtTerm/Term/TermBuffer.h  - new bits + IS_ macros + enhUlColor
  cde/lib/DtTerm/Term/TermBufferP.h - video:6 -> :10; uint32_t ulColor;
  cde/lib/DtTerm/Term/TermData.h    - enhVideoState widens; enhUlColorState
  cde/lib/DtTerm/Term/TermBuffer.c  - SET/GET/DIRTY/blankEnh cover ulColor

Parser / handler changes:
  cde/lib/DtTerm/Term/TermFunction.{c,h}  - _DtTermSetUlColor + new cases
  cde/lib/DtTerm/Term/Term.c              - saveCursor.enhUlColorState init
  cde/lib/DtTerm/Term/TermParse.c         - DECSC/DECRC ulColor + 58 peek

Nothing visible to the user yet -- the renderer still ignores
DOUBLE_UNDERLINE, OVERLINE, SUPERSCRIPT, SUBSCRIPT and ulFg.  Adding
that lives in the next change.
Make the SGR sequences parsed in the previous commit actually visible.
TermEnhInfoRec gains a Pixel ulFg; four new TermENH_* flag bits live
alongside the existing SECURE / UNDERLINE / OVERSTRIKE flags.

TermEnhance.c populates info->ulFg from the per-cell enhUlColor slot
(defaulting to info->fg when the cell has ENH_MODE_DEFAULT) and lights
up the new flag bits from the cell's video bit-field.

The TermFontRenderFunction signature and _DtTermPrimRenderText grow a
Pixel ulFg parameter.  Each renderer (FontRenderFunction,
FontSetRenderFunction, LineDrawRenderFunction) uses ulFg when drawing
underline / double-underline / overline lines and restores the GC
foreground to fg afterwards.

The selection-highlight fast-path in TermPrimRender.c and
TermPrimRenderMb.c (which bypasses the renderFunction and draws its
own underline) gets the same three-line block for under / double /
over using enhInfo.ulFg.

Superscript / subscript shift the XDrawImageString baseline by a
third of the cell height in FontRenderFunction.  The bg rectangle
XDrawImageString paints moves with it, so a freshly-painted cell
stays consistent; cells repainted over a previously-coloured bg may
briefly show the prior bg above or below the shifted glyph until the
next full redraw (acceptable trade-off vs. doing a separate
XFillRectangle + XDrawString pre-fill).

Backward compatibility:

  - ulFg defaults to fg when the cell has no SGR 58, so plain SGR 4
    underline still renders in the foreground colour.
  - DOUBLE_UNDERLINE and OVERLINE flags are zero in the absence of
    SGR 21 / 53, so no extra lines are drawn for legacy content.
  - SUPERSCRIPT / SUBSCRIPT are zero unless SGR 73 / 74 fires, so
    the baseline doesn't shift for legacy content.

Files touched:

  cde/lib/DtTerm/TermPrim/TermPrimBuffer.h
        - TermENH_DOUBLE_UNDERLINE / OVERLINE / SUPER / SUB flags
        - TermIS_* macros
        - Pixel ulFg in TermEnhInfoRec
  cde/lib/DtTerm/TermPrim/TermPrimRenderP.h
        - TermFontRenderFunction grows a Pixel ulFg parameter
  cde/lib/DtTerm/TermPrim/TermPrimRender.{h,c}
        - _DtTermPrimRenderText grows a Pixel ulFg parameter
        - selection-highlight fast-path uses ulFg, draws new decorations
        - default-fill of enhInfo also seeds enhInfo.ulFg
  cde/lib/DtTerm/TermPrim/TermPrimRenderMb.c
        - same updates as Render.c for the multibyte path
  cde/lib/DtTerm/TermPrim/TermPrimRender{Font,FontSet,LineDraw}.c
        - signatures gain Pixel ulFg
        - underline / double-underline / overline drawing uses ulFg
  cde/lib/DtTerm/TermPrim/TermPrimRenderFont.c
        - Y-shift for SUPERSCRIPT / SUBSCRIPT
  cde/lib/DtTerm/Term/TermEnhance.c
        - resolve ulFg from enhUlColor
        - set the new TermENH_* flag bits
dtterm's CSI parser previously only treated ';' (0x3B) as a parameter
separator; the ECMA-48 standard sub-parameter form using ':' (0x3A)
fell through the default error handler and aborted the sequence.
Modern terminals (kitty, wezterm, recent xterm) accept both, and
neovim emits the colon form for SGR 38 / 48 / 58 by default when
nvim_set_hl is used with truecolour highlights.

Add ':' entries to the two CSI parse tables -- left_bracket_table
(initial state after ESC [) and left_bracket_table_no_Q (subsequent
parameter accumulation, after any digit) -- routing them identically
to ';' via _DtTermParsePushNum.  This makes the parameter stream

  ESC [ 3 8 : 5 : 208 m
  ESC [ 3 8 : 2 : 255 : 128 : 0 m
  ESC [ 5 8 : 5 : 196 m
  ESC [ 5 8 : 2 : 0 : 200 : 255 m

land in context->parms[] as three / five-int sequences in the same
shape the existing 38/48/58 peek-ahead loop in _DtTermCharAttributes
already consumes for the ';' form, so the SGR handler doesn't need
any change.

Caveat for strict ECMA-48 sub-parameter semantics:

The standard distinguishes ';' (parameter separator) from ':'
(sub-parameter separator within one parameter).  With this simple
"treat colon like semicolon" approach, dtterm collapses both into a
flat parameter list, so the SGR 38 / 48 / 58 sequences (which are
the overwhelmingly common use of ':') work transparently.  However,
the rarer SGR 4:N form (where N is an underline-style sub-parameter:
2=double, 3=curly, 4=dotted, 5=dashed) would be parsed as two
parameters '4' (UNDERLINE on) and 'N' (whatever SGR N means), not
as "underline with style N".  This matches what xterm did until
fairly recently and is what most apps fall back to when their
output is piped through a `;`-only terminal.

Adding richer underline-style sub-parameter handling would need a
sub-parameter list per parameter in the parser context, which is a
bigger change deferred for now.
Add support for the xterm / DEC cursor-style sequence so apps such as
vim, neovim and tmux can switch the cursor between block, underline
and vertical bar to indicate insert / normal / replace mode.

  Ps = 0, 1, 2   block       (existing DtTERM_CHAR_CURSOR_BOX)
  Ps = 3, 4      underline   (existing DtTERM_CHAR_CURSOR_BAR)
  Ps = 5, 6      vertical bar (new DtTERM_CHAR_CURSOR_VBAR)

The blinking-vs-steady distinction (odd / even Ps) is intentionally
ignored: dtterm keeps its existing cursor-blink resource handling so
users who turn off blink globally don't get it forced back on by an
application sequence.

Parser plumbing:

  cde/lib/DtTerm/Term/TermParseTable.c
        - new intermediate state _DtTermStateLeftBracketSpace
          (CSI ... SP <byte>) with a single 'q' finalizer
        - ' ' (0x20) entries in both left_bracket_table_no_Q and
          left_bracket_table route into the new state so CSI
          parameter accumulation followed by SP is recognised
  cde/lib/DtTerm/Term/TermParseTable.h
        - extern for the new _DtTermSetCursorStyle handler
  cde/lib/DtTerm/Term/TermParse.c
        - _DtTermSetCursorStyle reads parms[1] (default 1) and
          XtVaSetValues the DtNcharCursorStyle resource so the
          widget's regular set_values machinery handles the repaint

Renderer:

  cde/include/Dt/Term.h
  cde/include/Dt/TermPrim.h
        - DtTERM_CHAR_CURSOR_VBAR = 3 alongside the existing
          BOX (0), BAR (1, underline) and INVISIBLE (2)
  cde/lib/DtTerm/TermPrim/TermPrimCursor.c
        - cursorToggle replaces its BOX / not-BOX if-else with a
          switch that draws:
            BOX        full cell      (cellWidth x cellHeight)
            BAR        underline      (cellWidth x 2 at ascent+1)
            VBAR       left-edge bar  (2 x cellHeight)
            INVISIBLE  nothing this toggle
Implement xterm's OSC 10 / 11 / 12 dynamic colour control:

  ESC ] 10 ; spec  ST   set default foreground colour
  ESC ] 11 ; spec  ST   set default background colour
  ESC ] 12 ; spec  ST   set cursor colour
  ESC ] N  ; ?     ST   query, respond \e]N;rgb:RRRR/GGGG/BBBB\e\\

`spec` is any string XParseColor accepts: rgb:RR/GG/BB, #RRGGBB, or a
named colour from rgb.txt.

Also fixes a pre-existing dispatch drop: when an OSC payload was
terminated with ST (ESC \\) instead of BEL (0x07), get_stringBS_table
called _DtTermPrimParserNextState (just reset the state) instead of
_DtTermChangeTextParam (the actual dispatcher).  All BEL-terminated
OSC sequences worked, but ST-terminated ones were silently dropped --
which is what xterm-spawned applications often emit by default.  The
fix is a one-entry change in get_stringBS_table so both terminators
route to the dispatcher.

Implementation:

  cde/lib/DtTerm/Term/TermParseTable.c
        - ST (`\\`) entry in get_stringBS_table now invokes
          _DtTermChangeTextParam instead of _DtTermPrimParserNextState

  cde/lib/DtTerm/Term/TermParse.c
        - new static helper _DtTermOscDefaultColour parses '?' (query)
          or any X colour spec (set) and either responds via
          sendEscSequence or updates the widget
        - cases 10 / 11 / 12 added to the OSC dispatcher switch

For SET:
  - 10 / 11 push through XtVaSetValues XtNforeground / XtNbackground
    so the widget's set_values machinery triggers a proper repaint
  - colorPairs[0] is also updated so the cached default fg/bg matches
  - 12 pokes tw->term.tpd->cursorGC directly via XSetForeground; the
    cursor picks up the new colour on its next blink toggle

For QUERY:
  - reads the current Pixel, runs XQueryColor for the RGB, and emits
    rgb:RRRR/GGGG/BBBB with ST terminator -- the standard xterm
    response format consumed by curses, vim, neovim and tmux
Implement xterm's OSC 4, the per-index palette control:

  ESC ] 4 ; N ; ?     ST   query palette entry N
  ESC ] 4 ; N ; spec  ST   set palette entry N
  ESC ] 4 ; N1 ; spec1 ; N2 ; spec2 ; ... ST   set multiple

`spec` is any string XParseColor accepts.  N maps to dtterm's storage:

  0..7    -> colorPairs[1..8]   (dim ANSI palette)
  8..15   -> colorPairs[9..16]  (bright ANSI palette)
  16..255 -> pal256[N]          (lazy 256-colour cache)

For SET on a previously-owned slot, the old pixel is XFreeColors'd
before the new one is allocated so the colormap usage stays steady on
PseudoColor visuals; on TrueColor this is a no-op.  After applying
the change the widget area is XClearArea'd with exposures = True so
the existing on-screen cells re-render with the new colour.

For QUERY, the response is the canonical xterm reply

  ESC ] 4 ; N ; rgb:RRRR/GGGG/BBBB ESC \\

with ST terminator.  Querying an entry that hasn't been lazily
allocated for the 256-palette cache forces allocation now, so a
follow-up render is consistent with what the application thinks the
palette holds.

Multi-pair payloads (the most common form in actual programs) work
because the helper walks the ';'-separated stringParms buffer with
strtok_r, handling each `N;spec` pair independently.

Files touched:

  cde/lib/DtTerm/Term/TermParse.c
        - new static helper _DtTermOscPalette
        - case 4 added to _DtTermChangeTextParam
        - <string.h> include for strtok_r if not already pulled in
Implement xterm's OSC 52 clipboard-set sequence:

  ESC ] 52 ; sel ; base64-data ST

When the application emits OSC 52 with base64-encoded text, dtterm
decodes the payload and pushes it onto the Motif CLIPBOARD via the
same XmClipboardStartCopy / XmClipboardCopy / XmClipboardEndCopy path
the existing "Copy" menu uses.

The behaviour is gated behind a new boolean resource

    Dtterm*allowClipboardOps: False (default)

so a malicious or compromised program can't silently replace the
user's clipboard.  This matches xterm's `allowClipboardOps` default
and is what the man page (CVE-2014-9650 era guidance) recommends.

The clipboard *query* direction (`OSC 52 ; sel ; ? ST`) is
intentionally not implemented.  Letting an arbitrary program read
back the user's clipboard contents is a confused-deputy hazard
(passwords, secrets pasted from password managers, etc.), and the
practical benefit is small -- callers that actually need round-trip
clipboard access typically use platform helpers like xclip / pbpaste
through their own piped IPC.

PRIMARY selection ('p') is mapped onto CLIPBOARD too because
dtterm's existing PRIMARY ownership is wired to the cell-level
selection machinery in TermPrimSelect.c, and re-owning PRIMARY for a
free-form OSC 52 payload would clobber that.  Most applications that
care use 'c' (or no selection char at all, which defaults).

Files touched:

  cde/include/Dt/Term.h   - DtN/DtC defines for allowClipboardOps
  cde/include/Dt/TermP.h  - Boolean allowClipboardOps on DtTermPart
  cde/lib/DtTerm/Term/Term.c
        - resources[] entry, default False
  cde/lib/DtTerm/Term/TermParse.c
        - _DtTermB64Decode: small in-place base64 decoder
        - _DtTermOscClipboard: gated handler that decodes and
          XmClipboardCopy's the result
        - case 52 in _DtTermChangeTextParam
Recognise the OSC 8 hyperlink sequence used by modern ls / git / gh /
fastfetch / browsers:

  ESC ] 8 ; params ; URL ST    start a hyperlink for subsequent cells
  ESC ] 8 ; ; ST               end the active hyperlink

The current URL is tracked on DtTermData (td->linkUrl, single-slot:
nested links are not modelled).  While a URL is active, a new
LINK_ACTIVE bit is OR'd into the per-cell video bit-field, and the
renderer treats LINK_ACTIVE the same way it treats TermENH_UNDERLINE
so hyperlinked text is visibly underlined even when the application
doesn't emit SGR 4 itself.

Click-to-open is intentionally not implemented in this commit -- the
URL is just tracked.  A follow-up change can hook a Button1 event
handler that looks up the URL of the clicked cell and invokes
xdg-open or the user's preferred protocol launcher.

The spec-reserved `params` field of OSC 8 (typically `id=...`) is
parsed but currently dropped; nothing dtterm renders depends on the
id grouping at this stage.

Storage / plumbing:

  cde/lib/DtTerm/Term/TermBuffer.h
        - LINK_ACTIVE (1 << 10) and IS_LINK_ACTIVE macro
        - VIDEO_MASK extended to include the new bit
  cde/lib/DtTerm/Term/TermBufferP.h
        - DtTermEnhPart.video widens from :10 to :11
  cde/lib/DtTerm/Term/TermData.h
        - char *linkUrl on DtTermDataRec
  cde/lib/DtTerm/Term/TermParse.c
        - new _DtTermOscHyperlink helper splits "params;URL", manages
          td->linkUrl, and OR/AND-s LINK_ACTIVE into td->enhVideoState
        - case 8 added to _DtTermChangeTextParam
  cde/lib/DtTerm/Term/TermEnhance.c
        - _DtTermEnhProc OR-s TermENH_UNDERLINE when LINK_ACTIVE is set
          on the cell, regardless of explicit SGR 4
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