@linxiraos/pi-tui 1.0.0

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.
Files changed (77) hide show
  1. package/CHANGELOG.md +2219 -0
  2. package/README.md +705 -0
  3. package/dist/types/autocomplete.d.ts +116 -0
  4. package/dist/types/bracketed-paste.d.ts +51 -0
  5. package/dist/types/components/box.d.ts +31 -0
  6. package/dist/types/components/cancellable-loader.d.ts +21 -0
  7. package/dist/types/components/editor.d.ts +162 -0
  8. package/dist/types/components/image.d.ts +112 -0
  9. package/dist/types/components/input.d.ts +25 -0
  10. package/dist/types/components/loader.d.ts +25 -0
  11. package/dist/types/components/markdown.d.ts +88 -0
  12. package/dist/types/components/scroll-view.d.ts +62 -0
  13. package/dist/types/components/select-list.d.ts +69 -0
  14. package/dist/types/components/settings-list.d.ts +123 -0
  15. package/dist/types/components/spacer.d.ts +11 -0
  16. package/dist/types/components/tab-bar.d.ts +89 -0
  17. package/dist/types/components/text.d.ts +27 -0
  18. package/dist/types/components/truncated-text.d.ts +10 -0
  19. package/dist/types/deccara.d.ts +49 -0
  20. package/dist/types/desktop-notify.d.ts +52 -0
  21. package/dist/types/editor-component.d.ts +38 -0
  22. package/dist/types/fuzzy.d.ts +48 -0
  23. package/dist/types/index.d.ts +32 -0
  24. package/dist/types/keybindings.d.ts +197 -0
  25. package/dist/types/keys.d.ts +210 -0
  26. package/dist/types/kill-ring.d.ts +20 -0
  27. package/dist/types/kitty-graphics.d.ts +76 -0
  28. package/dist/types/latex-block.d.ts +8 -0
  29. package/dist/types/latex-to-unicode.d.ts +50 -0
  30. package/dist/types/loop-watchdog.d.ts +44 -0
  31. package/dist/types/mouse.d.ts +67 -0
  32. package/dist/types/stdin-buffer.d.ts +60 -0
  33. package/dist/types/symbols.d.ts +25 -0
  34. package/dist/types/terminal-capabilities.d.ts +285 -0
  35. package/dist/types/terminal.d.ts +175 -0
  36. package/dist/types/tmux.d.ts +6 -0
  37. package/dist/types/ttyid.d.ts +9 -0
  38. package/dist/types/tui.d.ts +457 -0
  39. package/dist/types/utils.d.ts +100 -0
  40. package/package.json +70 -0
  41. package/src/autocomplete.ts +1079 -0
  42. package/src/bracketed-paste.ts +123 -0
  43. package/src/components/box.ts +236 -0
  44. package/src/components/cancellable-loader.ts +40 -0
  45. package/src/components/editor.ts +3301 -0
  46. package/src/components/image.ts +460 -0
  47. package/src/components/input.ts +482 -0
  48. package/src/components/loader.ts +174 -0
  49. package/src/components/markdown.ts +3119 -0
  50. package/src/components/scroll-view.ts +227 -0
  51. package/src/components/select-list.ts +539 -0
  52. package/src/components/settings-list.ts +793 -0
  53. package/src/components/spacer.ts +32 -0
  54. package/src/components/tab-bar.ts +300 -0
  55. package/src/components/text.ts +173 -0
  56. package/src/components/truncated-text.ts +69 -0
  57. package/src/deccara.ts +314 -0
  58. package/src/desktop-notify.ts +192 -0
  59. package/src/editor-component.ts +74 -0
  60. package/src/fuzzy.ts +384 -0
  61. package/src/index.ts +51 -0
  62. package/src/keybindings.ts +346 -0
  63. package/src/keys.ts +566 -0
  64. package/src/kill-ring.ts +51 -0
  65. package/src/kitty-graphics.ts +171 -0
  66. package/src/latex-block.ts +1338 -0
  67. package/src/latex-to-unicode.ts +2017 -0
  68. package/src/loop-watchdog.ts +115 -0
  69. package/src/mouse.ts +105 -0
  70. package/src/stdin-buffer.ts +781 -0
  71. package/src/symbols.ts +26 -0
  72. package/src/terminal-capabilities.ts +1211 -0
  73. package/src/terminal.ts +1854 -0
  74. package/src/tmux.ts +14 -0
  75. package/src/ttyid.ts +84 -0
  76. package/src/tui.ts +4275 -0
  77. package/src/utils.ts +619 -0
@@ -0,0 +1,227 @@
1
+ import { matchesKey } from "../keys";
2
+ import type { Component } from "../tui";
3
+ import { Ellipsis, replaceTabs, truncateToWidth, visibleWidth } from "../utils";
4
+
5
+ const DEFAULT_TRACK = "│";
6
+ const DEFAULT_THUMB = "█";
7
+
8
+ type ScrollbarMode = "auto" | "always" | "never";
9
+
10
+ export interface ScrollViewTheme {
11
+ track?: (text: string) => string;
12
+ thumb?: (text: string) => string;
13
+ }
14
+
15
+ export interface ScrollViewOptions {
16
+ height: number;
17
+ /** Defaults to "auto". "auto" reserves a scrollbar column only when content overflows. */
18
+ scrollbar?: ScrollbarMode | boolean;
19
+ /** Logical row count for pre-windowed line slices. Defaults to lines.length. */
20
+ totalRows?: number;
21
+ theme?: ScrollViewTheme;
22
+ trackChar?: string;
23
+ thumbChar?: string;
24
+ /**
25
+ * Indicator appended when a row overflows `contentWidth`. Defaults to
26
+ * {@link Ellipsis.Unicode}. Pass {@link Ellipsis.Omit} when callers wrap
27
+ * lines to width themselves and only trailing padding can overflow (e.g.
28
+ * the plan-review overlay), so no stray `…` lands on every padded row.
29
+ */
30
+ ellipsis?: Ellipsis;
31
+ /**
32
+ * Rows moved per keystroke when {@link ScrollView.handleScrollKey} sees a
33
+ * Shift+Arrow (the "scroll faster" affordance). Defaults to 5.
34
+ */
35
+ fastScrollLines?: number;
36
+ }
37
+
38
+ function normalizeScrollbarMode(scrollbar: ScrollViewOptions["scrollbar"]): ScrollbarMode {
39
+ if (scrollbar === true) return "auto";
40
+ if (scrollbar === false) return "never";
41
+ return scrollbar ?? "auto";
42
+ }
43
+
44
+ function firstCellGlyph(value: string, fallback: string): string {
45
+ const glyph = Array.from(value)[0] ?? fallback;
46
+ return visibleWidth(glyph) === 1 ? glyph : fallback;
47
+ }
48
+
49
+ /**
50
+ * Fixed-height viewport over pre-rendered lines, with optional right-edge scrollbar.
51
+ *
52
+ * ScrollView owns only the row offset. Callers remain responsible for producing
53
+ * already-wrapped logical lines appropriate for the current render width.
54
+ */
55
+ export class ScrollView implements Component {
56
+ #lines: string[];
57
+ #height: number;
58
+ #scrollOffset = 0;
59
+ #totalRows: number | undefined;
60
+ #scrollbar: ScrollbarMode;
61
+ #theme: Required<ScrollViewTheme>;
62
+ #trackChar: string;
63
+ #thumbChar: string;
64
+ #ellipsis: Ellipsis;
65
+ #fastScrollLines: number;
66
+
67
+ constructor(lines: readonly string[], options: ScrollViewOptions) {
68
+ this.#lines = [...lines];
69
+ this.#height = Number.isFinite(options.height) ? Math.max(0, Math.trunc(options.height)) : 0;
70
+ this.#totalRows = options.totalRows === undefined ? undefined : Math.max(0, Math.trunc(options.totalRows));
71
+ this.#scrollbar = normalizeScrollbarMode(options.scrollbar);
72
+ this.#theme = {
73
+ track: options.theme?.track ?? (text => text),
74
+ thumb: options.theme?.thumb ?? (text => text),
75
+ };
76
+ this.#trackChar = firstCellGlyph(options.trackChar ?? DEFAULT_TRACK, DEFAULT_TRACK);
77
+ this.#thumbChar = firstCellGlyph(options.thumbChar ?? DEFAULT_THUMB, DEFAULT_THUMB);
78
+ this.#ellipsis = options.ellipsis ?? Ellipsis.Unicode;
79
+ this.#fastScrollLines = Math.max(1, Math.trunc(options.fastScrollLines ?? 5));
80
+ this.#clampScrollOffset();
81
+ }
82
+
83
+ setLines(lines: readonly string[]): void {
84
+ this.#lines = [...lines];
85
+ this.#clampScrollOffset();
86
+ }
87
+
88
+ setTotalRows(totalRows: number | undefined): void {
89
+ this.#totalRows = totalRows === undefined ? undefined : Math.max(0, Math.trunc(totalRows));
90
+ this.#clampScrollOffset();
91
+ }
92
+
93
+ setHeight(height: number): void {
94
+ this.#height = Number.isFinite(height) ? Math.max(0, Math.trunc(height)) : 0;
95
+ this.#clampScrollOffset();
96
+ }
97
+
98
+ setScrollbar(scrollbar: ScrollViewOptions["scrollbar"]): void {
99
+ this.#scrollbar = normalizeScrollbarMode(scrollbar);
100
+ }
101
+
102
+ getScrollOffset(): number {
103
+ return this.#scrollOffset;
104
+ }
105
+
106
+ getMaxScrollOffset(): number {
107
+ const rowCount = this.#totalRows ?? this.#lines.length;
108
+ return Math.max(0, rowCount - this.#height);
109
+ }
110
+
111
+ setScrollOffset(offset: number): void {
112
+ this.#scrollOffset = Number.isFinite(offset) ? Math.trunc(offset) : 0;
113
+ this.#clampScrollOffset();
114
+ }
115
+
116
+ scroll(delta: number): void {
117
+ this.setScrollOffset(this.#scrollOffset + (Number.isFinite(delta) ? Math.trunc(delta) : 0));
118
+ }
119
+
120
+ page(delta: number): void {
121
+ const step = Math.max(1, this.#height - 1);
122
+ this.scroll(step * (Number.isFinite(delta) ? Math.trunc(delta) : 0));
123
+ }
124
+
125
+ scrollToTop(): void {
126
+ this.#scrollOffset = 0;
127
+ }
128
+
129
+ scrollToBottom(): void {
130
+ this.#scrollOffset = this.getMaxScrollOffset();
131
+ }
132
+
133
+ /**
134
+ * Apply a standard navigation key to the viewport. Shift+Arrow scrolls by
135
+ * {@link ScrollViewOptions.fastScrollLines} (the "scroll faster" affordance);
136
+ * plain Arrow by one line; PageUp/PageDown by a page; Home/End to the ends.
137
+ * Returns true when the key was consumed, so callers can fall through to
138
+ * their own (e.g. vim-style) bindings. Generic on purpose: every ScrollView
139
+ * consumer gets the same scroll keys, including Shift-to-go-faster.
140
+ */
141
+ handleScrollKey(data: string): boolean {
142
+ if (matchesKey(data, "shift+up")) {
143
+ this.scroll(-this.#fastScrollLines);
144
+ return true;
145
+ }
146
+ if (matchesKey(data, "shift+down")) {
147
+ this.scroll(this.#fastScrollLines);
148
+ return true;
149
+ }
150
+ if (matchesKey(data, "up")) {
151
+ this.scroll(-1);
152
+ return true;
153
+ }
154
+ if (matchesKey(data, "down")) {
155
+ this.scroll(1);
156
+ return true;
157
+ }
158
+ if (matchesKey(data, "pageUp")) {
159
+ this.page(-1);
160
+ return true;
161
+ }
162
+ if (matchesKey(data, "pageDown")) {
163
+ this.page(1);
164
+ return true;
165
+ }
166
+ if (matchesKey(data, "home")) {
167
+ this.scrollToTop();
168
+ return true;
169
+ }
170
+ if (matchesKey(data, "end")) {
171
+ this.scrollToBottom();
172
+ return true;
173
+ }
174
+ return false;
175
+ }
176
+
177
+ invalidate(): void {
178
+ // No cached layout to invalidate.
179
+ }
180
+
181
+ render(width: number): readonly string[] {
182
+ this.#clampScrollOffset();
183
+ const safeWidth = Number.isFinite(width) ? Math.max(0, Math.trunc(width)) : 0;
184
+ if (this.#height === 0) return [];
185
+ const showScrollbar = safeWidth > 0 && this.#shouldRenderScrollbar();
186
+ const contentWidth = Math.max(0, safeWidth - (showScrollbar ? 1 : 0));
187
+ const thumb = showScrollbar ? this.#thumbRange() : undefined;
188
+ const lines: string[] = [];
189
+ for (let row = 0; row < this.#height; row++) {
190
+ const sourceIndex = this.#totalRows === undefined ? this.#scrollOffset + row : row;
191
+ const source = this.#lines[sourceIndex] ?? "";
192
+ const truncated = truncateToWidth(replaceTabs(source), contentWidth, this.#ellipsis);
193
+ if (!showScrollbar) {
194
+ lines.push(truncated);
195
+ continue;
196
+ }
197
+ const content = `${truncated}${" ".repeat(Math.max(0, contentWidth - visibleWidth(truncated)))}`;
198
+ const barGlyph = thumb && row >= thumb.start && row < thumb.end ? this.#thumbChar : this.#trackChar;
199
+ const styledBar =
200
+ thumb && row >= thumb.start && row < thumb.end ? this.#theme.thumb(barGlyph) : this.#theme.track(barGlyph);
201
+ lines.push(`${content}${styledBar}`);
202
+ }
203
+ return lines;
204
+ }
205
+
206
+ #clampScrollOffset(): void {
207
+ this.#scrollOffset = Math.max(0, Math.min(this.#scrollOffset, this.getMaxScrollOffset()));
208
+ }
209
+
210
+ #shouldRenderScrollbar(): boolean {
211
+ if (this.#height <= 0) return false;
212
+ if (this.#scrollbar === "never") return false;
213
+ if (this.#scrollbar === "always") return true;
214
+ return (this.#totalRows ?? this.#lines.length) > this.#height;
215
+ }
216
+
217
+ #thumbRange(): { start: number; end: number } {
218
+ if (this.#height <= 0) return { start: 0, end: 0 };
219
+ const rowCount = this.#totalRows ?? this.#lines.length;
220
+ if (rowCount <= this.#height) return { start: 0, end: this.#height };
221
+ const thumbSize = Math.max(1, Math.min(Math.floor((this.#height * this.#height) / rowCount), this.#height));
222
+ const travel = this.#height - thumbSize;
223
+ const maxOffset = this.getMaxScrollOffset();
224
+ const start = maxOffset === 0 ? 0 : Math.round((this.#scrollOffset / maxOffset) * travel);
225
+ return { start, end: start + thumbSize };
226
+ }
227
+ }