Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 73 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,73 @@ fallbacks) for inline images/maps.
- **The demo scene loads with `_watching` off.** It pours a spawn window's whole history in before the
first frame, and every line would otherwise count as missed — so any frame that later made such a
window visible carried an activity bar reporting the client's own setup as news.
- **A pane's output can be selected with the mouse and copied with `⌃C`, and almost all of it is the
framework's** (`MarkupControl.EnableSelection`, shipped in the pinned 2.5.14 and **off by default**).
Drag, double-click word, triple-click line, drag-autoscroll through the `ScrollablePanelControl` each
pane already sits in, one-selection-per-window arbitration and a wrap-aware copy all come from the
package. This is the rare case where the answer was to *switch something on* rather than build it; what
had to be ours is the colour, the clipboard, and what happens to a selection when the buffer moves.
- **The terminal's own selection cannot do this job, which is `UrlDetector`'s argument one layer over.**
Under `?1003` — which this client needs for the wheel, tab and rail clicks and pane drag-and-drop — a
plain drag belongs to the application, and the emulator's escape hatch (kitty's ⇧-drag) selects a
terminal **row**: on a vertical split that row crosses the divider into another pane's output, and
since a pane is narrower than the row a logical line wraps and comes back with newlines injected at
the wrap points. The framework's copy walks the painted cells and breaks a line only where a row is
not a soft-wrap continuation. There is no partial retreat from `?1003` that keeps the wheel.
- **The selection band is one pair per theme, and for a different reason than `ReadingPlane`'s.** There
it is cost; here it is meaning. A pane's plane already says whose connection this is (hue) and where
the keyboard is (luminance), and a selection is neither — it is the client answering a gesture being
made *now*, in exactly one pane at a time, so a highlight that changed colour with whose pane it
landed in would report a fact nobody asked about. `WorkspacePalette.SelectionBand` is `ReadingPlane`
pushed *further* in the direction of travel — brighter on a dark theme, darker on a light one — then
leaned toward `Theme.Prompt`, the same anchor `ArmedBand` uses. It is held to a **fill** floor against
all fourteen planes (1.5:1; tightest measured 2.70:1) and `SelectionInk` to `Contrast.Floor` on the
band, which matters more here than anywhere: the highlight replaces the world's *foreground* too, so
that one ink is what all selected output is read in.
- **The clipboard writer is caller-supplied and null by default** — the `save:`/`logRoot:`/`openUrl:`
family, and here it also buys a *single* copy path. The framework's own ⌃C writes straight to the
system clipboard through a static helper no caller can substitute, so a test run would replace
whatever the developer had copied and the one path that could not be injected would be the one under
test. `CopyEnabled = false` on the pane controls and the chord is answered by
`SharpMUTermApp.CopyFocusedSelection` instead; `Program` supplies `ClipboardHelper.SetText`, which
covers OSC 52 *and* the platform tool, so a copy lands locally and over ssh alike.
- **⌃C is claimed in the main window's key chain, not in `MacroKeys.AppShortcuts`.** A global shortcut
runs ahead of *every* window including the composer, whose `MultilineEditControl` has its own ⌃C and
is a real editor. Being in the chain also puts it after `DispatchMacro`, so a macro bound to ⌃C wins —
the same relationship ⌃←/→ has with pane selection, and the reason `Verdict` needs no special case.
- **A selection is dropped whenever the rows under it move, and the clear belongs in `FeedRange`** — the
one function that actually replaces a pane control's content. `RepaintPane` is *not* the only caller:
`BuildFrozenContent` feeds both halves of a frozen pane and `ToggleFreeze`'s thaw branch pours the whole
buffer back, so clearing at the repaint site alone left freeze and thaw re-feeding under a live
selection. The thaw is the case with teeth — freezing leaves the live control empty, so a stale anchor
merely yields nothing, while after a thaw the rows exist again and ⌃C hands over real text nobody
dragged across (`FreezingAndThawingDropsTheSelectionRatherThanReAnchoringIt` copied `The Grand Plaza…`
before this moved). **`MarkupControl.SetContent` does not clear a selection** — only its append path
does (`OnContentAppended`) — so this cannot be left to the framework.
- **The copy asks the window's `SelectionManager`, never the focused pane.** That manager owns the one
active selection and clears the previous owner when a new one starts. The focused pane is the wrong
question: pane selection moves on ⌃arrows, ⌃O and a tab click, and a press in a pane's *body* moves
none of them — so a drag in the pane beside the focused one left the copy looking elsewhere and
reporting `nothing selected`, which reads as a feature that does not work. It also retires the special
case for a frozen pane: one selection, one owner, whichever control that is.
- **A pane id and a window id are different namespaces that are both strings**, and this is where that
bites: `PaneOutputRects` is keyed by *pane*, while `SimulatePaneDrag` and `PaneSelection` take a
*window*. `PaneWindows()` maps between them, and exists because a test that passed a pane id to the
drag seam selected nothing — indistinguishable from the feature being broken.
- **`NewPaneControl` is the one place a pane's control is made**, and it exists because enabling
selection on `PaneContentFor` alone left the **main** window — the pane most people are looking at —
unable to select anything. That control is built in the constructor, before a workspace exists; every
other one is built on demand. Two creation paths, and the trap announced itself immediately.
- **`SimulatePaneDrag` is the test seam**, for `SimulatePaneClick`'s reason: the framework registers its
driver-mouse handler inside `Run()`, which no test calls. The drag flag rides *with* the button flag,
because SGR encodes motion-while-held as `Button1Pressed | Button1Dragged` and a seam sending the bare
form would exercise a path the terminal never produces. The `selection` view is the frame, and it is
in `FrameContrastTests`' list — a colour nothing renders is a colour nobody checks.
- **Not fixed, and known**: chrome rows live in the same buffer and are selectable (a terminal selection
would take them too); OSC 52 caps at ~74 KB and `Osc52.BuildSequence` returns null past it, so a very
large copy lands locally and **silently** does not travel over ssh; GNU screen has OSC 52 disabled
upstream; tmux needs `allow-passthrough on`. The Windows mouse path is a separate ad-hoc parser in
`NetConsoleDriver` and nothing here can verify it — treat Windows drag-select as unproven.
- **Coming back to a window you were not watching leaves a bar where you left off, and that covers two
different absences.** The *window* one is `NEW` and is the common case: a line lands while the window
is not `Workspace.IsCaughtUp` — visible **and** at its live tail — and `_missedFrom` records the index
Expand Down Expand Up @@ -539,7 +606,12 @@ python3 tools/ansi_frame_to_image.py frame.ansi frame.html # or .svg
a viewport row), `activity-bar` (the *other* absence — a window the reader was not watching: three
lines land in the main window while Chat is in front of it, and picking main back lands on the `NEW`
bar with those three under it. Separate from `away` because the two are separate facts with separate
wording, and this is the one that happens many times an hour), `prefix-panel` (the ⌃B which-key
wording, and this is the one that happens many times an hour),
`selection` (a real ⌃-drag across the main window's output, through `SimulatePaneDrag` and the control's
own hit test rather than a highlight posed by hand — the only frame carrying `WorkspacePalette.SelectionBand`,
which is why it is in `FrameContrastTests`' list: the band is the one plane this client invents rather
than derives from a pane, and its ink replaces the world's own on every selected cell),
`prefix-panel` (the ⌃B which-key
panel — the state `prefix` becomes a few hundred milliseconds later, if no key has arrived),
`focus`/`focus-moved` (a split *and* a second command line — the one geometry showing a focused pane
beside an unfocused one and an armed bar above an idle one, before and after a real ⌃→),
Expand Down
3 changes: 2 additions & 1 deletion docs/design/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -425,7 +425,8 @@ count on the tab, the rail character, and the rail world.

`⌃P` command surface · `⌃F` search the output (`⌥G` next hit) · `⌥F` freeze/resume in focused pane ·
`⌃R` command-history search ·
`⌃N` next tab in the focused pane · `⌥D`/`⌥R` disconnect/reconnect · `⌥↑`/`⌥↓` command history (`↑`/`↓` do it too,
`⌃N` next tab in the focused pane · `⌃C` copy the pane selection (drag to select) ·
`⌥D`/`⌥R` disconnect/reconnect · `⌥↑`/`⌥↓` command history (`↑`/`↓` do it too,
where the caret has nowhere further to go) · `⌥⏎`, or `⌃L`, newline in input · `F1` composer ·
`F2`–`F9` config · `Esc` close overlay.

Expand Down
7 changes: 7 additions & 0 deletions src/SharpMUTerm.Core/Commands/CommandCatalog.cs
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,13 @@ public static IReadOnlyList<CommandItem> Build(
"term:restore-purge",
"deletes every pane's saved content"));

// Copying output. Listed unconditionally and subtitled with the *gesture* as well as the chord,
// because the gesture is the part nobody can guess: under mouse reporting a plain drag belongs to
// the application, so a user who has learnt that their terminal needs ⇧-drag has no reason to try
// dragging here. It refuses out loud with nothing selected, which is what earns it a row at all.
items.Add(new CommandItem(
CommandGroup.Terminal, "Copy the selection", "term:copy", "⌃C · drag across a pane to select"));

// The client's own messages — the status-line notices that dismiss themselves — kept out of the
// output window (and so out of the session log) and readable here instead.
items.Add(new CommandItem(
Expand Down
10 changes: 9 additions & 1 deletion src/SharpMUTerm.Tui/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,14 @@ private static int Main(string[] args)
logRoot: logRoot,
restore: restore,
mssp: mssp,
openUrl: ExternalBrowser.Open);
openUrl: ExternalBrowser.Open,

// The one place a real clipboard is reached. SharpConsoleUI's helper writes OSC 52 *and*
// mirrors to the platform tool (wl-copy / xclip / pbcopy / Win32), so a copy lands whether the
// client is local or on the far end of an ssh session. Supplied here rather than reached for
// inside the app for the reason logRoot and the browser launcher are: nothing that is not this
// entry point may touch the developer's clipboard, least of all the test suite.
clipboard: SharpConsoleUI.Helpers.ClipboardHelper.SetText);
var exitCode = liveApp.Run(startup); // blocks on the SharpConsoleUI main loop until exit

// Persist the workspace so the next launch resumes where this one left off.
Expand Down Expand Up @@ -300,6 +307,7 @@ private static void WriteUsage(TextWriter usage)
usage.WriteLine("character's saved password and connect line — F5's 'login' row says which.");
usage.WriteLine();
usage.WriteLine("In-app: Up/Down history · Ctrl+N next tab · Ctrl+W close · Ctrl+P palette · Ctrl+Q quit.");
usage.WriteLine("Selection: drag across a pane's output to select it, Ctrl+C to copy.");
// The composer earns a line of its own because what it *sends* is not guessable from the window:
// the buffer is one command and its line breaks are written %r, which is what a MUSH board or
// mail body wants. Naming the send chord matters for the same reason — Ctrl+Enter is what a
Expand Down
Loading