Skip to content
Merged
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
14 changes: 13 additions & 1 deletion DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -2805,7 +2805,19 @@ Alert rows of the table above from the `--droplet-*` roles in `src/styles.css`.
this device's, kept in its own `ovis:mirror-wrap` key so an older tab's save of `ovis:settings` cannot
erase it), the grid at its size panned sideways as before, and Wrap to screen back. Until the
user chooses, a narrow screen (a touch screen, or 768px and under) folds and a desktop does not. A
watching phone folds too; an attached pty and a mirror its mount fits are drawn as they are. Folded
watching phone folds too. An attached pane's grid folds the same way while this device does not
size the pty: watching it (a device paired to watch), or under a phone's soft keyboard, where the
grid stays at another device's size until the keyboard goes (#1045). It is drawn from the xterm
buffer the attach stream wrote, never from a pane read, and the pane is never resized for it; the
pane menu then offers the same choice, under the same setting. A cursor the grid shows (an attached
pane's; a mirror hides it) is drawn in the folded rows in the grid's shape and colours, and the hidden
grid moves down and sideways onto it, so a preedit stands on the caret even in an earlier part of a
long folded line. A phone that drives the pty is never
folded (the program redraws at its width), nor is a split's following half, and a grid its mount
fits is drawn as it is. Under the keyboard the folded rows pan by finger and wheel up to their
edges, past which a swipe scrolls the pane as before; a tap on them is no mouse click for the
program (herdr's attach reports the mouse for every pane, so a program that wants clicks cannot be
told apart): Original width keeps clicks. Folded
rows keep the cursor's row in view and pan only up and down; a tap on a web address, a file path or
an OSC 8 hyperlink's label opens it as the grid's links do, any other tap focuses the terminal, and a wheel past their edges
scrolls the mirror's history at the chosen speed. Find's boxes are hidden while folded.
Expand Down
6 changes: 6 additions & 0 deletions changelog.d/wrap-view-attach.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
### Fixed
- On a phone that only watches a pane, a pane another device sized wider than the screen is no
longer cut off on the right: it is drawn folded to the screen's width, as a Windows PC's pane is,
and so is a pane drawn at another device's size while the phone's keyboard is up. The pane menu's
Original width draws it at that device's width again, panned sideways. A phone that types into
the pane still gets the program redrawn at its own width.
2 changes: 1 addition & 1 deletion docs/development.md
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ bun scripts/chat-browser-qa.ts # chat lens end to end
bun scripts/omo-plan-browser-regression.ts # OmO todo progress and plan panel on isolated native fixtures
bun scripts/pane-input-focus-demo-regression.ts # the input focus shortcut against fictional panes
bun scripts/mirror-scroll-demo-regression.ts # fixed mirror wheel/touch, native history, no guessed program input, observe guards
bun scripts/mirror-fold-demo-regression.ts # a mirror wider than a phone folded whole (portrait, landscape), Original width from the pane menu, observers and desktops unfolded
bun scripts/mirror-fold-demo-regression.ts # a mirror, or an attached pane a phone watches at a wider grid, folded whole (portrait, landscape), Original width from the pane menu, driving phones and desktops unfolded
bun scripts/shortcut-exact-modifiers-demo-regression.ts # recording exact-modifier chords: confirm, refusals, dispatch
bun scripts/output-browser-qa.ts # terminal output flow control end to end
bun scripts/math-browser-qa.ts # chat math: KaTeX loads with the first expression
Expand Down
277 changes: 242 additions & 35 deletions scripts/mirror-fold-demo-regression.ts

Large diffs are not rendered by default.

11 changes: 7 additions & 4 deletions src/components/PaneCanvas.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { layoutCells, zoomMode } from "../lib/layoutMap.ts";
import { layoutTopology, paneContextPress, paneFieldTarget, SOLO_PANE_QUERY, soloPane, splitPath, splitRatioAt } from "../lib/paneCanvas.ts";
import { useT } from "../lib/i18n.ts";
import { useSettings } from "../lib/settings.ts";
import { MIRROR_WRAP_QUERY } from "../lib/terminalFold.ts";
import { MIRROR_WRAP_QUERY, wideGridKey, wideGrids } from "../lib/terminalFold.ts";
import { userErrorMessage, type UserError } from "../lib/userError.ts";
import { useFocusTrap } from "../lib/useFocusTrap.ts";
import { usePhoneSheetBack } from "../lib/sheetHistory.ts";
Expand Down Expand Up @@ -64,6 +64,8 @@ export function PaneCanvas({ snapshot, pane, selectedPaneId, onSelect, onChanged
const { settings, update: updateSettings } = useSettings();
const narrowScreen = useMediaQuery(MIRROR_WRAP_QUERY);
const mirrorWraps = settings.terminalMirrorWrap ?? narrowScreen;
// attached panes drawn here at a grid this device does not size, wider than the screen (watched, or held under the keyboard)
const wideGrid = useSyncExternalStore(wideGrids.subscribe, wideGrids.snapshot, wideGrids.snapshot);
const machineId = useMachineId();
const apple = /Mac|iPhone|iPad/.test(navigator.platform);
const coarse = useMediaQuery("(pointer: coarse)");
Expand Down Expand Up @@ -174,9 +176,10 @@ export function PaneCanvas({ snapshot, pane, selectedPaneId, onSelect, onChanged
await api.zoomPane(target.pane_id, layout ? zoomMode(layout, target.pane_id) : "on");
if (alive.current && latest.current.tabId === tabId && latest.current.selection === selection) onSelect(target.pane_id);
}) },
// a mirror wider than the screen is folded to it (lib/terminalFold.ts); a full-screen program reads
// better at its own width, panned sideways. This device's choice, for every mirrored pane
...(mirrored ? [{ id: "mirror-width", label: t(mirrorWraps ? "Original width" : "Wrap to screen"),
// a mirror, or a pane watched at another device's grid, wider than the screen is folded to it
// (lib/terminalFold.ts); a full-screen program reads better at its own width, panned sideways.
// This device's choice, for every such pane
...(mirrored || wideGrid.has(wideGridKey(machineId, target.pane_id)) ? [{ id: "mirror-width", label: t(mirrorWraps ? "Original width" : "Wrap to screen"),
icon: mirrorWraps ? MoveHorizontal : TextWrap,
run: () => updateSettings({ terminalMirrorWrap: !mirrorWraps }) }] : []),
{ id: "close", label: t("Close pane"), icon: X, danger: true, run: () => requestClose(target) },
Expand Down
9 changes: 6 additions & 3 deletions src/components/PaneTerminal.css
Original file line number Diff line number Diff line change
Expand Up @@ -46,12 +46,15 @@
overflow: auto;
}

/* a mirror wider than the screen, drawn folded to its width (lib/terminalFold.ts). The grid stays
under the folded rows for its input and its readers, unseen and clipped to the mount, so only the
folded rows pan, up and down. The helper textarea is not hidden: typing still reaches xterm */
/* a grid wider than the screen that this browser does not size (a mirror, a watched pane, one held
under the keyboard at another device's size), drawn folded to its width (lib/terminalFold.ts). The
grid stays under the folded rows for its input and its readers, unseen and clipped to the mount, so
only the folded rows pan, up and down (a held grid is not adopted, so its mount scrolls from here).
The helper textarea is not hidden: typing still reaches xterm */
.pane-terminal[data-folded] {
position: relative;
overflow-x: hidden;
overflow-y: auto;
}

.pane-terminal[data-folded] > .xterm {
Expand Down
2 changes: 1 addition & 1 deletion src/components/PaneTerminal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -586,7 +586,7 @@ export function PaneTerminal({

useTerminalRefits({ activeRef, autoSelected, baseFontRef, chatView, chatViewRef, coarseRef, findOpenRef, fitRef, fittedRef, fixedGridRef, follow, followCursorRef, keyboardHoldsRef, narrowFont, narrowFontFitted, observeRef, palette, paneRef, peekRef, sharedGridRef, shown, sizesPty, socketRef, termRef, terminalFontFamily, terminalFontSize, theme });

usePaneAttach({ activeRef, autoSelected, cached, coarseRef, deferAttachRef, draftPaneRef, findOpenRef, fitRef, fittedRef, fixedGridRef, frameGateRef, hyperlinksRef, hostRef, machineId, mirrored, modifyOtherKeysRef, observeRef, paneId, pending, presented, presentationRef, releaseRef, releasedRef, retireOutputRef, secretRef, setDraft, setComposing, setEnded, setHeld, setInputError, setInputReady, setOutputError, setOutputReady, setReleased, setSecret, setUnsupported, setWatching, sharedGridRef, socketRef, stalledRef, terminalId, termRef, chatViewRef, cancelLeaveRef, cachedRef, watchingRef, keyboardHoldsRef, followRef, leaveRef, shownRef });
usePaneAttach({ activeRef, autoSelected, cached, coarseRef, deferAttachRef, draftPaneRef, findOpenRef, fitRef, fittedRef, fixedGridRef, frameGateRef, hyperlinksRef, hostRef, machineId, mirrored, modifyOtherKeysRef, observeRef, paneId, pending, presented, presentationRef, releaseRef, releasedRef, retireOutputRef, secretRef, setDraft, setComposing, setEnded, setHeld, setInputError, setInputReady, setOutputError, setOutputReady, setReleased, setSecret, setUnsupported, setWatching, sharedGridRef, socketRef, stalledRef, terminalId, termRef, chatViewRef, cancelLeaveRef, cachedRef, watchingRef, keyboardHoldsRef, followRef, followCursorRef, leaveRef, shownRef });


// A sibling already mounted in the split canvas becomes the keyboard target only on an
Expand Down
10 changes: 6 additions & 4 deletions src/lib/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,10 +97,12 @@ export interface Settings {
* Stored under this key, not 0.4.1's `terminalOsc52`: settings are saved whole, so a `false` there
* was written by any change at all, not chosen, and is ignored. */
paneClipboard: boolean;
/** a mirrored pane (a Windows PC: no terminal attach) whose grid is wider than this screen is drawn
* folded to the screen's width (lib/terminalFold.ts); false draws the grid at its original width, panned
* sideways. Chosen from the pane menu, for this device; null until then: folded on a narrow screen
* (MIRROR_WRAP_QUERY), the original width elsewhere. */
/** a pane whose grid this device does not size and is wider than this screen (a mirrored pane: a Windows
* PC, no terminal attach; an attached pane it only watches, or holds under the keyboard, at the grid
* another device gave the pty) is drawn folded to the screen's width (lib/terminalFold.ts); false draws
* the grid at its original width, panned sideways. Chosen from the pane menu, for this device; null
* until then: folded on a narrow screen (MIRROR_WRAP_QUERY), the original width elsewhere. The name and
* its `ovis:mirror-wrap` key are kept from when only mirrors folded, so a choice made then still holds. */
terminalMirrorWrap: boolean | null;
/** a desktop tab out of use for a moment detaches its pane (PaneTerminal's release) and only watches it, so herdr's own
* window keeps the pane at its size; paused where the server cannot watch. Off: the tab keeps the pane and its size. */
Expand Down
43 changes: 42 additions & 1 deletion src/lib/terminalFold.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { describe, expect, test } from "bun:test";
import type { IBufferCell, IBufferLine } from "@xterm/xterm";
import { columnAt, contrastRatio, paletteColor, readableColor, rowHtml, webLinkAt } from "./terminalFold.ts";
import { columnAt, contrastRatio, paletteColor, readableColor, rowHtml, webLinkAt, wideGridKey, wideGrids } from "./terminalFold.ts";

interface Cell { chars: string; width?: number; fg?: number; bg?: number; bgRgb?: number; bold?: boolean; dim?: boolean; inverse?: boolean }

Expand Down Expand Up @@ -104,4 +104,45 @@ describe("the folded view of a mirrored grid", () => {
test("bold text of the first eight colours draws in their bright forms", () => {
expect(rowHtml(line([{ chars: "x", fg: 1, bold: true }], 1), 1, {}, scratch)).toBe('<span style="color:#ef2929;font-weight:bold;">x</span>');
});

test("the cursor's cell is drawn on its own in the cursor's shape, the blank cells up to it included", () => {
const theme = { foreground: "#eeeeee", background: "#111111", cursor: "#ff0000", cursorAccent: "#000000" };
expect(rowHtml(line(plain("abc"), 6), 6, theme, scratch, 1, { x: 1, shape: "block" }))
.toBe('a<span class="terminal-fold-cursor" style="background:#ff0000;color:#000000;">b</span>c');
// a shell's prompt ends in a blank the caret stands after
expect(rowHtml(line(plain("ab"), 6), 6, theme, scratch, 1, { x: 3, shape: "outline" }))
.toBe('ab <span class="terminal-fold-cursor" style="box-shadow:inset 0 0 0 1px #ff0000;"> </span>');
// on the second half of a wide glyph: the glyph is the cursor's
expect(rowHtml(line([{ chars: "한", width: 2 }, { chars: "", width: 0 }, { chars: "x" }], 3), 3, theme, scratch, 1, { x: 1, shape: "none" }))
.toBe('<span class="terminal-fold-cursor">한</span>x');
// a write that filled the row: the cursor waits past the last column, and stands on the last cell
expect(rowHtml(line(plain("abcde"), 5), 5, theme, scratch, 1, { x: 5, shape: "block" }))
.toBe('abcd<span class="terminal-fold-cursor" style="background:#ff0000;color:#000000;">e</span>');
});

test("a pane stays wide for the menu while any terminal drawing it says so, and its listeners hear each change", () => {
const pane = wideGridKey("pc", "w1:p1");
const first = {};
const second = {};
let heard = 0;
const stop = wideGrids.subscribe(() => { heard++; });
try {
wideGrids.set(first, pane);
wideGrids.set(second, pane);
wideGrids.set(first, pane);
expect(heard).toBe(2);
wideGrids.set(first, null);
expect(wideGrids.snapshot().has(pane)).toBe(true);
// the other terminal moved on to another pane: this one is no longer wide anywhere
wideGrids.set(second, wideGridKey("pc", "w1:p2"));
expect(wideGrids.snapshot().has(pane)).toBe(false);
wideGrids.set(second, null);
expect(wideGrids.snapshot().size).toBe(0);
expect(heard).toBe(5);
} finally {
stop();
wideGrids.set(first, null);
wideGrids.set(second, null);
}
});
});
Loading
Loading