@timurproko/a1 0.2.7-dev.1221 → 0.2.7-dev.1225
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -6
- package/bin/ui.js +1 -0
- package/dist/app/session-shell/dock-chrome.js +1 -1
- package/dist/app/session-shell/exit-notice.d.ts +2 -2
- package/dist/app/session-shell/exit-notice.js +4 -4
- package/dist/app/session-shell/session-lifecycle.d.ts +2 -1
- package/dist/app/session-shell/session-lifecycle.js +4 -2
- package/dist/app/session-shell/session-shell-root.d.ts +2 -0
- package/dist/app/session-shell/session-shell-root.js +3 -2
- package/dist/app/session-shell/session-shell.js +9 -1
- package/dist/app/session-shell/session-viewport-controller.d.ts +2 -1
- package/dist/app/session-shell/session-viewport-controller.js +26 -37
- package/dist/app/session-shell/shell-editor-factory.js +4 -1
- package/dist/cli/dispatch.js +15 -2
- package/dist/composition/owned-ui.d.ts +2 -0
- package/dist/composition/owned-ui.js +43 -11
- package/dist/composition/reference-selection-options.d.ts +3 -0
- package/dist/composition/reference-selection-options.js +16 -0
- package/dist/composition/settings-route-host.d.ts +1 -2
- package/dist/composition/settings-route-host.js +19 -13
- package/dist/composition/settings-search-input.js +4 -1
- package/dist/composition/utility-tab.js +4 -2
- package/dist/features/launch/intent.js +2 -2
- package/dist/features/owned-ui/plus-menu-dialog.js +28 -20
- package/dist/features/owned-ui/reference-document-selection.d.ts +30 -0
- package/dist/features/owned-ui/reference-document-selection.js +179 -0
- package/dist/features/owned-ui/reference-screen-app.d.ts +4 -1
- package/dist/features/owned-ui/reference-screen-app.js +39 -8
- package/dist/features/owned-ui/settings-app.d.ts +1 -0
- package/dist/features/owned-ui/settings-app.js +23 -19
- package/dist/foundation/release/automatic-update.js +90 -22
- package/dist/foundation/release/bootstrap.js +2 -2
- package/dist/foundation/release/latest-release.d.ts +5 -1
- package/dist/foundation/release/latest-release.js +74 -31
- package/dist/integrations/pi/components/owned-editor-ux.js +48 -5
- package/dist/integrations/pi/engine/workflow-runner.js +1 -1
- package/dist/native/darwin-arm64/manifest.json +1 -1
- package/dist/native/darwin-arm64/terminal-host.manifest.json +3 -3
- package/dist/native/darwin-arm64/terminal-host.node +0 -0
- package/dist/native/darwin-x64/manifest.json +1 -1
- package/dist/native/darwin-x64/terminal-host.manifest.json +3 -3
- package/dist/native/darwin-x64/terminal-host.node +0 -0
- package/dist/native/linux-x64/manifest.json +1 -1
- package/dist/native/linux-x64/terminal-host.manifest.json +3 -3
- package/dist/native/linux-x64/terminal-host.node +0 -0
- package/dist/native/win32-x64/manifest.json +2 -2
- package/dist/native/win32-x64/process-guardian.node +0 -0
- package/dist/native/win32-x64/terminal-host.manifest.json +3 -3
- package/dist/native/win32-x64/terminal-host.node +0 -0
- package/dist/ui/components/dialog-panel.d.ts +17 -8
- package/dist/ui/components/dialog-panel.js +97 -26
- package/dist/ui/components/index.d.ts +3 -3
- package/dist/ui/components/index.js +3 -3
- package/dist/ui/components/scrollbar.d.ts +1 -1
- package/dist/ui/components/scrollbar.js +1 -1
- package/dist/ui/components/selection-copy.d.ts +2 -4
- package/dist/ui/components/selection-copy.js +4 -2
- package/dist/ui/components/text-selection.d.ts +1 -2
- package/dist/ui/components/text-selection.js +5 -2
- package/dist/ui/components/transcript-viewport.js +3 -3
- package/dist/ui/settings/declarations.d.ts +1 -1
- package/dist/ui/settings/declarations.js +1 -1
- package/docs/architecture/resident-tabs.md +5 -4
- package/docs/features/launch-profiles.md +7 -4
- package/package.json +3 -3
|
@@ -18,7 +18,7 @@ export function createOwnedRouteHost(settings, references, options = {}) {
|
|
|
18
18
|
const title = session ? "Session Info" : changelog ? input ? "What's New" : "Changelog" : HOTKEYS_TITLE;
|
|
19
19
|
const document = session ? references.session() : changelog ? references.changelog(input) : references.hotkeys();
|
|
20
20
|
const pinned = options.tabRoute === route;
|
|
21
|
-
return deferredSurface(route, title, () => loadReferenceSurface(settings, route, title, document, pinned));
|
|
21
|
+
return deferredSurface(route, title, () => loadReferenceSurface(settings, route, title, document, pinned, options));
|
|
22
22
|
},
|
|
23
23
|
};
|
|
24
24
|
}
|
|
@@ -63,9 +63,10 @@ function deferredSurface(id, label, load) {
|
|
|
63
63
|
};
|
|
64
64
|
}
|
|
65
65
|
async function loadSettingsSurface(settings, options) {
|
|
66
|
-
const [{ SettingsApp }, { createSettingsSearchInputFactory }] = await Promise.all([
|
|
66
|
+
const [{ SettingsApp }, { createSettingsSearchInputFactory }, { renderFloatingDialogSurface, renderFloatingMenuSurface }] = await Promise.all([
|
|
67
67
|
import("../features/owned-ui/settings-app.js"),
|
|
68
68
|
import("./settings-search-input.js"),
|
|
69
|
+
import("../ui/components/dialog-panel.js"),
|
|
69
70
|
]);
|
|
70
71
|
const pinned = options.tabRoute === SETTINGS_ROUTE;
|
|
71
72
|
const searchInputFactory = createSettingsSearchInputFactory({
|
|
@@ -78,14 +79,19 @@ async function loadSettingsSurface(settings, options) {
|
|
|
78
79
|
pinned,
|
|
79
80
|
searchInputFactory,
|
|
80
81
|
configurePlusMenu: options.configurePlusMenu ?? null,
|
|
82
|
+
dialogPanel: text => renderFloatingDialogSurface(text, piTheme().getColorMode()),
|
|
81
83
|
...(options.copyText === undefined ? {} : { copyText: options.copyText }),
|
|
82
|
-
}), !pinned, true);
|
|
84
|
+
}), !pinned, true, text => renderFloatingMenuSurface(text, piTheme().getColorMode()));
|
|
83
85
|
}
|
|
84
|
-
async function loadReferenceSurface(settings, route, title, document, pinned) {
|
|
86
|
+
async function loadReferenceSurface(settings, route, title, document, pinned, options) {
|
|
85
87
|
const id = route;
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
88
|
+
const [{ ReferenceScreenApp }, content, selection] = await Promise.all([
|
|
89
|
+
import("../features/owned-ui/reference-screen-app.js"),
|
|
90
|
+
document,
|
|
91
|
+
route === CHANGELOG_ROUTE
|
|
92
|
+
? import("./reference-selection-options.js").then(module => module.createReferenceSelectionOptions(settings.file, options.copyText))
|
|
93
|
+
: undefined,
|
|
94
|
+
]);
|
|
89
95
|
return hostApp(id, route, () => new ReferenceScreenApp({
|
|
90
96
|
id,
|
|
91
97
|
title,
|
|
@@ -96,9 +102,10 @@ async function loadReferenceSurface(settings, route, title, document, pinned) {
|
|
|
96
102
|
scrollbarSpeed: settings.value("scrollbarSpeed"),
|
|
97
103
|
}),
|
|
98
104
|
pinned,
|
|
99
|
-
|
|
105
|
+
...(selection === undefined ? {} : { selection }),
|
|
106
|
+
}), !pinned, selection !== undefined);
|
|
100
107
|
}
|
|
101
|
-
async function hostApp(id, route, create, closeOnInterrupt = true, appInputBeforeInterrupt = false) {
|
|
108
|
+
async function hostApp(id, route, create, closeOnInterrupt = true, appInputBeforeInterrupt = false, panel = text => text) {
|
|
102
109
|
const [{ UiAppHost }, { UiAppRegistry }] = await Promise.all([
|
|
103
110
|
import("../ui/apps/host.js"),
|
|
104
111
|
import("../ui/apps/registry.js"),
|
|
@@ -114,7 +121,7 @@ async function hostApp(id, route, create, closeOnInterrupt = true, appInputBefor
|
|
|
114
121
|
registry,
|
|
115
122
|
closeOnInterrupt,
|
|
116
123
|
appInputBeforeInterrupt,
|
|
117
|
-
theme: pinnedTheme(),
|
|
124
|
+
theme: pinnedTheme(panel),
|
|
118
125
|
surface: {
|
|
119
126
|
size: () => size,
|
|
120
127
|
requestRender: () => onRender(),
|
|
@@ -143,8 +150,7 @@ async function hostApp(id, route, create, closeOnInterrupt = true, appInputBefor
|
|
|
143
150
|
onExitRequested: listener => { onExit = listener; },
|
|
144
151
|
};
|
|
145
152
|
}
|
|
146
|
-
|
|
147
|
-
function pinnedTheme() {
|
|
153
|
+
function pinnedTheme(panel) {
|
|
148
154
|
return {
|
|
149
155
|
fg: (token, text) => piTheme().fg(token, text),
|
|
150
156
|
bold: (text) => piTheme().bold(text),
|
|
@@ -152,6 +158,6 @@ function pinnedTheme() {
|
|
|
152
158
|
accentPreview: (color, text) => renderPiAccentPreview(color, text),
|
|
153
159
|
disabled: (text) => faint(piTheme().fg("dim", text)),
|
|
154
160
|
highlight: (text) => piTheme().bg("selectedBg", text),
|
|
155
|
-
panel
|
|
161
|
+
panel,
|
|
156
162
|
};
|
|
157
163
|
}
|
|
@@ -3,6 +3,7 @@ import { createPiShellEditor } from "../integrations/pi/components/shell-editor-
|
|
|
3
3
|
import { piShellTruncateToWidth, piShellVisibleWidth, } from "../integrations/pi/components/shell-shared-facade.js";
|
|
4
4
|
import { backgroundSgrSpan } from "../ui/components/spans.js";
|
|
5
5
|
import { caretCell } from "../ui/components/line-input.js";
|
|
6
|
+
import { paintTextSelection } from "../ui/components/text-selection.js";
|
|
6
7
|
import { PromptInput } from "../ui/components/prompt-input.js";
|
|
7
8
|
/** Builds Settings search from the same configurable owned editor used by the agent prompt. */
|
|
8
9
|
export function createSettingsSearchInputFactory(options = {}) {
|
|
@@ -32,7 +33,9 @@ function createSettingsSearchInput(surface, options) {
|
|
|
32
33
|
requestRender: surface.requestRender,
|
|
33
34
|
onSubmit: () => { },
|
|
34
35
|
onCopyText: text => surface.copyText(text),
|
|
35
|
-
paintEditorSelection: (line, from, to, atomic) =>
|
|
36
|
+
paintEditorSelection: (line, from, to, atomic) => atomic
|
|
37
|
+
? backgroundSgrSpan(line, from, to, "\u001b[7m", "\u001b[27m", piShellVisibleWidth)
|
|
38
|
+
: paintTextSelection(line, from, to, piShellVisibleWidth, true),
|
|
36
39
|
promptPresentation: {
|
|
37
40
|
input: new PromptInput({ fg: (token, text) => surface.theme.fg(token, text) }, {
|
|
38
41
|
measure: piShellVisibleWidth,
|
|
@@ -73,8 +73,10 @@ function createUtilityRouteHost(options, settings, keybindings, cwd, agentDir) {
|
|
|
73
73
|
agentDir,
|
|
74
74
|
cwd,
|
|
75
75
|
copyText: text => {
|
|
76
|
-
if (Buffer.byteLength(text)
|
|
77
|
-
|
|
76
|
+
if (Buffer.byteLength(text) > MAX_CLIPBOARD_TEXT_BYTES)
|
|
77
|
+
return false;
|
|
78
|
+
void writeSystemClipboardText(text).catch(() => { });
|
|
79
|
+
return true;
|
|
78
80
|
},
|
|
79
81
|
}));
|
|
80
82
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { sessionSelectionArguments } from "../../foundation/lifecycle/index.js";
|
|
2
2
|
export { parseSessionSelection } from "../../foundation/lifecycle/index.js";
|
|
3
3
|
export function interactiveLaunchIntent(profileId, sessionSelection, launchMode = profileId === "a1" ? "tabs" : "solo") {
|
|
4
|
-
if (sessionSelection !== undefined &&
|
|
5
|
-
throw new Error("session selection requires the
|
|
4
|
+
if (sessionSelection !== undefined && profileId !== "a1") {
|
|
5
|
+
throw new Error("session selection requires the A1 profile");
|
|
6
6
|
}
|
|
7
7
|
if (profileId === "pi" && launchMode !== "solo")
|
|
8
8
|
throw new Error("the Pi comparison launch is always solo");
|
|
@@ -1,9 +1,15 @@
|
|
|
1
|
-
import { LineInput,
|
|
1
|
+
import { LineInput, handleLineInputKey, padToWidth, renderInputRow, renderShortcutHintsWithClose, truncateToWidth, } from "../../ui/components/index.js";
|
|
2
2
|
/** The A1 setting holding the tab kinds the strip's `+` offers. */
|
|
3
3
|
export const PLUS_MENU_SETTING_ID = "tabBarPlusMenu";
|
|
4
4
|
const TITLE = "Plus Menu";
|
|
5
5
|
const DESCRIPTION = "Choose what the + button in the tab bar can add.";
|
|
6
|
-
const
|
|
6
|
+
const HINTS = [
|
|
7
|
+
{ key: "Type", action: "search" },
|
|
8
|
+
{ key: "↑↓", action: "navigate" },
|
|
9
|
+
{ key: "Space", action: "toggle" },
|
|
10
|
+
{ key: "esc", action: "close" },
|
|
11
|
+
];
|
|
12
|
+
const PADDING_X = 2;
|
|
7
13
|
/**
|
|
8
14
|
* The plus-menu dialog: every tab kind with `●` when `+` offers it and `○` when not, filtered by typed
|
|
9
15
|
* text. It owns only its filter and the row in hand; the enabled record and its saving stay with the
|
|
@@ -41,36 +47,38 @@ export class PlusMenuDialog {
|
|
|
41
47
|
return "handled";
|
|
42
48
|
}
|
|
43
49
|
lines(enabled, width, theme) {
|
|
44
|
-
const
|
|
45
|
-
const
|
|
46
|
-
const
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
50
|
+
const frameWidth = Math.max(0, width);
|
|
51
|
+
const left = Math.min(PADDING_X, frameWidth);
|
|
52
|
+
const right = Math.min(PADDING_X, frameWidth - left);
|
|
53
|
+
const contentWidth = Math.max(0, frameWidth - left - right);
|
|
54
|
+
const blank = " ".repeat(frameWidth);
|
|
55
|
+
const row = (painted, selected = false) => {
|
|
56
|
+
const content = padToWidth(truncateToWidth(painted, contentWidth), contentWidth);
|
|
57
|
+
const rightPadding = " ".repeat(right);
|
|
58
|
+
return `${" ".repeat(left)}${selected ? theme.highlight(content) : content}${selected ? theme.panel(rightPadding) : rightPadding}`;
|
|
50
59
|
};
|
|
51
60
|
const shown = this.shown();
|
|
52
61
|
const index = Math.min(this.#index, Math.max(0, shown.length - 1));
|
|
53
62
|
const rows = shown.length === 0
|
|
54
|
-
? [row(theme.fg("dim", "No tabs match")
|
|
63
|
+
? [row(theme.fg("dim", "No tabs match"))]
|
|
55
64
|
: shown.map((kind, position) => {
|
|
56
65
|
const selected = position === index;
|
|
57
66
|
const cursor = selected ? "→ " : " ";
|
|
58
67
|
const marker = enabled[kind.key] === true ? "●" : "○";
|
|
59
|
-
const raw = `${cursor}${marker} ${kind.label}`;
|
|
60
68
|
const content = `${selected ? theme.fg("accent", cursor) : cursor}${theme.fg("text", marker)} ${theme.fg("text", kind.label)}`;
|
|
61
|
-
return row(
|
|
69
|
+
return row(content, selected);
|
|
62
70
|
});
|
|
63
71
|
return [
|
|
64
|
-
|
|
65
|
-
row(theme.bold(theme.fg("accent", TITLE))
|
|
66
|
-
row(theme.fg("dim", DESCRIPTION)
|
|
67
|
-
|
|
68
|
-
...renderInputRow(this.#filter,
|
|
69
|
-
|
|
72
|
+
blank,
|
|
73
|
+
row(theme.bold(theme.fg("accent", TITLE))),
|
|
74
|
+
row(theme.fg("dim", DESCRIPTION)),
|
|
75
|
+
blank,
|
|
76
|
+
...renderInputRow(this.#filter, contentWidth, { ruled: false, promptGlyph: "> ", theme }).lines.map(line => row(line)),
|
|
77
|
+
blank,
|
|
70
78
|
...rows,
|
|
71
|
-
|
|
72
|
-
row(
|
|
73
|
-
|
|
79
|
+
blank,
|
|
80
|
+
row(renderShortcutHintsWithClose(HINTS, theme, contentWidth)),
|
|
81
|
+
blank,
|
|
74
82
|
];
|
|
75
83
|
}
|
|
76
84
|
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { type PaneMouseEvent, type UiTheme } from "../../ui/components/index.js";
|
|
2
|
+
export interface ReferenceDocumentSelectionOptions {
|
|
3
|
+
readonly copyOnSelect: () => boolean;
|
|
4
|
+
readonly copyText: (text: string) => boolean | void;
|
|
5
|
+
}
|
|
6
|
+
interface ReferenceSelectionFrame {
|
|
7
|
+
readonly bodyRowStart: number;
|
|
8
|
+
readonly contentWidth: number;
|
|
9
|
+
readonly documentRows: readonly string[];
|
|
10
|
+
readonly visibleDocumentRows: readonly (number | undefined)[];
|
|
11
|
+
}
|
|
12
|
+
/** Selection state for an opted-in read-only reference document. */
|
|
13
|
+
export declare class ReferenceDocumentSelection {
|
|
14
|
+
#private;
|
|
15
|
+
constructor(options: ReferenceDocumentSelectionOptions);
|
|
16
|
+
get selecting(): boolean;
|
|
17
|
+
get hasSelection(): boolean;
|
|
18
|
+
updateFrame(frame: ReferenceSelectionFrame): void;
|
|
19
|
+
paintVisibleRow(visibleRow: number, line: string, theme: UiTheme): string;
|
|
20
|
+
handleInput(data: string): {
|
|
21
|
+
readonly consumed: boolean;
|
|
22
|
+
readonly cleared: boolean;
|
|
23
|
+
};
|
|
24
|
+
handleMouse(event: PaneMouseEvent, requestRender?: () => void): {
|
|
25
|
+
readonly consumed: boolean;
|
|
26
|
+
readonly changed: boolean;
|
|
27
|
+
};
|
|
28
|
+
clear(): void;
|
|
29
|
+
}
|
|
30
|
+
export {};
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { COPY_ACKNOWLEDGEMENT_DURATION_MS, copyAcknowledgementText, displayWidth, extendTextSelection, orderedTextSelection, overlaySpan, paintTextSelection, padToWidth, pressTextSelection, releaseTextSelection, stripAnsi, textSelectionLineExtendColumn, textSelectionPointAt, textSelectionText, truncateToWidth, usefulTextLineContent, } from "../../ui/components/index.js";
|
|
2
|
+
const INTERRUPT = "\u0003";
|
|
3
|
+
class CopyAcknowledgement {
|
|
4
|
+
#message;
|
|
5
|
+
#timer;
|
|
6
|
+
paint(line, finalRow, width, theme) {
|
|
7
|
+
if (!finalRow || this.#message === undefined || width === 0)
|
|
8
|
+
return line;
|
|
9
|
+
const message = truncateToWidth(theme.fg("accent", this.#message), width);
|
|
10
|
+
return overlaySpan(line, width - displayWidth(message), width, message, { inheritStartStyle: true });
|
|
11
|
+
}
|
|
12
|
+
show(characters, requestRender) {
|
|
13
|
+
this.#message = copyAcknowledgementText(characters);
|
|
14
|
+
this.#clearTimer();
|
|
15
|
+
this.#timer = setTimeout(() => {
|
|
16
|
+
this.#timer = undefined;
|
|
17
|
+
this.#message = undefined;
|
|
18
|
+
requestRender();
|
|
19
|
+
}, COPY_ACKNOWLEDGEMENT_DURATION_MS);
|
|
20
|
+
this.#timer.unref?.();
|
|
21
|
+
requestRender();
|
|
22
|
+
}
|
|
23
|
+
clear() {
|
|
24
|
+
this.#clearTimer();
|
|
25
|
+
this.#message = undefined;
|
|
26
|
+
}
|
|
27
|
+
#clearTimer() {
|
|
28
|
+
if (this.#timer !== undefined)
|
|
29
|
+
clearTimeout(this.#timer);
|
|
30
|
+
this.#timer = undefined;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/** Selection state for an opted-in read-only reference document. */
|
|
34
|
+
export class ReferenceDocumentSelection {
|
|
35
|
+
#copyOnSelect;
|
|
36
|
+
#copyText;
|
|
37
|
+
#selection;
|
|
38
|
+
#click;
|
|
39
|
+
#frame;
|
|
40
|
+
#acknowledgement = new CopyAcknowledgement();
|
|
41
|
+
constructor(options) {
|
|
42
|
+
this.#copyOnSelect = options.copyOnSelect;
|
|
43
|
+
this.#copyText = options.copyText;
|
|
44
|
+
}
|
|
45
|
+
get selecting() {
|
|
46
|
+
return this.#selection?.selecting === true;
|
|
47
|
+
}
|
|
48
|
+
get hasSelection() {
|
|
49
|
+
return orderedTextSelection(this.#selection) !== undefined;
|
|
50
|
+
}
|
|
51
|
+
updateFrame(frame) {
|
|
52
|
+
const previous = this.#frame;
|
|
53
|
+
if (this.#selection !== undefined && previous !== undefined && !selectionStillMaps(this.#selection, previous.documentRows, frame.documentRows)) {
|
|
54
|
+
this.#selection = undefined;
|
|
55
|
+
this.#click = undefined;
|
|
56
|
+
}
|
|
57
|
+
this.#frame = frame;
|
|
58
|
+
}
|
|
59
|
+
paintVisibleRow(visibleRow, line, theme) {
|
|
60
|
+
const frame = this.#frame;
|
|
61
|
+
const documentRow = frame?.visibleDocumentRows[visibleRow];
|
|
62
|
+
const ordered = orderedTextSelection(this.#selection);
|
|
63
|
+
const width = frame?.contentWidth ?? 0;
|
|
64
|
+
let painted = padToWidth(line, width);
|
|
65
|
+
if (documentRow !== undefined && ordered !== undefined
|
|
66
|
+
&& documentRow >= ordered.start.line && documentRow <= ordered.end.line) {
|
|
67
|
+
const from = documentRow === ordered.start.line ? ordered.start.column : 0;
|
|
68
|
+
const to = documentRow === ordered.end.line ? ordered.end.column : width;
|
|
69
|
+
painted = paintTextSelection(painted, Math.max(0, from), Math.min(width, to));
|
|
70
|
+
}
|
|
71
|
+
return this.#acknowledgement.paint(painted, visibleRow === (frame?.visibleDocumentRows.length ?? 0) - 1, width, theme);
|
|
72
|
+
}
|
|
73
|
+
handleInput(data) {
|
|
74
|
+
if (data === INTERRUPT && this.hasSelection) {
|
|
75
|
+
this.#submitSelection();
|
|
76
|
+
this.#selection = undefined;
|
|
77
|
+
this.#click = undefined;
|
|
78
|
+
return { consumed: true, cleared: true };
|
|
79
|
+
}
|
|
80
|
+
if (this.#selection === undefined)
|
|
81
|
+
return { consumed: false, cleared: false };
|
|
82
|
+
this.#selection = undefined;
|
|
83
|
+
this.#click = undefined;
|
|
84
|
+
return { consumed: false, cleared: true };
|
|
85
|
+
}
|
|
86
|
+
handleMouse(event, requestRender) {
|
|
87
|
+
const frame = this.#frame;
|
|
88
|
+
if (frame === undefined)
|
|
89
|
+
return { consumed: false, changed: false };
|
|
90
|
+
if (event.kind === "motion" && this.selecting) {
|
|
91
|
+
const row = documentRowAt(frame, event.row, true);
|
|
92
|
+
if (row === undefined)
|
|
93
|
+
return { consumed: true, changed: false };
|
|
94
|
+
this.#extend(row, event.column);
|
|
95
|
+
return { consumed: true, changed: true };
|
|
96
|
+
}
|
|
97
|
+
if (event.kind === "release" && this.selecting) {
|
|
98
|
+
this.#selection = releaseTextSelection(this.#selection);
|
|
99
|
+
const copiedCharacters = this.hasSelection && this.#copyOnSelect() ? this.#submitSelection() : undefined;
|
|
100
|
+
if (copiedCharacters !== undefined && requestRender !== undefined) {
|
|
101
|
+
this.#acknowledgement.show(copiedCharacters, requestRender);
|
|
102
|
+
}
|
|
103
|
+
return { consumed: true, changed: true };
|
|
104
|
+
}
|
|
105
|
+
if (event.kind !== "press" || event.button !== 0)
|
|
106
|
+
return { consumed: false, changed: false };
|
|
107
|
+
const row = documentRowAt(frame, event.row, false);
|
|
108
|
+
if (row === undefined || event.column < 1 || event.column > frame.contentWidth) {
|
|
109
|
+
const changed = this.#selection !== undefined;
|
|
110
|
+
this.#selection = undefined;
|
|
111
|
+
this.#click = undefined;
|
|
112
|
+
return { consumed: false, changed };
|
|
113
|
+
}
|
|
114
|
+
const source = frame.documentRows[row] ?? "";
|
|
115
|
+
const pressed = pressTextSelection({
|
|
116
|
+
line: row,
|
|
117
|
+
column: event.column,
|
|
118
|
+
contentWidth: frame.contentWidth,
|
|
119
|
+
lineText: source,
|
|
120
|
+
lineContent: usefulTextLineContent(source),
|
|
121
|
+
...(this.#click === undefined ? {} : { previousClick: this.#click }),
|
|
122
|
+
});
|
|
123
|
+
this.#selection = pressed.selection;
|
|
124
|
+
this.#click = pressed.click;
|
|
125
|
+
return { consumed: true, changed: true };
|
|
126
|
+
}
|
|
127
|
+
clear() {
|
|
128
|
+
this.#selection = undefined;
|
|
129
|
+
this.#click = undefined;
|
|
130
|
+
this.#frame = undefined;
|
|
131
|
+
this.#acknowledgement.clear();
|
|
132
|
+
}
|
|
133
|
+
#extend(row, column) {
|
|
134
|
+
const selection = this.#selection;
|
|
135
|
+
const frame = this.#frame;
|
|
136
|
+
if (selection?.selecting !== true || frame === undefined)
|
|
137
|
+
return;
|
|
138
|
+
const targetColumn = selection.fullRow
|
|
139
|
+
? textSelectionLineExtendColumn(selection, row, frame.contentWidth)
|
|
140
|
+
: Math.max(1, Math.min(column, frame.contentWidth));
|
|
141
|
+
this.#selection = extendTextSelection(selection, selection.fullRow
|
|
142
|
+
? { line: row, column: targetColumn }
|
|
143
|
+
: textSelectionPointAt(row, targetColumn, frame.documentRows[row] ?? ""));
|
|
144
|
+
}
|
|
145
|
+
#submitSelection() {
|
|
146
|
+
const frame = this.#frame;
|
|
147
|
+
const ordered = orderedTextSelection(this.#selection);
|
|
148
|
+
if (frame === undefined || ordered === undefined)
|
|
149
|
+
return undefined;
|
|
150
|
+
const text = textSelectionText(ordered, frame.documentRows, line => usefulTextLineContent(frame.documentRows[line] ?? "")).trim();
|
|
151
|
+
if (text.length === 0 || this.#copyText(text) === false)
|
|
152
|
+
return undefined;
|
|
153
|
+
return text.length;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
function documentRowAt(frame, screenRow, clampToDocument) {
|
|
157
|
+
const visible = frame.visibleDocumentRows;
|
|
158
|
+
let index = screenRow - frame.bodyRowStart;
|
|
159
|
+
if (!clampToDocument)
|
|
160
|
+
return visible[index];
|
|
161
|
+
index = Math.max(0, Math.min(index, visible.length - 1));
|
|
162
|
+
if (visible[index] !== undefined)
|
|
163
|
+
return visible[index];
|
|
164
|
+
const defined = visible.flatMap(row => row === undefined ? [] : [row]);
|
|
165
|
+
if (defined.length === 0)
|
|
166
|
+
return undefined;
|
|
167
|
+
return screenRow < frame.bodyRowStart ? defined[0] : defined.at(-1);
|
|
168
|
+
}
|
|
169
|
+
function selectionStillMaps(selection, previous, next) {
|
|
170
|
+
const first = Math.min(selection.anchor.line, selection.head.line);
|
|
171
|
+
const last = Math.max(selection.anchor.line, selection.head.line);
|
|
172
|
+
if (last >= previous.length || last >= next.length)
|
|
173
|
+
return false;
|
|
174
|
+
for (let line = first; line <= last; line += 1) {
|
|
175
|
+
if (stripAnsi(previous[line] ?? "") !== stripAnsi(next[line] ?? ""))
|
|
176
|
+
return false;
|
|
177
|
+
}
|
|
178
|
+
return true;
|
|
179
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { OwnedUiViewportSettings } from "../../contracts/owned-ui/index.js";
|
|
2
|
+
import { type ReferenceDocumentSelectionOptions } from "./reference-document-selection.js";
|
|
2
3
|
import type { AppHostServices, UiApp } from "../../ui/apps/index.js";
|
|
3
4
|
import { ShortcutRegistry, type PaneInputResult, type PaneMouseEvent, type PaneRect } from "../../ui/components/index.js";
|
|
4
5
|
type Action = "up" | "down" | "page-up" | "page-down" | "first" | "last" | "close";
|
|
@@ -26,6 +27,8 @@ export interface ReferenceScreenOptions {
|
|
|
26
27
|
readonly scrollSettings: () => OwnedUiViewportSettings;
|
|
27
28
|
/** The screen is a resident tab, such as the Changelog tab: it offers no screen-level close. */
|
|
28
29
|
readonly pinned?: boolean;
|
|
30
|
+
/** Enables owned text selection for document routes that explicitly opt in. */
|
|
31
|
+
readonly selection?: ReferenceDocumentSelectionOptions;
|
|
29
32
|
}
|
|
30
33
|
/**
|
|
31
34
|
* Presents one read-only document full screen between two accent rules: the bold
|
|
@@ -42,6 +45,6 @@ export declare class ReferenceScreenApp implements UiApp {
|
|
|
42
45
|
onClose(_host: AppHostServices): void;
|
|
43
46
|
render(rect: PaneRect, host: AppHostServices): readonly string[];
|
|
44
47
|
onInput(data: string, host: AppHostServices): PaneInputResult;
|
|
45
|
-
onMouse(event: PaneMouseEvent,
|
|
48
|
+
onMouse(event: PaneMouseEvent, host: AppHostServices): PaneInputResult;
|
|
46
49
|
}
|
|
47
50
|
export {};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ReferenceDocumentSelection } from "./reference-document-selection.js";
|
|
1
2
|
import { DIALOG_CLOSE_SHORTCUT_HINT } from "../../contracts/presentation/index.js";
|
|
2
3
|
import { GLOBAL_SCOPE, PLAIN_THEME, RAIL_COLUMNS, ShortcutRegistry, ScrollbarRails, assertNoShortcutConflicts, displayWidth, layoutList, padToWidth, renderGroupHeader, renderShortcutHintsWithClose, scrollForTrackPage, scrollbarGeometry, scrollbarPresentation, scrollbarWheelRows, truncateToWidth, withScrollbarRail, } from "../../ui/components/index.js";
|
|
3
4
|
const SCOPE = "reference-screen";
|
|
@@ -49,6 +50,7 @@ export class ReferenceScreenApp {
|
|
|
49
50
|
#document;
|
|
50
51
|
#scrollSettings;
|
|
51
52
|
#pinned;
|
|
53
|
+
#selection;
|
|
52
54
|
#scroll = 0;
|
|
53
55
|
#interruptArmed = false;
|
|
54
56
|
#failure = null;
|
|
@@ -58,7 +60,6 @@ export class ReferenceScreenApp {
|
|
|
58
60
|
#rails = new ScrollbarRails();
|
|
59
61
|
// Rationale: the rail as drawn in the last frame, or null when nothing can be pointed at.
|
|
60
62
|
#railFrame = null;
|
|
61
|
-
#bodyHeight = 0;
|
|
62
63
|
// Invariant: a scroll lights the rail until this time; the timer repaints once it has passed.
|
|
63
64
|
#activeUntil = 0;
|
|
64
65
|
#activityTimer;
|
|
@@ -69,6 +70,7 @@ export class ReferenceScreenApp {
|
|
|
69
70
|
this.#document = options.document;
|
|
70
71
|
this.#scrollSettings = options.scrollSettings;
|
|
71
72
|
this.#pinned = options.pinned === true;
|
|
73
|
+
this.#selection = options.selection === undefined ? undefined : new ReferenceDocumentSelection(options.selection);
|
|
72
74
|
}
|
|
73
75
|
onActivate(host) {
|
|
74
76
|
const ready = this.#document.ready;
|
|
@@ -85,6 +87,7 @@ export class ReferenceScreenApp {
|
|
|
85
87
|
onClose(_host) {
|
|
86
88
|
this.#clearActivityTimer();
|
|
87
89
|
this.#rails.clear();
|
|
90
|
+
this.#selection?.clear();
|
|
88
91
|
}
|
|
89
92
|
render(rect, host) {
|
|
90
93
|
const theme = host.theme ?? PLAIN_THEME;
|
|
@@ -93,9 +96,9 @@ export class ReferenceScreenApp {
|
|
|
93
96
|
const reservesRail = settings.scrollbarAppearance !== "hidden";
|
|
94
97
|
const contentWidth = reservesRail ? Math.max(0, rect.width - RAIL_COLUMNS) : rect.width;
|
|
95
98
|
const bodyHeight = Math.max(0, rect.height - HEADER_ROWS - FOOTER_ROWS);
|
|
96
|
-
this.#bodyHeight = bodyHeight;
|
|
97
99
|
const content = this.#content(contentWidth, theme);
|
|
98
100
|
const body = [];
|
|
101
|
+
const visibleDocumentRows = [];
|
|
99
102
|
let contentLength;
|
|
100
103
|
let viewportLength = bodyHeight;
|
|
101
104
|
if (content.kind === "sections") {
|
|
@@ -115,10 +118,22 @@ export class ReferenceScreenApp {
|
|
|
115
118
|
for (let index = this.#scroll; index < this.#scroll + bodyHeight; index++) {
|
|
116
119
|
const row = content.rows[index] ?? "";
|
|
117
120
|
body.push(displayWidth(row) > contentWidth ? truncateToWidth(row, contentWidth) : row);
|
|
121
|
+
visibleDocumentRows.push(index < content.rows.length ? index : undefined);
|
|
118
122
|
}
|
|
119
123
|
}
|
|
120
124
|
while (body.length < bodyHeight)
|
|
121
125
|
body.push("");
|
|
126
|
+
while (visibleDocumentRows.length < bodyHeight)
|
|
127
|
+
visibleDocumentRows.push(undefined);
|
|
128
|
+
this.#selection?.updateFrame({
|
|
129
|
+
bodyRowStart: HEADER_ROWS + 1,
|
|
130
|
+
contentWidth,
|
|
131
|
+
documentRows: content.kind === "flat" ? content.rows : [],
|
|
132
|
+
visibleDocumentRows,
|
|
133
|
+
});
|
|
134
|
+
const paintedBody = this.#selection === undefined
|
|
135
|
+
? body
|
|
136
|
+
: body.map((row, index) => this.#selection.paintVisibleRow(index, row, theme));
|
|
122
137
|
const now = Date.now();
|
|
123
138
|
// Rationale: every way of scrolling ends in this frame, so a moved document is noticed here
|
|
124
139
|
// once rather than at each wheel, drag, and key branch.
|
|
@@ -138,7 +153,7 @@ export class ReferenceScreenApp {
|
|
|
138
153
|
this.#railFrame = reservesRail && geometry !== null
|
|
139
154
|
? { rail: { key: this.id, column: rect.width, rowStart: HEADER_ROWS, trackHeight: geometry.trackHeight }, geometry, page: viewportLength }
|
|
140
155
|
: null;
|
|
141
|
-
const withRail = withScrollbarRail(
|
|
156
|
+
const withRail = withScrollbarRail(paintedBody.slice(0, bodyHeight), geometry, contentWidth, theme, { presentation });
|
|
142
157
|
const hint = this.#interruptArmed
|
|
143
158
|
? theme.fg("dim", " press ctrl+c again to exit a1")
|
|
144
159
|
: renderShortcutHintsWithClose(this.#hintEntries(), theme, rect.width, 1);
|
|
@@ -150,11 +165,19 @@ export class ReferenceScreenApp {
|
|
|
150
165
|
return frame.slice(0, rect.height).concat(Array(Math.max(0, rect.height - frame.length)).fill(""));
|
|
151
166
|
}
|
|
152
167
|
onInput(data, host) {
|
|
168
|
+
const explicitCopy = data === "\u0003" ? this.#selection?.handleInput(data) : undefined;
|
|
169
|
+
if (explicitCopy?.consumed === true)
|
|
170
|
+
return { consumed: true };
|
|
153
171
|
const action = REFERENCE_SCREEN_SHORTCUTS.resolve(KEYS[data] ?? data, SCOPE);
|
|
154
|
-
if (action === null)
|
|
172
|
+
if (action === null) {
|
|
173
|
+
const cleared = explicitCopy?.cleared ?? (this.#selection?.handleInput(data).cleared === true);
|
|
174
|
+
if (cleared)
|
|
175
|
+
host.requestRender();
|
|
155
176
|
return { consumed: false };
|
|
177
|
+
}
|
|
156
178
|
switch (action) {
|
|
157
179
|
case "close":
|
|
180
|
+
this.#selection?.handleInput(data);
|
|
158
181
|
if (!this.#pinned)
|
|
159
182
|
host.close();
|
|
160
183
|
return { consumed: true };
|
|
@@ -165,10 +188,10 @@ export class ReferenceScreenApp {
|
|
|
165
188
|
this.#scrollBy(1);
|
|
166
189
|
return { consumed: true };
|
|
167
190
|
case "page-up":
|
|
168
|
-
this.#scrollBy(-Math.max(1,
|
|
191
|
+
this.#scrollBy(-Math.max(1, host.getSize().height - HEADER_ROWS - FOOTER_ROWS));
|
|
169
192
|
return { consumed: true };
|
|
170
193
|
case "page-down":
|
|
171
|
-
this.#scrollBy(Math.max(1,
|
|
194
|
+
this.#scrollBy(Math.max(1, host.getSize().height - HEADER_ROWS - FOOTER_ROWS));
|
|
172
195
|
return { consumed: true };
|
|
173
196
|
case "first":
|
|
174
197
|
this.#scroll = 0;
|
|
@@ -178,17 +201,25 @@ export class ReferenceScreenApp {
|
|
|
178
201
|
return { consumed: true };
|
|
179
202
|
}
|
|
180
203
|
}
|
|
181
|
-
onMouse(event,
|
|
204
|
+
onMouse(event, host) {
|
|
182
205
|
if (event.kind === "wheel-up" || event.kind === "wheel-down") {
|
|
183
206
|
// Invariant: the whole screen owns wheel scrolling; it must not depend on the pointer's row.
|
|
184
207
|
const distance = scrollbarWheelRows(this.#scrollSettings().scrollbarSpeed);
|
|
185
208
|
this.#scrollBy(event.kind === "wheel-down" ? distance : -distance);
|
|
186
209
|
return { consumed: true };
|
|
187
210
|
}
|
|
211
|
+
if (this.#selection?.selecting === true) {
|
|
212
|
+
const selection = this.#selection.handleMouse(event, () => host.requestRender());
|
|
213
|
+
if (selection.consumed)
|
|
214
|
+
return { consumed: true, render: selection.changed };
|
|
215
|
+
}
|
|
188
216
|
const rail = this.#railPointer(event);
|
|
189
217
|
if (rail.owned)
|
|
190
218
|
return { consumed: true };
|
|
191
|
-
|
|
219
|
+
const selection = this.#selection?.handleMouse(event, () => host.requestRender());
|
|
220
|
+
if (selection?.consumed === true)
|
|
221
|
+
return { consumed: true, render: selection.changed || rail.changed };
|
|
222
|
+
return { consumed: event.kind !== "motion", render: selection?.changed === true || rail.changed };
|
|
192
223
|
}
|
|
193
224
|
// Rationale: the rail takes its share of a pointer report first: hover, a thumb drag, or a
|
|
194
225
|
// track page. Owned means the document must not see the report; changed means the rail looks different.
|
|
@@ -22,6 +22,7 @@ export declare class SettingsApp implements UiApp {
|
|
|
22
22
|
readonly searchInputFactory: SettingsSearchInputFactory;
|
|
23
23
|
readonly copyText?: (text: string) => void;
|
|
24
24
|
readonly configurePlusMenu?: (() => boolean) | null;
|
|
25
|
+
readonly dialogPanel?: (text: string) => string;
|
|
25
26
|
});
|
|
26
27
|
onActivate(host: AppHostServices): void;
|
|
27
28
|
onClose(_host: AppHostServices): void;
|