@sayknow-cli/tui 0.3.11 → 0.3.13

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 (39) hide show
  1. package/package.json +8 -9
  2. package/src/components/image.ts +79 -39
  3. package/src/components/markdown.ts +282 -37
  4. package/src/components/sayknow-pet.ts +435 -0
  5. package/src/components/text.ts +60 -36
  6. package/src/index.ts +1 -0
  7. package/src/terminal-capabilities.ts +42 -0
  8. package/src/tui.ts +471 -55
  9. package/src/utils.ts +144 -8
  10. package/dist/types/animation-scheduler.d.ts +0 -13
  11. package/dist/types/autocomplete.d.ts +0 -83
  12. package/dist/types/bracketed-paste.d.ts +0 -26
  13. package/dist/types/components/box.d.ts +0 -20
  14. package/dist/types/components/cancellable-loader.d.ts +0 -21
  15. package/dist/types/components/editor.d.ts +0 -126
  16. package/dist/types/components/image.d.ts +0 -16
  17. package/dist/types/components/input.d.ts +0 -16
  18. package/dist/types/components/loader.d.ts +0 -23
  19. package/dist/types/components/markdown.d.ts +0 -77
  20. package/dist/types/components/select-list.d.ts +0 -46
  21. package/dist/types/components/settings-list.d.ts +0 -39
  22. package/dist/types/components/spacer.d.ts +0 -11
  23. package/dist/types/components/tab-bar.d.ts +0 -56
  24. package/dist/types/components/text.d.ts +0 -13
  25. package/dist/types/components/truncated-text.d.ts +0 -10
  26. package/dist/types/editor-component.d.ts +0 -36
  27. package/dist/types/fuzzy.d.ts +0 -15
  28. package/dist/types/index.d.ts +0 -27
  29. package/dist/types/keybindings.d.ts +0 -201
  30. package/dist/types/keys.d.ts +0 -208
  31. package/dist/types/kill-ring.d.ts +0 -27
  32. package/dist/types/metrics.d.ts +0 -85
  33. package/dist/types/stdin-buffer.d.ts +0 -50
  34. package/dist/types/symbols.d.ts +0 -23
  35. package/dist/types/terminal-capabilities.d.ts +0 -143
  36. package/dist/types/terminal.d.ts +0 -90
  37. package/dist/types/ttyid.d.ts +0 -9
  38. package/dist/types/tui.d.ts +0 -215
  39. package/dist/types/utils.d.ts +0 -87
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@sayknow-cli/tui",
4
- "version": "0.3.11",
4
+ "version": "0.3.13",
5
5
  "description": "Terminal User Interface library with differential rendering for efficient text-based applications",
6
6
  "homepage": "https://sayknow-cli.com",
7
7
  "author": "jaybeyond",
@@ -27,7 +27,7 @@
27
27
  "cli"
28
28
  ],
29
29
  "main": "./src/index.ts",
30
- "types": "./dist/types/index.d.ts",
30
+ "types": "./src/index.ts",
31
31
  "scripts": {
32
32
  "check": "biome check . && bun run check:types",
33
33
  "check:types": "tsgo -p tsconfig.json --noEmit",
@@ -38,8 +38,8 @@
38
38
  "fmt": "biome format --write ."
39
39
  },
40
40
  "dependencies": {
41
- "@sayknow-cli/natives": "0.3.11",
42
- "@sayknow-cli/utils": "0.3.11",
41
+ "@sayknow-cli/natives": "0.3.13",
42
+ "@sayknow-cli/utils": "0.3.13",
43
43
  "lru-cache": "11.3.6",
44
44
  "marked": "^18.0.3"
45
45
  },
@@ -53,20 +53,19 @@
53
53
  "files": [
54
54
  "src",
55
55
  "README.md",
56
- "CHANGELOG.md",
57
- "dist/types"
56
+ "CHANGELOG.md"
58
57
  ],
59
58
  "exports": {
60
59
  ".": {
61
- "types": "./dist/types/index.d.ts",
60
+ "types": "./src/index.ts",
62
61
  "import": "./src/index.ts"
63
62
  },
64
63
  "./*": {
65
- "types": "./dist/types/*.d.ts",
64
+ "types": "./src/*.ts",
66
65
  "import": "./src/*.ts"
67
66
  },
68
67
  "./components/*": {
69
- "types": "./dist/types/components/*.d.ts",
68
+ "types": "./src/components/*.ts",
70
69
  "import": "./src/components/*.ts"
71
70
  },
72
71
  "./*.js": "./src/*.ts"
@@ -3,6 +3,8 @@ import {
3
3
  type ImageDimensions,
4
4
  ImageProtocol,
5
5
  imageFallback,
6
+ isCursorNeutralImagePermittedInFallback,
7
+ isTerminalGraphicsFallbackActive,
6
8
  kittyImageId,
7
9
  renderImage,
8
10
  TERMINAL,
@@ -28,10 +30,11 @@ export interface ImageOptions {
28
30
  maxWidthCells?: number;
29
31
  maxHeightCells?: number;
30
32
  filename?: string;
33
+ refetch?: () => string;
31
34
  }
32
35
 
33
36
  export class Image implements Component {
34
- #base64Data: string;
37
+ #base64Data?: string;
35
38
  #mimeType: string;
36
39
  #dimensions: ImageDimensions;
37
40
  #theme: ImageTheme;
@@ -39,7 +42,8 @@ export class Image implements Component {
39
42
 
40
43
  #cachedLines?: string[];
41
44
  #cachedWidth?: number;
42
- // Kitty graphics: content-derived image id + per-instance placement id.
45
+ #cachedFallbackActive?: boolean;
46
+ #cachedProtocol?: ImageProtocol | null;
43
47
  // Computed lazily so non-kitty terminals never pay the hash cost.
44
48
  #kittyImageId?: number;
45
49
  readonly #kittyPlacementId = allocatePlacementId();
@@ -61,10 +65,40 @@ export class Image implements Component {
61
65
  invalidate(): void {
62
66
  this.#cachedLines = undefined;
63
67
  this.#cachedWidth = undefined;
68
+ this.#cachedFallbackActive = undefined;
69
+ this.#cachedProtocol = undefined;
70
+ }
71
+
72
+ get retainedBase64DataForTest(): string | undefined {
73
+ return this.#base64Data;
74
+ }
75
+
76
+ #fallbackLines(): string[] {
77
+ const fallback = imageFallback(this.#mimeType, this.#dimensions, this.#options.filename);
78
+ return [this.#theme.fallbackColor(fallback)];
79
+ }
80
+
81
+ #getBase64Data(): string | undefined {
82
+ if (this.#base64Data) return this.#base64Data;
83
+ const refetched = this.#options.refetch?.();
84
+ if (refetched) this.#base64Data = refetched;
85
+ return this.#base64Data;
64
86
  }
65
87
 
66
88
  render(width: number): string[] {
67
- if (this.#cachedLines && this.#cachedWidth === width) {
89
+ // Kitty placements are cursor-neutral, so an opted-in fallback scope
90
+ // (e.g. the IRC split) can still render them safely; iTerm2/SIXEL
91
+ // advance the cursor and stay suppressed.
92
+ const graphicsSuppressed =
93
+ isTerminalGraphicsFallbackActive() &&
94
+ !(TERMINAL.imageProtocol === ImageProtocol.Kitty && isCursorNeutralImagePermittedInFallback());
95
+ const protocol = TERMINAL.imageProtocol;
96
+ if (
97
+ this.#cachedLines &&
98
+ this.#cachedWidth === width &&
99
+ this.#cachedFallbackActive === graphicsSuppressed &&
100
+ this.#cachedProtocol === protocol
101
+ ) {
68
102
  return this.#cachedLines;
69
103
  }
70
104
 
@@ -73,51 +107,57 @@ export class Image implements Component {
73
107
 
74
108
  let lines: string[];
75
109
 
76
- if (TERMINAL.imageProtocol) {
77
- if (TERMINAL.imageProtocol === ImageProtocol.Kitty) {
78
- this.#kittyImageId ??= kittyImageId(this.#base64Data);
79
- }
80
- const result = renderImage(this.#base64Data, this.#dimensions, {
81
- maxWidthCells: maxWidth,
82
- maxHeightCells: this.#options.maxHeightCells,
83
- imageId: this.#kittyImageId,
84
- placementId: this.#kittyPlacementId,
85
- });
86
-
87
- if (result) {
88
- // Return `rows` lines so the TUI accounts for the image height.
89
- if (result.cursorNeutral) {
90
- // Kitty a=p,C=1 placements neither move the cursor nor carry
91
- // pixel data, so the escape lives on the FIRST row — the image
92
- // anchors to that cell and no cursor-up trick is needed (the
93
- // old CUU approach clamped at the viewport top edge and placed
94
- // the image over transcript text when partially scrolled out).
95
- lines = [result.sequence];
96
- for (let i = 0; i < result.rows - 1; i++) {
97
- lines.push("");
110
+ if (protocol && !graphicsSuppressed) {
111
+ const base64Data = this.#getBase64Data();
112
+ if (!base64Data) {
113
+ lines = this.#fallbackLines();
114
+ } else {
115
+ if (protocol === ImageProtocol.Kitty) {
116
+ this.#kittyImageId ??= kittyImageId(base64Data);
117
+ }
118
+ const result = renderImage(base64Data, this.#dimensions, {
119
+ maxWidthCells: maxWidth,
120
+ maxHeightCells: this.#options.maxHeightCells,
121
+ imageId: this.#kittyImageId,
122
+ placementId: this.#kittyPlacementId,
123
+ });
124
+
125
+ if (result) {
126
+ // Return `rows` lines so the TUI accounts for the image height.
127
+ if (result.cursorNeutral) {
128
+ // Kitty a=p,C=1 placements neither move the cursor nor carry
129
+ // pixel data, so the escape lives on the FIRST row — the image
130
+ // anchors to that cell and no cursor-up trick is needed (the
131
+ // old CUU approach clamped at the viewport top edge and placed
132
+ // the image over transcript text when partially scrolled out).
133
+ lines = [result.sequence];
134
+ for (let i = 0; i < result.rows - 1; i++) {
135
+ lines.push("");
136
+ }
137
+ } else {
138
+ // iTerm2/SIXEL draw at the cursor and advance it: reserve
139
+ // rows-1 blank lines (TUI clears them), then move the cursor
140
+ // up and draw from the last line.
141
+ lines = [];
142
+ for (let i = 0; i < result.rows - 1; i++) {
143
+ lines.push("");
144
+ }
145
+ const moveUp = result.rows > 1 ? `\x1b[${result.rows - 1}A` : "";
146
+ lines.push(moveUp + result.sequence);
98
147
  }
148
+ this.#base64Data = undefined;
99
149
  } else {
100
- // iTerm2/SIXEL draw at the cursor and advance it: reserve
101
- // rows-1 blank lines (TUI clears them), then move the cursor
102
- // up and draw from the last line.
103
- lines = [];
104
- for (let i = 0; i < result.rows - 1; i++) {
105
- lines.push("");
106
- }
107
- const moveUp = result.rows > 1 ? `\x1b[${result.rows - 1}A` : "";
108
- lines.push(moveUp + result.sequence);
150
+ lines = this.#fallbackLines();
109
151
  }
110
- } else {
111
- const fallback = imageFallback(this.#mimeType, this.#dimensions, this.#options.filename);
112
- lines = [this.#theme.fallbackColor(fallback)];
113
152
  }
114
153
  } else {
115
- const fallback = imageFallback(this.#mimeType, this.#dimensions, this.#options.filename);
116
- lines = [this.#theme.fallbackColor(fallback)];
154
+ lines = this.#fallbackLines();
117
155
  }
118
156
 
119
157
  this.#cachedLines = lines;
120
158
  this.#cachedWidth = width;
159
+ this.#cachedFallbackActive = graphicsSuppressed;
160
+ this.#cachedProtocol = protocol;
121
161
 
122
162
  return lines;
123
163
  }