Three widgets under lib/src/terminal/src/ui/ and one TerminalView
parameter were leftovers from the era when TerminalView wrapped its
viewport in a real Scrollable. The Scrollable path was replaced with
PointerScrollEvent → PgUp/PgDown translation (alive, well-tested in
terminal_view_test.dart); these helpers stayed behind with tests but
zero production callers.
Drops:
- TerminalScrollGestureHandler (scroll_handler.dart, 100 LOC)
- InfiniteScrollView (infinite_scroll_view.dart, 117 LOC)
- KeyboardVisibilty (keyboard_visibility.dart, 59 LOC) — last
production caller was removed in 048e835
- TerminalView.simulateScroll parameter — declared, never read
- The matching test groups + imports in ui_widget_test.dart
- The KeyboardVisibilty export from the terminal barrel
Net: -585 lines from lib/ + test/, no behavior change, and
infinite_scroll_view.dart stops being the 90%-coverage outlier we
were apologising for in the previous test sweep.
Same shape as T-93 (dead onTapUp wiring), T-95 (dead tertiary tap),
and 048e835 (dead scrollController plumbing). Public-or-tested
surface that no caller exercised.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
TerminalView's build tree doesn't wrap content in a Scrollable —
scroll is handled by translating PointerScrollEvent into PgUp/PgDown
keyInput. The ScrollController parameter, _scrollableKey, internal
_scrollController, _scrollToBottom helper, and its five call sites
were all dead: _scrollableKey.currentState was always null because no
Scrollable in the tree carried the key, so _scrollToBottom's jumpTo
never fired.
Drops:
- public scrollController parameter on TerminalView
- _scrollableKey + _scrollController fields
- the didUpdateWidget swap block and dispose call
- _scrollToBottom + the five call sites
- KeyboardVisibilty wrapper (its only callback was _scrollToBottom,
now a no-op; the widget remains a reusable primitive under ui/ for
future use)
- the matching tests in terminal_view_test.dart
Same shape as T-93 (dead onTapUp wiring) and T-95 (dead tertiary tap
surface) — public API that no caller used + internal state that no
path executed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
test / unit + widget + golden + a11y (push) Failing after 30s
test / integration_test (xvfb) (push) Has been skipped
test / bundle smoke (xvfb 5s) (push) Has been skipped
test / daemon subprocess + web WASM smoke (push) Has been skipped
test / dart doc (lib API) (push) Failing after 1m1s
The middle-click ("tertiary tap") path in TerminalGestureHandler was
wired wrong: build() bound onTertiaryTapDown to the secondary state
method, so a middle-click fired as if it were a right-click. The
state's onTertiaryTapDown/Up methods were unreachable, and the
onTertiaryTapUp body had a copy-paste bug (button=right instead of
middle). No production caller passed onTertiaryTapDown / onTertiaryTapUp
through, and TerminalView didn't expose them either, so the public
parameters were dead too.
Drops both layers of dead surface — option B of T-95. Same shape as
T-93's resolution (delete unused, restore later when a real consumer
needs it). Also collapses the unreachable onDragStart selectWord
branch (PanGestureRecognizer is mouse-only, so the touch path can't
fire) into a single selectCharacters call with a comment.
Companion: refines the reflow-padding test in coverage_trivials_test
to use narrow→wide reflow setup (more honest about intent, also
actually exercises the padding branch — Buffer.resize now 100%) and
clears two unnecessary_import warnings surfaced by the deletion.
Coverage: gesture_handler 55/59 -> 59/59; gesture_detector 50/50;
buffer/buffer 260/261 -> 261/261.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
test / unit + widget + golden + a11y (push) Failing after 28s
test / integration_test (xvfb) (push) Has been skipped
test / bundle smoke (xvfb 5s) (push) Has been skipped
test / daemon subprocess + web WASM smoke (push) Has been skipped
test / dart doc (lib API) (push) Failing after 59s
`TerminalView.onTapUp` was documented as "Callback for when the
user taps on the terminal" but was wired to a code path nothing
ever invoked:
- `TerminalView.build` passed it via `onTapUp:` to
`TerminalGestureHandler`,
- which forwarded via `onTapUp:` to `TerminalGestureDetector`,
- whose `onTapUp` field was declared and accepted but never called
by `_handleTapUp` (which only fires `onSingleTapUp`).
Net: every caller that registered an `onTapUp` callback on
`TerminalView` got silent failure. zero in-tree callers depended
on it (clide_pty_view.dart is the only TerminalView callsite and
doesn't pass any tap callback), but the public API said one thing
and did another.
Fix: wire `_onTapUp` (the cell-resolving state-method) through the
detector's working `onSingleTapUp` slot. The user-facing semantics
("fires on confirmed single tap with the resolved cell offset")
match the only sane interpretation of the docstring, and don't
overlap with the existing `onSecondaryTap*` (which were already
correctly wired through TapGestureRecognizer's secondary callbacks).
Also drops the dead surface that surfaced the bug:
- `TerminalGestureHandler.onTapUp` parameter + field — no caller
passes it after the fix; was only used to forward into the dead
detector field.
- `TerminalGestureDetector.onTapUp` parameter + field — never
invoked by `_handleTapUp`. Pure dead code.
Tests: extends `terminal_view_test.dart` with a primary-tap
regression case + paired tests for selection-clearing and
secondary-tap callback routing. The double-tap recognizer's
300 ms timer is flushed via `pump(const Duration(seconds: 1))`
(pumpAndSettle waits for animations, not arbitrary timers).
Coverage delta:
- terminal_view.dart: 151/188 → 180/188 (95.7%; remaining gaps
are IME `_onComposing`/`_onEditableRect`/`_onKeyboardShow`
body branches that need deeper IME mocking).
- gesture_handler.dart: 18/60 → 39/59.
- gesture_detector.dart: 30/50 → 42/50.
- Total project: 65.76% → 66.97%; coverage_floor 65 → 66.
Co-Authored-By: Claude <noreply@anthropic.com>
Adds test/terminal/terminal_test.dart — 54 unit tests covering the
`Terminal` class as a pure-Dart orchestrator: construction +
TerminalState defaults, the Observable mixin, write/writeChar,
keyInput / charInput / textInput / paste (with bracketed-paste +
ctrl/alt encodings, including macOS reservation), mouseInput
gating, resize (clamping + onResize callback + alt-buffer
scrollback clear), buffer switching (use{Alt,Main}Buffer +
clearAltBuffer), every SBC handler (bell / backspace / lineFeed /
CR / SO / SI / unknown), tab-stop manipulation (tab jump +
saturation, clearTabStopUnderCursor, clearAllTabStops, setTapStop),
every ANSI escape handler (save/restore cursor, index, nextLine,
reverseIndex, designateCharset), CSI cursor + erase + line/char
insert/delete + scroll + repeatPreviousCharacter (incl. no-op when
no preceding char), device-attribute and status reports, every
mode setter mirroring into its getter, every SGR set/unset attr +
colour setter, OSC handlers (setTitle / setIconName / unknownOSC),
and all the documented no-op fallbacks (unknownSBC, unkownEscape,
unknownCSI, setUnknownMode, setUnknownDecMode, setColumnMode,
unsupportedStyle).
Also fixes a real production bug surfaced while writing tests:
`BufferLine.eraseRange(0, 0, ...)` panicked with a `RangeError`
because the right-side wide-char guard read `_data[-1]` via
`getWidth(end - 1)` when `end == 0`. The left guard already had a
`start > 0` check; the right guard was missing the symmetric
`end > 0`. Real trigger path: `Terminal.eraseDisplayAbove`
(`ESC[1J`) with the cursor at column 0 — common after `ESC[H\x1b[1J`
home-then-erase-above sequences that many TUIs emit on redraw.
Regression test added in line_test.dart.
Coverage delta:
- terminal.dart: 0/283 → 291/291 (file grew by 8 LF for the
fix's comment lines).
- base/observable.dart: 0/7 → 7/7 (covered transitively via
Terminal's listener tests).
- Total project: 56.40% → 59.82%; coverage_floor bumped 56 → 59.
Co-Authored-By: Claude <noreply@anthropic.com>
test / unit + widget + golden + a11y (push) Failing after 29s
test / integration_test (xvfb) (push) Has been skipped
test / bundle smoke (xvfb 5s) (push) Has been skipped
test / daemon subprocess + web WASM smoke (push) Has been skipped
test / dart doc (lib API) (push) Failing after 1m0s
`_LineReflow._addPart`'s post-loop block reparents anchors past the
source line's trimmed content onto whatever `_builder._result` was
active at that moment. When no further content lands in the builder
(non-wrapped lines, or the last logical line of a wrapped run),
`finish()` was emitting only when `_builder.isNotEmpty` — leaving
the empty result line with the reparented anchor unappended. The
anchor then pointed to a `BufferLine` that the reflow output never
included, `lines.replaceWith(reflowResult)` discarded it, and
`CellAnchor.attached` returned false. The selection controller's
`extent.attached` null-check then dropped the selection silently
on resize.
The fix adds a `_LineBuilder.hasAnchors` getter and uses it in
`finish()` so the builder line is also emitted when it's carrying
an anchor — even when otherwise empty. Trade-off: an extra trailing
line in the reflow output when (and only when) a tail anchor would
have dangled. `Buffer.resize` already pads the result to `newHeight`
afterward, so for the common case (resize fits inside view height)
the total ring length is unchanged; only when the result already
meets / exceeds `newHeight` does the buffer grow by one. Acceptable
in exchange for selections surviving a width change.
User-visible trigger paths:
- `SelectAllTextIntent` (Ctrl+A) creates an end anchor at
`x = viewWidth` on the last buffer line — exactly the past-
trimmed-length position. Resizing narrower while the selection
was active dropped it.
- Mouse drag selections past the end of a partially-filled line
hit the same shape.
Tests:
- The pre-existing `reflow anchors on the source line tail (past
trimmedLength) get reparented` test was originally written to
document the buggy behaviour ("anchor moves off the source onto
a dangling builder line"). Updated to assert the post-fix
contract: `out.contains(tail.line)` is true.
- New `SelectAllTextIntent-shaped end anchor survives shrink`
regression test that mirrors the actual production trigger
(anchor at `x = viewWidth` on a partially-filled line, narrower
reflow).
reflow.dart 71/71 → 72/72 (the new getter is a one-liner). Project
coverage 54.62% unchanged within rounding.
Co-Authored-By: Claude <noreply@anthropic.com>
Adds test/terminal/core/core_test.dart — 36 unit tests across the
small standalone files that sit directly under
`lib/src/terminal/src/core/*.dart`:
- CellData (constructor + empty + getHash + toString),
- CursorStyle (default ctor, every set/unset attr getter pair, all
three colour-mode setters per channel, reset, the .empty
singleton) + CursorPosition,
- Charset (translate, designate/use, save/restore, asciiTranslator,
decSpecGraphicsTranslator with in-table, out-of-table, and high-
codepoint paths),
- TabStops (default 8-column grid, find with empty-range / out-of-
bounds / no-stop-in-range cases, setAt/clearAt/clearAll/reset),
- reflow (empty input, single-line passthrough, grow, shrink-with-
split, wrapped-run continuation, wide-char boundary on the new
width, inner wide-char clamp during _addPart, anchor reparent on
the main path, anchor reparent past trimmedLength).
Two source-side cleanups folded in:
- `CursorStyle.isItalis` was a defined-but-never-called getter
with a typo. No external callers reference it; renamed to
`isItalic` in the same change as the test that exercises it.
- `_LineBuilder.isEmpty` in reflow.dart was dead — the only callers
use `isNotEmpty` or check `_lines.isNotEmpty` separately.
Removed.
Coverage delta:
- cell.dart: 3/7 → 7/7.
- charset.dart: 12/25 → 25/25.
- cursor.dart: 2/62 → 62/62.
- tabs.dart: 0/23 → 23/23.
- reflow.dart: 24/72 → 71/71 (file shrank by one line after the
isEmpty getter removal).
- Total project: 52.72% → 54.62%; coverage_floor bumped 52 → 54.
Note for follow-up (not blocking): the post-loop "anchor.x >= to"
branch in reflow's `_addPart` reparents anchors past trimmedLength
onto whatever builder line is active at that moment. If no
subsequent content is added (no wrapped continuations after the
last shrink iteration), that builder line is never emitted by
`finish()` and the anchor lands on a dangling reference. The path
is exercised by the new test, but the contract it implements is
arguably broken — anchors that should follow the source content
end up off the visible buffer. Worth a separate ticket if real
terminals trip it.
Co-Authored-By: Claude <noreply@anthropic.com>
test / unit + widget + golden + a11y (push) Failing after 35s
test / integration_test (xvfb) (push) Has been skipped
test / bundle smoke (xvfb 5s) (push) Has been skipped
test / daemon subprocess + web WASM smoke (push) Has been skipped
test / dart doc (lib API) (push) Failing after 1m0s
Clears the 19 misc lint hits left after the test_app print sweep
+ libc.dart suppression. By rule:
- `withOpacity(α)` → `withValues(alpha: α)` (deprecated_member_use)
in `painter.dart:187` and `terminal_view.dart:318`.
- `Pointer.elementAt(n)` → `Pointer + n` (deprecated_member_use)
in `native_pty.dart:306` and `session.dart:187`.
- Brace single-statement for/if bodies in `native_pty.dart`
(×3) and `decisions_view.dart` (curly_braces_in_flow_control_
structures).
- `IsolateClient` and `InProcessClient` constructors switched to
`super.log` / `super.events` parameters (use_super_parameters);
associated unused imports of `kernel/src/log.dart` and
`kernel/src/events/bus.dart` removed in the same files.
- `InProcessClient._dispatcher` field + getter/setter pair folded
into a single mutable public `dispatcher` field
(unnecessary_getters_setters).
- `_buildDispatcher` local in `lib/main.dart` renamed to
`buildDispatcher` (no_leading_underscores_for_local_identifiers).
- `_onTapDown(_)` in `terminal_view.dart` typed as
`TapDownDetails _` (strict_top_level_inference).
- `operator []=(...)` in `circular_buffer.dart` given an explicit
`void` return type (strict_top_level_inference).
- `CustomKeyboardListener` and `TerminalGestureDetector` callsites
reordered so `child:` lands last (sort_child_properties_last).
- `CustomTextEdit` constructor declared `const`
(prefer_const_constructors_in_immutables).
- `LinkedHashMap<K, V>()` in `paragraph_cache.dart` collapsed to a
`<K, V>{}` literal (prefer_collection_literals); the now-unused
`dart:collection` import dropped.
Project analyze: 19 → 0 issues. `make test` stays green; coverage
unchanged at 52.72%.
Co-Authored-By: Claude <noreply@anthropic.com>
The file's only purpose is to expose the default-keytab string
constant, but it carried a `void main()` at the end that parsed
that constant and printed the result. That entry point:
- doesn't belong in `lib/` (Dart entry points live in `bin/` or
`tool/`),
- pulls in `keytab_parse` and `keytab_token` imports that are
unused everywhere else in the file,
- emits one of the pre-existing `avoid_print` analyze infos,
- only ever ran when a contributor manually invoked
`dart lib/src/terminal/src/core/input/keytab/keytab_default.dart`,
which the build never does.
Removing it unblocks the file from the coverage report (no
executable lines remain, just the string constant), drops the
unused imports, and shaves an analyze info off the pre-existing
total. If the dump-to-stdout helper turns out to be useful again,
the right home is a `tool/dump_keytab.dart` outside the package's
runtime surface.
Co-Authored-By: Claude <noreply@anthropic.com>
Four `throw` sites in `core/input/keytab/` were unreachable through
the public API:
- `keytab_token.dart` `_parseKeyboardNameDefine` and `_parseKeyDefine`
each tested `reader.readString() == 'keyboard'` / `'key'` after
the caller in the same file (`tokenize`) had already gated entry
on `_isKeyboardNameDefine` / `_isKeyDefine`. Both checks
redundantly re-derived a fact already established a function
call earlier; the `else { throw }` was dead code.
- `keytab_parse.dart` `_parseName` and `_parseKeyDefine` checked
the first token's type, but `addTokens` only delegates to those
functions after `peek().type` matches the expected kind. Same
pattern: the throw protects an invariant the caller already
enforces.
Surfaced while bringing `core/input/` to ~100% coverage. Per the
"near-perfect discipline" / "no carve-outs" rules, dead defensive
code is cleaned, not skipped — the surrounding callers in the same
file are tight enough that introducing a real callsite gap would
be a localised and obvious bug, not a silent failure rescued by
these guards.
The two `else`-throw sites in keytab_token.dart fold into a single
unconditional `reader.readString()` (consume the leading word) +
`yield` of the matching token type. The two type-check throws in
keytab_parse.dart fold into an unconditional `reader.take()` to
skip the already-validated token.
All public-API ParseError paths exercised by `core/input/`'s
unit tests still throw correctly — they're guarded by the second
check in each function (the action-token type check after
modeStatus loops, and the input-token check in _parseName).
After cleanup:
- keytab_token.dart: 80 / 80
- keytab_parse.dart: 63 / 63
Co-Authored-By: Claude <noreply@anthropic.com>
`_csiHandleSgr` carried a `// ignore: dead_code` directive with the
note "workaround for a bug in the analyzer". Re-running the
analyzer with the suppression removed produces no warning — Dart's
flow analysis has caught up since the comment was added.
Per the no-lint-suppression rule the suppression needed to be
either removed or given a more substantive justification; the
analyzer's silence makes the call easy.
Co-Authored-By: Claude <noreply@anthropic.com>
`removeCells`, `insertCells`, and `dispose` each iterate over
`_anchors` while invoking `anchor.dispose()` on entries inside the
loop — but `dispose()` removes the anchor from the same list, which
shifts later indexes left and causes the for-loop to skip them.
Symptoms (no user-facing report yet, but real correctness bug):
- After `removeCells` with multiple anchors past the start, anchors
that should be repositioned were silently left at their old `x`.
- After `insertCells` with anchors getting pushed past `_length`,
ones meant to be disposed could survive.
- `BufferLine.dispose` would throw `ConcurrentModificationError` as
soon as more than one anchor was attached.
Fix: iterate `_anchors.toList()` (a snapshot) in all three sites.
Cheap, safe, and matches the expected anchor-management semantics.
Surfaced by the unit tests added under T-91; that commit covers the
fix with regression tests.
Co-Authored-By: Claude <noreply@anthropic.com>
Bold attributes from terminal escapes now render in a real bold
weight instead of being silently flattened.
- pubspec.yaml: register JetBrainsMono Bold + BoldItalic at
weight 700 under family JetBrainsMono. Files already shipped on
disk; only the registration was missing.
- assets/licenses.yaml: bump JetBrainsMono weights_bundled to
[Regular, Italic, Bold, BoldItalic] per D-42 (the entry must
match what is actually wired into the family).
- lib/src/terminal/src/ui/painter.dart: revert the `bold: false`
override and drop the workaround comment. Bold now flows from
CellFlags.bold to TextStyle.fontWeight.
- test/terminal/painter_bold_metrics_test.dart: load Regular and
Bold via FontLoader and assert paragraph maxIntrinsicWidth is
identical (cell-grid drift = 0). JetBrainsMono Bold's monospace
by spec; this test is the canary for the day someone swaps the
font.
- test/goldens/goldens/{ci,linux}/clide_button.png: regenerate.
ClideButton's label renders slightly heavier on the bold variant
(expected — 0.28% pixel diff before regen).
Earlier perception of over-bolding in the Claude pane was
synthetic-bold smearing (Flutter overpaints when no Bold.ttf is
registered for the family), not legitimate bold rendering. Visual
A/B confirms a real Bold face renders crisp emphasis without the
smear, so no per-pane renderer config is needed.
Co-Authored-By: Claude <noreply@anthropic.com>
`mouse/button.dart` and `mouse/button_state.dart` were imported but
nothing in terminal_view referenced their symbols — analyzer
warnings, not infos. Removed.
Probable origin: a half-landed mouse-forwarding refactor (the actual
work is now scoped under T-74); the imports can come back when the
real wiring lands. Removing them in the meantime keeps the analyze
gate clean.
Co-Authored-By: Claude <noreply@anthropic.com>
Mechanical `dart format` sweep across files that drifted from the
formatter's output (mostly trailing-comma and line-wrap differences
from a Dart SDK / formatter version bump). No semantic changes.
Caught because the pre-push gate now actually fires.
Co-Authored-By: Claude <noreply@anthropic.com>
Flutter falls back to synthetic bold when JetBrainsMono-Bold isn't
registered, and synthetic bold drifts glyph advance widths enough
to break the monospace cell grid (cursor block lands between
characters, prompts wrap mid-word). Color is enough to convey
emphasis in TUIs; semantic italic and underline still render.
Also drop the temporary `tmux -L clide kill-server` from the
install target — the rapid-iteration loop is no longer needed.
Co-Authored-By: Claude <noreply@anthropic.com>
Spawn `claude` directly as the tmux command with
CLAUDE_CODE_NO_FLICKER=1 so Claude Code runs in its fullscreen TUI
mode (input box pinned at bottom, owns its own scrollback). Mouse
wheel events are converted to PgUp/PgDown key input — universal
scroll signal that Claude, less, vim normal mode all respect, and
sidesteps the mouse-mode-but-no-scroll dead end where TUI apps
capture mouse without binding the wheel.
Drops the 1000-row tmux canvas + SingleChildScrollView experiment
in favor of viewport-sized tmux and Claude's native bottom-pinning.
Makefile install target now kills the clide tmux server so the
new config takes effect immediately. Marked TEMP — to be removed
once we no longer need the rapid-iteration loop.
Co-Authored-By: Claude <noreply@anthropic.com>
licenses.yaml: xterm entry changed from dart-package to
inlined-source with derivative-work description. JetBrains Mono
weights updated (Bold/BoldItalic dropped). Terminal LICENSE
clarifies this is a derivative work based on xterm.dart v4.0.0.
Co-Authored-By: Claude <noreply@anthropic.com>
Replace the xterm pub.dev package with owned code under
lib/src/terminal/. Based on xterm.dart v4.0.0 by xuty (MIT).
Quiver LRU replaced with hand-rolled LinkedHashMap cache.
Scrollable removed from TerminalView — scroll events are forwarded
via Listener.onPointerSignal instead. zmodem, debugger, and
suggestion modules stripped as unused.
Also: bundle clide.tmux.conf (no status bar, 50k scrollback,
mouse on, zero escape delay, isolated -L clide socket), bump PTY
read buffer to 64KB, add 2px terminal padding, drop bold
JetBrains Mono registration.
Co-Authored-By: Claude <noreply@anthropic.com>