@pi9/ask 0.1.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.
- package/LICENSE +21 -0
- package/README.md +92 -0
- package/package.json +66 -0
- package/src/component.ts +570 -0
- package/src/config.ts +30 -0
- package/src/context.ts +209 -0
- package/src/deadline.ts +78 -0
- package/src/glyphs.ts +4 -0
- package/src/index.ts +231 -0
- package/src/preview.ts +55 -0
- package/src/questionnaire.ts +37 -0
- package/src/replay-renderer.ts +85 -0
- package/src/replay.ts +100 -0
- package/src/response.ts +31 -0
- package/src/rpc.ts +133 -0
- package/src/schema.ts +43 -0
- package/src/state.ts +203 -0
- package/src/types.ts +33 -0
- package/src/validation.ts +48 -0
- package/src/viewport.ts +148 -0
package/src/viewport.ts
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
export interface FocusRange {
|
|
2
|
+
/** Inclusive absolute row index in the full logical row list. */
|
|
3
|
+
start: number;
|
|
4
|
+
/** Exclusive absolute row index in the full logical row list. */
|
|
5
|
+
end: number;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export type ViewportOverflow = "above" | "below" | "both";
|
|
9
|
+
|
|
10
|
+
export interface ViewportRow<T> {
|
|
11
|
+
value: T;
|
|
12
|
+
overflow?: ViewportOverflow;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Fit logical rows into a terminal-height viewport.
|
|
17
|
+
*
|
|
18
|
+
* Fixed rows are taken from the beginning and end of `rows`. The rows between
|
|
19
|
+
* them form the scrollable region. Overflow is metadata on boundary rows, so
|
|
20
|
+
* it never consumes an additional terminal row.
|
|
21
|
+
*
|
|
22
|
+
* If the viewport is smaller than the fixed chrome, the first top row wins a
|
|
23
|
+
* one-row viewport. With two or more rows, at least one row from each fixed
|
|
24
|
+
* edge is retained, then remaining space is assigned to the top edge first.
|
|
25
|
+
*/
|
|
26
|
+
export function fitViewport<T>(
|
|
27
|
+
rows: readonly T[],
|
|
28
|
+
focus: FocusRange | undefined,
|
|
29
|
+
maxRows: number,
|
|
30
|
+
fixedTopRows: number,
|
|
31
|
+
fixedBottomRows: number,
|
|
32
|
+
): ViewportRow<T>[] {
|
|
33
|
+
const limit = rowLimit(maxRows, rows.length);
|
|
34
|
+
if (limit === 0 || rows.length === 0) return [];
|
|
35
|
+
|
|
36
|
+
if (rows.length <= limit) return rows.map(value => ({ value }));
|
|
37
|
+
|
|
38
|
+
const topSize = fixedSize(fixedTopRows, rows.length);
|
|
39
|
+
const bottomSize = fixedSize(fixedBottomRows, rows.length - topSize);
|
|
40
|
+
const middleStart = topSize;
|
|
41
|
+
const middleEnd = rows.length - bottomSize;
|
|
42
|
+
const chromeSize = topSize + bottomSize;
|
|
43
|
+
|
|
44
|
+
if (limit < chromeSize) {
|
|
45
|
+
const { topVisible, bottomVisible } = degradedChrome(limit, topSize, bottomSize);
|
|
46
|
+
return [
|
|
47
|
+
...rows.slice(0, topVisible),
|
|
48
|
+
...rows.slice(rows.length - bottomVisible),
|
|
49
|
+
].map(value => ({ value }));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const middleCapacity = limit - chromeSize;
|
|
53
|
+
const middleSize = middleEnd - middleStart;
|
|
54
|
+
const visibleCount = Math.min(middleCapacity, middleSize);
|
|
55
|
+
const windowStart = chooseWindowStart(
|
|
56
|
+
focus,
|
|
57
|
+
rows.length,
|
|
58
|
+
middleStart,
|
|
59
|
+
middleEnd,
|
|
60
|
+
visibleCount,
|
|
61
|
+
);
|
|
62
|
+
const hiddenAbove = windowStart > middleStart;
|
|
63
|
+
const hiddenBelow = windowStart + visibleCount < middleEnd;
|
|
64
|
+
const middle = rows
|
|
65
|
+
.slice(windowStart, windowStart + visibleCount)
|
|
66
|
+
.map(value => ({ value } as ViewportRow<T>));
|
|
67
|
+
|
|
68
|
+
markOverflow(middle, hiddenAbove, hiddenBelow);
|
|
69
|
+
|
|
70
|
+
return [
|
|
71
|
+
...rows.slice(0, topSize).map(value => ({ value })),
|
|
72
|
+
...middle,
|
|
73
|
+
...rows.slice(middleEnd).map(value => ({ value })),
|
|
74
|
+
];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function rowLimit(value: number, contentLength: number): number {
|
|
78
|
+
if (Number.isNaN(value) || value <= 0) return 0;
|
|
79
|
+
if (!Number.isFinite(value)) return contentLength;
|
|
80
|
+
return Math.floor(value);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function fixedSize(value: number, available: number): number {
|
|
84
|
+
if (Number.isNaN(value) || value <= 0) return 0;
|
|
85
|
+
if (!Number.isFinite(value)) return available;
|
|
86
|
+
return Math.min(Math.floor(value), available);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function degradedChrome(
|
|
90
|
+
limit: number,
|
|
91
|
+
topSize: number,
|
|
92
|
+
bottomSize: number,
|
|
93
|
+
): { topVisible: number; bottomVisible: number } {
|
|
94
|
+
if (topSize === 0) return { topVisible: 0, bottomVisible: Math.min(limit, bottomSize) };
|
|
95
|
+
if (bottomSize === 0 || limit === 1) {
|
|
96
|
+
return { topVisible: Math.min(limit, topSize), bottomVisible: 0 };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const topVisible = Math.min(topSize, limit - 1);
|
|
100
|
+
const bottomVisible = Math.min(bottomSize, limit - topVisible);
|
|
101
|
+
return { topVisible, bottomVisible };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function chooseWindowStart(
|
|
105
|
+
focus: FocusRange | undefined,
|
|
106
|
+
rowCount: number,
|
|
107
|
+
middleStart: number,
|
|
108
|
+
middleEnd: number,
|
|
109
|
+
visibleCount: number,
|
|
110
|
+
): number {
|
|
111
|
+
const latestStart = middleEnd - visibleCount;
|
|
112
|
+
const range = normalizeFocus(focus, rowCount);
|
|
113
|
+
if (!range) return middleStart;
|
|
114
|
+
|
|
115
|
+
if (range.end <= middleStart) return middleStart;
|
|
116
|
+
if (range.start >= middleEnd) return latestStart;
|
|
117
|
+
|
|
118
|
+
const focusStart = Math.max(range.start, middleStart);
|
|
119
|
+
const focusEnd = Math.min(range.end, middleEnd);
|
|
120
|
+
const focusSize = focusEnd - focusStart;
|
|
121
|
+
if (focusSize <= 0) return middleStart;
|
|
122
|
+
if (focusSize >= visibleCount) return Math.min(focusStart, latestStart);
|
|
123
|
+
|
|
124
|
+
const contextBefore = Math.floor((visibleCount - focusSize) / 2);
|
|
125
|
+
return Math.max(middleStart, Math.min(focusStart - contextBefore, latestStart));
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function normalizeFocus(focus: FocusRange | undefined, rowCount: number): FocusRange | undefined {
|
|
129
|
+
if (!focus || !Number.isFinite(focus.start) || !Number.isFinite(focus.end)) return undefined;
|
|
130
|
+
const start = Math.max(0, Math.min(Math.floor(focus.start), rowCount));
|
|
131
|
+
const end = Math.max(0, Math.min(Math.floor(focus.end), rowCount));
|
|
132
|
+
if (end <= start) return undefined;
|
|
133
|
+
return { start, end };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function markOverflow<T>(
|
|
137
|
+
middle: ViewportRow<T>[],
|
|
138
|
+
hiddenAbove: boolean,
|
|
139
|
+
hiddenBelow: boolean,
|
|
140
|
+
): void {
|
|
141
|
+
if (middle.length === 0) return;
|
|
142
|
+
if (middle.length === 1 && hiddenAbove && hiddenBelow) {
|
|
143
|
+
middle[0]!.overflow = "both";
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
if (hiddenAbove) middle[0]!.overflow = "above";
|
|
147
|
+
if (hiddenBelow) middle[middle.length - 1]!.overflow = "below";
|
|
148
|
+
}
|