@suds-cli/viewport 0.0.1-alpha.0 → 0.1.0-alpha.1

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/dist/index.cjs ADDED
@@ -0,0 +1,308 @@
1
+ 'use strict';
2
+
3
+ var chapstick = require('@suds-cli/chapstick');
4
+ var key = require('@suds-cli/key');
5
+ var tea = require('@suds-cli/tea');
6
+
7
+ // src/model.ts
8
+ var defaultKeyMap = {
9
+ pageDown: key.newBinding({ keys: ["pgdown", " ", "f"] }),
10
+ pageUp: key.newBinding({ keys: ["pgup", "b"] }),
11
+ halfPageDown: key.newBinding({ keys: ["d", "ctrl+d"] }),
12
+ halfPageUp: key.newBinding({ keys: ["u", "ctrl+u"] }),
13
+ down: key.newBinding({ keys: ["down", "j"] }),
14
+ up: key.newBinding({ keys: ["up", "k"] })
15
+ };
16
+
17
+ // src/messages.ts
18
+ var SyncMsg = class {
19
+ constructor(lines, topLine, bottomLine) {
20
+ this.lines = lines;
21
+ this.topLine = topLine;
22
+ this.bottomLine = bottomLine;
23
+ }
24
+ _tag = "viewport-sync";
25
+ };
26
+ var ScrollMsg = class {
27
+ constructor(percent, topLine) {
28
+ this.percent = percent;
29
+ this.topLine = topLine;
30
+ }
31
+ _tag = "viewport-scroll";
32
+ };
33
+
34
+ // src/model.ts
35
+ var ViewportModel = class _ViewportModel {
36
+ width;
37
+ height;
38
+ keyMap;
39
+ mouseWheelEnabled;
40
+ mouseWheelDelta;
41
+ yOffset;
42
+ lines;
43
+ style;
44
+ constructor(options) {
45
+ this.width = Math.max(0, options.width);
46
+ this.height = Math.max(0, options.height);
47
+ this.keyMap = options.keyMap;
48
+ this.mouseWheelEnabled = options.mouseWheelEnabled;
49
+ this.mouseWheelDelta = Math.max(0, options.mouseWheelDelta);
50
+ this.yOffset = Math.max(0, options.yOffset);
51
+ this.lines = options.lines;
52
+ this.style = options.style;
53
+ }
54
+ /** Create a new viewport with defaults. */
55
+ static new(options = {}) {
56
+ return new _ViewportModel({
57
+ width: options.width ?? 0,
58
+ height: options.height ?? 0,
59
+ keyMap: options.keyMap ?? defaultKeyMap,
60
+ mouseWheelEnabled: options.mouseWheelEnabled ?? true,
61
+ mouseWheelDelta: options.mouseWheelDelta ?? 3,
62
+ yOffset: 0,
63
+ lines: [],
64
+ style: options.style ?? new chapstick.Style()
65
+ });
66
+ }
67
+ /** Split string content into lines and store it. */
68
+ setContent(content) {
69
+ const normalized = content.replace(/\r\n/g, "\n");
70
+ return this.setContentLines(normalized.split("\n"));
71
+ }
72
+ /** Replace content with a line array. Y offset is clamped if needed. */
73
+ setContentLines(lines) {
74
+ const copy = [...lines];
75
+ const nextOffset = this.clampOffset(this.yOffset, copy, this.height);
76
+ return this.with({ lines: copy, yOffset: nextOffset });
77
+ }
78
+ /** Change width. */
79
+ setWidth(width) {
80
+ if (width === this.width) return this;
81
+ return this.with({ width: Math.max(0, width) });
82
+ }
83
+ /** Change height (and clamp offset if new height is smaller). */
84
+ setHeight(height) {
85
+ const normalized = Math.max(0, height);
86
+ if (normalized === this.height) return this;
87
+ const nextOffset = this.clampOffset(this.yOffset, this.lines, normalized);
88
+ return this.with({ height: normalized, yOffset: nextOffset });
89
+ }
90
+ /** Scroll upward by the given number of lines. */
91
+ scrollUp(lines) {
92
+ if (lines <= 0) return this;
93
+ const nextOffset = Math.max(0, this.yOffset - lines);
94
+ if (nextOffset === this.yOffset) return this;
95
+ return this.with({ yOffset: nextOffset });
96
+ }
97
+ /** Scroll downward by the given number of lines. */
98
+ scrollDown(lines) {
99
+ if (lines <= 0) return this;
100
+ const maxOffset = this.maxYOffset();
101
+ const nextOffset = Math.min(this.yOffset + lines, maxOffset);
102
+ if (nextOffset === this.yOffset) return this;
103
+ return this.with({ yOffset: nextOffset });
104
+ }
105
+ /** Jump to the top. */
106
+ scrollToTop() {
107
+ if (this.atTop()) return this;
108
+ return this.with({ yOffset: 0 });
109
+ }
110
+ /** Jump to the bottom. */
111
+ scrollToBottom() {
112
+ const maxOffset = this.maxYOffset();
113
+ if (this.yOffset === maxOffset) return this;
114
+ return this.with({ yOffset: maxOffset });
115
+ }
116
+ scrollPercent(percent) {
117
+ if (percent === void 0) {
118
+ if (this.height === 0 || this.lines.length === 0) {
119
+ return 1;
120
+ }
121
+ if (this.lines.length <= this.height) {
122
+ return 1;
123
+ }
124
+ const denom = Math.max(1, this.lines.length - this.height);
125
+ return clamp01(this.yOffset / denom);
126
+ }
127
+ const clamped = clamp01(percent);
128
+ const maxOffset = this.maxYOffset();
129
+ const targetOffset = Math.round(maxOffset * clamped);
130
+ if (targetOffset === this.yOffset) return this;
131
+ return this.with({ yOffset: targetOffset });
132
+ }
133
+ /** True when Y offset is at the top. */
134
+ atTop() {
135
+ return this.yOffset <= 0;
136
+ }
137
+ /** True when Y offset is at the bottom. */
138
+ atBottom() {
139
+ return this.yOffset >= this.maxYOffset();
140
+ }
141
+ /** Number of visible lines (without padding). */
142
+ visibleLineCount() {
143
+ return Math.max(0, Math.min(this.height, this.lines.length - this.yOffset));
144
+ }
145
+ /** Total number of content lines. */
146
+ totalLineCount() {
147
+ return this.lines.length;
148
+ }
149
+ /** Move up by n lines (default: 1). */
150
+ lineUp(n = 1) {
151
+ return this.scrollUp(Math.max(1, n));
152
+ }
153
+ /** Move down by n lines (default: 1). */
154
+ lineDown(n = 1) {
155
+ return this.scrollDown(Math.max(1, n));
156
+ }
157
+ /** Move up by half the viewport height. */
158
+ halfViewUp() {
159
+ const delta = Math.max(1, Math.floor(this.height / 2));
160
+ return this.scrollUp(delta);
161
+ }
162
+ /** Move down by half the viewport height. */
163
+ halfViewDown() {
164
+ const delta = Math.max(1, Math.floor(this.height / 2));
165
+ return this.scrollDown(delta);
166
+ }
167
+ /** Move up by one viewport height. */
168
+ viewUp() {
169
+ return this.scrollUp(this.height);
170
+ }
171
+ /** Move down by one viewport height. */
172
+ viewDown() {
173
+ return this.scrollDown(this.height);
174
+ }
175
+ /** Go to a specific line (0-based). Optionally center it. */
176
+ gotoLine(line, centered = false) {
177
+ if (this.lines.length === 0 || this.height === 0) {
178
+ return this;
179
+ }
180
+ const maxIndex = Math.max(0, this.lines.length - 1);
181
+ const targetLine = clamp(Math.floor(line), 0, maxIndex);
182
+ const maxOffset = this.maxYOffset();
183
+ let yOffset = targetLine;
184
+ if (centered) {
185
+ yOffset = targetLine - Math.floor((this.height - 1) / 2);
186
+ }
187
+ const clamped = clamp(yOffset, 0, maxOffset);
188
+ if (clamped === this.yOffset) return this;
189
+ return this.with({ yOffset: clamped });
190
+ }
191
+ /** Tea init hook (no-op). */
192
+ init() {
193
+ return null;
194
+ }
195
+ /** Handle key + mouse scrolling. */
196
+ update(msg) {
197
+ if (msg instanceof tea.MouseMsg) {
198
+ return this.handleMouse(msg);
199
+ }
200
+ if (msg instanceof tea.KeyMsg) {
201
+ return this.handleKey(msg);
202
+ }
203
+ return [this, null];
204
+ }
205
+ /** Render the visible window as a string (padded to height). */
206
+ view() {
207
+ return this.style.render(this.visibleLines().join("\n"));
208
+ }
209
+ /** Build a SyncMsg for high-level renderers. */
210
+ sync() {
211
+ const topLine = this.yOffset;
212
+ const bottomLine = clamp(
213
+ this.yOffset + this.height - 1,
214
+ topLine,
215
+ Math.max(topLine, this.lines.length - 1)
216
+ );
217
+ return () => new SyncMsg(this.visibleLines(), topLine, bottomLine);
218
+ }
219
+ handleKey(msg) {
220
+ if (key.matches(msg, this.keyMap.down)) {
221
+ const next = this.lineDown();
222
+ return [next, next.scrollCmdIfChanged(this)];
223
+ }
224
+ if (key.matches(msg, this.keyMap.up)) {
225
+ const next = this.lineUp();
226
+ return [next, next.scrollCmdIfChanged(this)];
227
+ }
228
+ if (key.matches(msg, this.keyMap.pageDown)) {
229
+ const next = this.viewDown();
230
+ return [next, next.scrollCmdIfChanged(this)];
231
+ }
232
+ if (key.matches(msg, this.keyMap.pageUp)) {
233
+ const next = this.viewUp();
234
+ return [next, next.scrollCmdIfChanged(this)];
235
+ }
236
+ if (key.matches(msg, this.keyMap.halfPageDown)) {
237
+ const next = this.halfViewDown();
238
+ return [next, next.scrollCmdIfChanged(this)];
239
+ }
240
+ if (key.matches(msg, this.keyMap.halfPageUp)) {
241
+ const next = this.halfViewUp();
242
+ return [next, next.scrollCmdIfChanged(this)];
243
+ }
244
+ return [this, null];
245
+ }
246
+ handleMouse(msg) {
247
+ if (!this.mouseWheelEnabled || msg.event.action !== tea.MouseAction.Press || this.height === 0) {
248
+ return [this, null];
249
+ }
250
+ if (msg.event.button === tea.MouseButton.WheelUp) {
251
+ const next = this.scrollUp(this.mouseWheelDelta);
252
+ return [next, next.scrollCmdIfChanged(this)];
253
+ }
254
+ if (msg.event.button === tea.MouseButton.WheelDown) {
255
+ const next = this.scrollDown(this.mouseWheelDelta);
256
+ return [next, next.scrollCmdIfChanged(this)];
257
+ }
258
+ return [this, null];
259
+ }
260
+ visibleLines() {
261
+ const start = this.yOffset;
262
+ const end = Math.min(start + this.height, this.lines.length);
263
+ const window = this.lines.slice(start, end);
264
+ while (window.length < this.height) {
265
+ window.push("");
266
+ }
267
+ return window;
268
+ }
269
+ maxYOffset(lines = this.lines, height = this.height) {
270
+ return Math.max(0, lines.length - height);
271
+ }
272
+ clampOffset(offset, lines, height) {
273
+ return clamp(offset, 0, this.maxYOffset(lines, height));
274
+ }
275
+ with(patch) {
276
+ return new _ViewportModel({
277
+ width: patch.width ?? this.width,
278
+ height: patch.height ?? this.height,
279
+ keyMap: patch.keyMap ?? this.keyMap,
280
+ mouseWheelEnabled: patch.mouseWheelEnabled ?? this.mouseWheelEnabled,
281
+ mouseWheelDelta: patch.mouseWheelDelta ?? this.mouseWheelDelta,
282
+ yOffset: patch.yOffset ?? this.yOffset,
283
+ lines: patch.lines ?? this.lines,
284
+ style: patch.style ?? this.style
285
+ });
286
+ }
287
+ scrollCmdIfChanged(previous) {
288
+ if (this === previous || this.yOffset === previous.yOffset) {
289
+ return null;
290
+ }
291
+ return () => new ScrollMsg(this.scrollPercent(), this.yOffset);
292
+ }
293
+ };
294
+ function clamp(value, min, max) {
295
+ if (max < min) return min;
296
+ return Math.min(max, Math.max(min, value));
297
+ }
298
+ function clamp01(value) {
299
+ if (Number.isNaN(value)) return 0;
300
+ return clamp(value, 0, 1);
301
+ }
302
+
303
+ exports.ScrollMsg = ScrollMsg;
304
+ exports.SyncMsg = SyncMsg;
305
+ exports.ViewportModel = ViewportModel;
306
+ exports.defaultKeyMap = defaultKeyMap;
307
+ //# sourceMappingURL=index.cjs.map
308
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/keymap.ts","../src/messages.ts","../src/model.ts"],"names":["newBinding","Style","MouseMsg","KeyMsg","matches","MouseAction","MouseButton"],"mappings":"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","file":"index.cjs","sourcesContent":["import { Binding, newBinding } from '@suds-cli/key'\n\n/**\n * Key bindings for viewport navigation.\n * @public\n */\nexport interface ViewportKeyMap {\n pageDown: Binding\n pageUp: Binding\n halfPageDown: Binding\n halfPageUp: Binding\n down: Binding\n up: Binding\n}\n\n/**\n * Pager-like default key bindings.\n * @public\n */\nexport const defaultKeyMap: ViewportKeyMap = {\n pageDown: newBinding({ keys: ['pgdown', ' ', 'f'] }),\n pageUp: newBinding({ keys: ['pgup', 'b'] }),\n halfPageDown: newBinding({ keys: ['d', 'ctrl+d'] }),\n halfPageUp: newBinding({ keys: ['u', 'ctrl+u'] }),\n down: newBinding({ keys: ['down', 'j'] }),\n up: newBinding({ keys: ['up', 'k'] }),\n}\n","/**\n * Sync viewport lines when content changes.\n * @public\n */\nexport class SyncMsg {\n readonly _tag = 'viewport-sync'\n\n constructor(\n public readonly lines: string[],\n public readonly topLine: number,\n public readonly bottomLine: number,\n ) {}\n}\n\n/**\n * Viewport scrolled notification.\n * @public\n */\nexport class ScrollMsg {\n readonly _tag = 'viewport-scroll'\n\n constructor(\n public readonly percent: number,\n public readonly topLine: number,\n ) {}\n}\n","import { Style } from '@suds-cli/chapstick'\nimport { matches } from '@suds-cli/key'\nimport {\n KeyMsg,\n MouseAction,\n MouseButton,\n MouseMsg,\n type Cmd,\n type Msg,\n} from '@suds-cli/tea'\nimport { defaultKeyMap, type ViewportKeyMap } from './keymap.js'\nimport { ScrollMsg, SyncMsg } from './messages.js'\n\n/**\n * Options for creating a viewport.\n * @public\n */\nexport interface ViewportOptions {\n width?: number\n height?: number\n keyMap?: ViewportKeyMap\n mouseWheelEnabled?: boolean\n mouseWheelDelta?: number\n style?: Style\n}\n\n/**\n * Scrollable window onto a list of lines.\n * @public\n */\nexport class ViewportModel {\n readonly width: number\n readonly height: number\n readonly keyMap: ViewportKeyMap\n readonly mouseWheelEnabled: boolean\n readonly mouseWheelDelta: number\n readonly yOffset: number\n readonly lines: string[]\n readonly style: Style\n\n private constructor(options: {\n width: number\n height: number\n keyMap: ViewportKeyMap\n mouseWheelEnabled: boolean\n mouseWheelDelta: number\n yOffset: number\n lines: string[]\n style: Style\n }) {\n this.width = Math.max(0, options.width)\n this.height = Math.max(0, options.height)\n this.keyMap = options.keyMap\n this.mouseWheelEnabled = options.mouseWheelEnabled\n this.mouseWheelDelta = Math.max(0, options.mouseWheelDelta)\n this.yOffset = Math.max(0, options.yOffset)\n this.lines = options.lines\n this.style = options.style\n }\n\n /** Create a new viewport with defaults. */\n static new(options: ViewportOptions = {}): ViewportModel {\n return new ViewportModel({\n width: options.width ?? 0,\n height: options.height ?? 0,\n keyMap: options.keyMap ?? defaultKeyMap,\n mouseWheelEnabled: options.mouseWheelEnabled ?? true,\n mouseWheelDelta: options.mouseWheelDelta ?? 3,\n yOffset: 0,\n lines: [],\n style: options.style ?? new Style(),\n })\n }\n\n /** Split string content into lines and store it. */\n setContent(content: string): ViewportModel {\n const normalized = content.replace(/\\r\\n/g, '\\n')\n return this.setContentLines(normalized.split('\\n'))\n }\n\n /** Replace content with a line array. Y offset is clamped if needed. */\n setContentLines(lines: string[]): ViewportModel {\n const copy = [...lines]\n const nextOffset = this.clampOffset(this.yOffset, copy, this.height)\n return this.with({ lines: copy, yOffset: nextOffset })\n }\n\n /** Change width. */\n setWidth(width: number): ViewportModel {\n if (width === this.width) return this\n return this.with({ width: Math.max(0, width) })\n }\n\n /** Change height (and clamp offset if new height is smaller). */\n setHeight(height: number): ViewportModel {\n const normalized = Math.max(0, height)\n if (normalized === this.height) return this\n const nextOffset = this.clampOffset(this.yOffset, this.lines, normalized)\n return this.with({ height: normalized, yOffset: nextOffset })\n }\n\n /** Scroll upward by the given number of lines. */\n scrollUp(lines: number): ViewportModel {\n if (lines <= 0) return this\n const nextOffset = Math.max(0, this.yOffset - lines)\n if (nextOffset === this.yOffset) return this\n return this.with({ yOffset: nextOffset })\n }\n\n /** Scroll downward by the given number of lines. */\n scrollDown(lines: number): ViewportModel {\n if (lines <= 0) return this\n const maxOffset = this.maxYOffset()\n const nextOffset = Math.min(this.yOffset + lines, maxOffset)\n if (nextOffset === this.yOffset) return this\n return this.with({ yOffset: nextOffset })\n }\n\n /** Jump to the top. */\n scrollToTop(): ViewportModel {\n if (this.atTop()) return this\n return this.with({ yOffset: 0 })\n }\n\n /** Jump to the bottom. */\n scrollToBottom(): ViewportModel {\n const maxOffset = this.maxYOffset()\n if (this.yOffset === maxOffset) return this\n return this.with({ yOffset: maxOffset })\n }\n\n /** Get or set scroll percentage (0-1). */\n scrollPercent(): number\n scrollPercent(percent: number): ViewportModel\n scrollPercent(percent?: number): number | ViewportModel {\n if (percent === undefined) {\n if (this.height === 0 || this.lines.length === 0) {\n return 1\n }\n if (this.lines.length <= this.height) {\n return 1\n }\n const denom = Math.max(1, this.lines.length - this.height)\n return clamp01(this.yOffset / denom)\n }\n const clamped = clamp01(percent)\n const maxOffset = this.maxYOffset()\n const targetOffset = Math.round(maxOffset * clamped)\n if (targetOffset === this.yOffset) return this\n return this.with({ yOffset: targetOffset })\n }\n\n /** True when Y offset is at the top. */\n atTop(): boolean {\n return this.yOffset <= 0\n }\n\n /** True when Y offset is at the bottom. */\n atBottom(): boolean {\n return this.yOffset >= this.maxYOffset()\n }\n\n /** Number of visible lines (without padding). */\n visibleLineCount(): number {\n return Math.max(0, Math.min(this.height, this.lines.length - this.yOffset))\n }\n\n /** Total number of content lines. */\n totalLineCount(): number {\n return this.lines.length\n }\n\n /** Move up by n lines (default: 1). */\n lineUp(n = 1): ViewportModel {\n return this.scrollUp(Math.max(1, n))\n }\n\n /** Move down by n lines (default: 1). */\n lineDown(n = 1): ViewportModel {\n return this.scrollDown(Math.max(1, n))\n }\n\n /** Move up by half the viewport height. */\n halfViewUp(): ViewportModel {\n const delta = Math.max(1, Math.floor(this.height / 2))\n return this.scrollUp(delta)\n }\n\n /** Move down by half the viewport height. */\n halfViewDown(): ViewportModel {\n const delta = Math.max(1, Math.floor(this.height / 2))\n return this.scrollDown(delta)\n }\n\n /** Move up by one viewport height. */\n viewUp(): ViewportModel {\n return this.scrollUp(this.height)\n }\n\n /** Move down by one viewport height. */\n viewDown(): ViewportModel {\n return this.scrollDown(this.height)\n }\n\n /** Go to a specific line (0-based). Optionally center it. */\n gotoLine(line: number, centered = false): ViewportModel {\n if (this.lines.length === 0 || this.height === 0) {\n return this\n }\n const maxIndex = Math.max(0, this.lines.length - 1)\n const targetLine = clamp(Math.floor(line), 0, maxIndex)\n const maxOffset = this.maxYOffset()\n let yOffset = targetLine\n if (centered) {\n yOffset = targetLine - Math.floor((this.height - 1) / 2)\n }\n const clamped = clamp(yOffset, 0, maxOffset)\n if (clamped === this.yOffset) return this\n return this.with({ yOffset: clamped })\n }\n\n /** Tea init hook (no-op). */\n init(): Cmd<Msg> {\n return null\n }\n\n /** Handle key + mouse scrolling. */\n update(msg: Msg): [ViewportModel, Cmd<Msg>] {\n if (msg instanceof MouseMsg) {\n return this.handleMouse(msg)\n }\n if (msg instanceof KeyMsg) {\n return this.handleKey(msg)\n }\n return [this, null]\n }\n\n /** Render the visible window as a string (padded to height). */\n view(): string {\n return this.style.render(this.visibleLines().join('\\n'))\n }\n\n /** Build a SyncMsg for high-level renderers. */\n sync(): Cmd<Msg> {\n const topLine = this.yOffset\n const bottomLine = clamp(\n this.yOffset + this.height - 1,\n topLine,\n Math.max(topLine, this.lines.length - 1),\n )\n return () => new SyncMsg(this.visibleLines(), topLine, bottomLine)\n }\n\n private handleKey(msg: KeyMsg): [ViewportModel, Cmd<Msg>] {\n if (matches(msg, this.keyMap.down)) {\n const next = this.lineDown()\n return [next, next.scrollCmdIfChanged(this)]\n }\n if (matches(msg, this.keyMap.up)) {\n const next = this.lineUp()\n return [next, next.scrollCmdIfChanged(this)]\n }\n if (matches(msg, this.keyMap.pageDown)) {\n const next = this.viewDown()\n return [next, next.scrollCmdIfChanged(this)]\n }\n if (matches(msg, this.keyMap.pageUp)) {\n const next = this.viewUp()\n return [next, next.scrollCmdIfChanged(this)]\n }\n if (matches(msg, this.keyMap.halfPageDown)) {\n const next = this.halfViewDown()\n return [next, next.scrollCmdIfChanged(this)]\n }\n if (matches(msg, this.keyMap.halfPageUp)) {\n const next = this.halfViewUp()\n return [next, next.scrollCmdIfChanged(this)]\n }\n return [this, null]\n }\n\n private handleMouse(msg: MouseMsg): [ViewportModel, Cmd<Msg>] {\n if (\n !this.mouseWheelEnabled ||\n msg.event.action !== MouseAction.Press ||\n this.height === 0\n ) {\n return [this, null]\n }\n\n if (msg.event.button === MouseButton.WheelUp) {\n const next = this.scrollUp(this.mouseWheelDelta)\n return [next, next.scrollCmdIfChanged(this)]\n }\n if (msg.event.button === MouseButton.WheelDown) {\n const next = this.scrollDown(this.mouseWheelDelta)\n return [next, next.scrollCmdIfChanged(this)]\n }\n return [this, null]\n }\n\n private visibleLines(): string[] {\n const start = this.yOffset\n const end = Math.min(start + this.height, this.lines.length)\n const window = this.lines.slice(start, end)\n while (window.length < this.height) {\n window.push('')\n }\n return window\n }\n\n private maxYOffset(lines = this.lines, height = this.height): number {\n return Math.max(0, lines.length - height)\n }\n\n private clampOffset(offset: number, lines: string[], height: number): number {\n return clamp(offset, 0, this.maxYOffset(lines, height))\n }\n\n private with(patch: Partial<ViewportModel>): ViewportModel {\n return new ViewportModel({\n width: patch.width ?? this.width,\n height: patch.height ?? this.height,\n keyMap: patch.keyMap ?? this.keyMap,\n mouseWheelEnabled: patch.mouseWheelEnabled ?? this.mouseWheelEnabled,\n mouseWheelDelta: patch.mouseWheelDelta ?? this.mouseWheelDelta,\n yOffset: patch.yOffset ?? this.yOffset,\n lines: patch.lines ?? this.lines,\n style: patch.style ?? this.style,\n })\n }\n\n private scrollCmdIfChanged(previous: ViewportModel): Cmd<Msg> {\n if (this === previous || this.yOffset === previous.yOffset) {\n return null\n }\n return () => new ScrollMsg(this.scrollPercent(), this.yOffset)\n }\n}\n\nfunction clamp(value: number, min: number, max: number): number {\n if (max < min) return min\n return Math.min(max, Math.max(min, value))\n}\n\nfunction clamp01(value: number): number {\n if (Number.isNaN(value)) return 0\n return clamp(value, 0, 1)\n}\n"]}
@@ -1,135 +1,131 @@
1
- import { Binding } from '@suds-cli/key';
2
- import { Cmd } from '@suds-cli/tea';
3
- import { Msg } from '@suds-cli/tea';
4
- import { Style } from '@suds-cli/chapstick';
5
-
6
- /**
7
- * Pager-like default key bindings.
8
- * @public
9
- */
10
- export declare const defaultKeyMap: ViewportKeyMap;
11
-
12
- /**
13
- * Viewport scrolled notification.
14
- * @public
15
- */
16
- export declare class ScrollMsg {
17
- readonly percent: number;
18
- readonly topLine: number;
19
- readonly _tag = "viewport-scroll";
20
- constructor(percent: number, topLine: number);
21
- }
22
-
23
- /**
24
- * Sync viewport lines when content changes.
25
- * @public
26
- */
27
- export declare class SyncMsg {
28
- readonly lines: string[];
29
- readonly topLine: number;
30
- readonly bottomLine: number;
31
- readonly _tag = "viewport-sync";
32
- constructor(lines: string[], topLine: number, bottomLine: number);
33
- }
34
-
35
- /**
36
- * Key bindings for viewport navigation.
37
- * @public
38
- */
39
- export declare interface ViewportKeyMap {
40
- pageDown: Binding;
41
- pageUp: Binding;
42
- halfPageDown: Binding;
43
- halfPageUp: Binding;
44
- down: Binding;
45
- up: Binding;
46
- }
47
-
48
- /**
49
- * Scrollable window onto a list of lines.
50
- * @public
51
- */
52
- export declare class ViewportModel {
53
- readonly width: number;
54
- readonly height: number;
55
- readonly keyMap: ViewportKeyMap;
56
- readonly mouseWheelEnabled: boolean;
57
- readonly mouseWheelDelta: number;
58
- readonly yOffset: number;
59
- readonly lines: string[];
60
- readonly style: Style;
61
- private constructor();
62
- /** Create a new viewport with defaults. */
63
- static new(options?: ViewportOptions): ViewportModel;
64
- /** Split string content into lines and store it. */
65
- setContent(content: string): ViewportModel;
66
- /** Replace content with a line array. Y offset is clamped if needed. */
67
- setContentLines(lines: string[]): ViewportModel;
68
- /** Change width. */
69
- setWidth(width: number): ViewportModel;
70
- /** Change height (and clamp offset if new height is smaller). */
71
- setHeight(height: number): ViewportModel;
72
- /** Scroll upward by the given number of lines. */
73
- scrollUp(lines: number): ViewportModel;
74
- /** Scroll downward by the given number of lines. */
75
- scrollDown(lines: number): ViewportModel;
76
- /** Jump to the top. */
77
- scrollToTop(): ViewportModel;
78
- /** Jump to the bottom. */
79
- scrollToBottom(): ViewportModel;
80
- /** Get or set scroll percentage (0-1). */
81
- scrollPercent(): number;
82
- scrollPercent(percent: number): ViewportModel;
83
- /** True when Y offset is at the top. */
84
- atTop(): boolean;
85
- /** True when Y offset is at the bottom. */
86
- atBottom(): boolean;
87
- /** Number of visible lines (without padding). */
88
- visibleLineCount(): number;
89
- /** Total number of content lines. */
90
- totalLineCount(): number;
91
- /** Move up by n lines (default: 1). */
92
- lineUp(n?: number): ViewportModel;
93
- /** Move down by n lines (default: 1). */
94
- lineDown(n?: number): ViewportModel;
95
- /** Move up by half the viewport height. */
96
- halfViewUp(): ViewportModel;
97
- /** Move down by half the viewport height. */
98
- halfViewDown(): ViewportModel;
99
- /** Move up by one viewport height. */
100
- viewUp(): ViewportModel;
101
- /** Move down by one viewport height. */
102
- viewDown(): ViewportModel;
103
- /** Go to a specific line (0-based). Optionally center it. */
104
- gotoLine(line: number, centered?: boolean): ViewportModel;
105
- /** Tea init hook (no-op). */
106
- init(): Cmd<Msg>;
107
- /** Handle key + mouse scrolling. */
108
- update(msg: Msg): [ViewportModel, Cmd<Msg>];
109
- /** Render the visible window as a string (padded to height). */
110
- view(): string;
111
- /** Build a SyncMsg for high-level renderers. */
112
- sync(): Cmd<Msg>;
113
- private handleKey;
114
- private handleMouse;
115
- private visibleLines;
116
- private maxYOffset;
117
- private clampOffset;
118
- private with;
119
- private scrollCmdIfChanged;
120
- }
121
-
122
- /**
123
- * Options for creating a viewport.
124
- * @public
125
- */
126
- export declare interface ViewportOptions {
127
- width?: number;
128
- height?: number;
129
- keyMap?: ViewportKeyMap;
130
- mouseWheelEnabled?: boolean;
131
- mouseWheelDelta?: number;
132
- style?: Style;
133
- }
134
-
135
- export { }
1
+ import { Style } from '@suds-cli/chapstick';
2
+ import { Cmd, Msg } from '@suds-cli/tea';
3
+ import { Binding } from '@suds-cli/key';
4
+
5
+ /**
6
+ * Key bindings for viewport navigation.
7
+ * @public
8
+ */
9
+ interface ViewportKeyMap {
10
+ pageDown: Binding;
11
+ pageUp: Binding;
12
+ halfPageDown: Binding;
13
+ halfPageUp: Binding;
14
+ down: Binding;
15
+ up: Binding;
16
+ }
17
+ /**
18
+ * Pager-like default key bindings.
19
+ * @public
20
+ */
21
+ declare const defaultKeyMap: ViewportKeyMap;
22
+
23
+ /**
24
+ * Options for creating a viewport.
25
+ * @public
26
+ */
27
+ interface ViewportOptions {
28
+ width?: number;
29
+ height?: number;
30
+ keyMap?: ViewportKeyMap;
31
+ mouseWheelEnabled?: boolean;
32
+ mouseWheelDelta?: number;
33
+ style?: Style;
34
+ }
35
+ /**
36
+ * Scrollable window onto a list of lines.
37
+ * @public
38
+ */
39
+ declare class ViewportModel {
40
+ readonly width: number;
41
+ readonly height: number;
42
+ readonly keyMap: ViewportKeyMap;
43
+ readonly mouseWheelEnabled: boolean;
44
+ readonly mouseWheelDelta: number;
45
+ readonly yOffset: number;
46
+ readonly lines: string[];
47
+ readonly style: Style;
48
+ private constructor();
49
+ /** Create a new viewport with defaults. */
50
+ static new(options?: ViewportOptions): ViewportModel;
51
+ /** Split string content into lines and store it. */
52
+ setContent(content: string): ViewportModel;
53
+ /** Replace content with a line array. Y offset is clamped if needed. */
54
+ setContentLines(lines: string[]): ViewportModel;
55
+ /** Change width. */
56
+ setWidth(width: number): ViewportModel;
57
+ /** Change height (and clamp offset if new height is smaller). */
58
+ setHeight(height: number): ViewportModel;
59
+ /** Scroll upward by the given number of lines. */
60
+ scrollUp(lines: number): ViewportModel;
61
+ /** Scroll downward by the given number of lines. */
62
+ scrollDown(lines: number): ViewportModel;
63
+ /** Jump to the top. */
64
+ scrollToTop(): ViewportModel;
65
+ /** Jump to the bottom. */
66
+ scrollToBottom(): ViewportModel;
67
+ /** Get or set scroll percentage (0-1). */
68
+ scrollPercent(): number;
69
+ scrollPercent(percent: number): ViewportModel;
70
+ /** True when Y offset is at the top. */
71
+ atTop(): boolean;
72
+ /** True when Y offset is at the bottom. */
73
+ atBottom(): boolean;
74
+ /** Number of visible lines (without padding). */
75
+ visibleLineCount(): number;
76
+ /** Total number of content lines. */
77
+ totalLineCount(): number;
78
+ /** Move up by n lines (default: 1). */
79
+ lineUp(n?: number): ViewportModel;
80
+ /** Move down by n lines (default: 1). */
81
+ lineDown(n?: number): ViewportModel;
82
+ /** Move up by half the viewport height. */
83
+ halfViewUp(): ViewportModel;
84
+ /** Move down by half the viewport height. */
85
+ halfViewDown(): ViewportModel;
86
+ /** Move up by one viewport height. */
87
+ viewUp(): ViewportModel;
88
+ /** Move down by one viewport height. */
89
+ viewDown(): ViewportModel;
90
+ /** Go to a specific line (0-based). Optionally center it. */
91
+ gotoLine(line: number, centered?: boolean): ViewportModel;
92
+ /** Tea init hook (no-op). */
93
+ init(): Cmd<Msg>;
94
+ /** Handle key + mouse scrolling. */
95
+ update(msg: Msg): [ViewportModel, Cmd<Msg>];
96
+ /** Render the visible window as a string (padded to height). */
97
+ view(): string;
98
+ /** Build a SyncMsg for high-level renderers. */
99
+ sync(): Cmd<Msg>;
100
+ private handleKey;
101
+ private handleMouse;
102
+ private visibleLines;
103
+ private maxYOffset;
104
+ private clampOffset;
105
+ private with;
106
+ private scrollCmdIfChanged;
107
+ }
108
+
109
+ /**
110
+ * Sync viewport lines when content changes.
111
+ * @public
112
+ */
113
+ declare class SyncMsg {
114
+ readonly lines: string[];
115
+ readonly topLine: number;
116
+ readonly bottomLine: number;
117
+ readonly _tag = "viewport-sync";
118
+ constructor(lines: string[], topLine: number, bottomLine: number);
119
+ }
120
+ /**
121
+ * Viewport scrolled notification.
122
+ * @public
123
+ */
124
+ declare class ScrollMsg {
125
+ readonly percent: number;
126
+ readonly topLine: number;
127
+ readonly _tag = "viewport-scroll";
128
+ constructor(percent: number, topLine: number);
129
+ }
130
+
131
+ export { ScrollMsg, SyncMsg, type ViewportKeyMap, ViewportModel, type ViewportOptions, defaultKeyMap };