@boba-cli/code 0.1.0-alpha.2

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 ADDED
@@ -0,0 +1,43 @@
1
+ # @boba-cli/code
2
+
3
+ Syntax-highlighted code viewer component for Boba terminal UIs.
4
+
5
+ ![Code Demo](../../examples/code-demo.gif)
6
+
7
+ ## Features
8
+
9
+ - Syntax highlighting with configurable themes
10
+ - Scrollable viewport for long code files
11
+ - Supports multiple programming languages via file extension detection
12
+ - Async file loading
13
+ - Keyboard navigation when active
14
+
15
+ ## Installation
16
+
17
+ ```bash
18
+ pnpm add @boba-cli/code
19
+ ```
20
+
21
+ ## Usage
22
+
23
+ ```typescript
24
+ import { CodeModel } from '@boba-cli/code'
25
+ import { Program } from '@boba-cli/tea'
26
+
27
+ const codeModel = CodeModel.new({ active: true })
28
+
29
+ // In init
30
+ const cmd = codeModel.setFileName('example.ts')
31
+
32
+ // In update - handle window resize
33
+ if (msg instanceof WindowSizeMsg) {
34
+ codeModel = codeModel.setSize(msg.width, msg.height)
35
+ }
36
+
37
+ // In view
38
+ return codeModel.view()
39
+ ```
40
+
41
+ ## API
42
+
43
+ See [API documentation](../../docs/code.codemodel.md) for details.
package/dist/index.cjs ADDED
@@ -0,0 +1,245 @@
1
+ 'use strict';
2
+
3
+ var chapstick = require('@boba-cli/chapstick');
4
+ var filesystem = require('@boba-cli/filesystem');
5
+ var viewport = require('@boba-cli/viewport');
6
+ var shiki = require('shiki');
7
+
8
+ // src/model.ts
9
+
10
+ // src/messages.ts
11
+ var SyntaxMsg = class {
12
+ constructor(content) {
13
+ this.content = content;
14
+ }
15
+ _tag = "code-syntax";
16
+ };
17
+ var ErrorMsg = class {
18
+ constructor(error) {
19
+ this.error = error;
20
+ }
21
+ _tag = "code-error";
22
+ };
23
+
24
+ // src/model.ts
25
+ async function highlight(content, extension, theme = "dracula") {
26
+ try {
27
+ const highlighter = await shiki.getHighlighter({
28
+ themes: [theme],
29
+ langs: []
30
+ });
31
+ const ext = extension.startsWith(".") ? extension.slice(1) : extension;
32
+ let lang = ext || "text";
33
+ try {
34
+ await highlighter.loadLanguage(lang);
35
+ } catch {
36
+ lang = "text";
37
+ try {
38
+ await highlighter.loadLanguage("text");
39
+ } catch {
40
+ return content;
41
+ }
42
+ }
43
+ const tokens = highlighter.codeToTokens(content, {
44
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-assignment
45
+ lang,
46
+ theme
47
+ });
48
+ let result = "";
49
+ for (const line of tokens.tokens) {
50
+ for (const token of line) {
51
+ if (token.color) {
52
+ result += `\x1B[38;2;${hexToRgb(token.color)}m${token.content}\x1B[0m`;
53
+ } else {
54
+ result += token.content;
55
+ }
56
+ }
57
+ result += "\n";
58
+ }
59
+ return result.trimEnd();
60
+ } catch (error) {
61
+ const errorMessage = error instanceof Error ? error.message : String(error);
62
+ throw new Error(`Syntax highlighting failed: ${errorMessage}`);
63
+ }
64
+ }
65
+ function hexToRgb(hex) {
66
+ const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
67
+ if (!result) return "255;255;255";
68
+ return `${parseInt(result[1], 16)};${parseInt(result[2], 16)};${parseInt(result[3], 16)}`;
69
+ }
70
+ function readFileContentCmd(fsAdapter, pathAdapter, fileName, syntaxTheme) {
71
+ return async () => {
72
+ try {
73
+ const content = await filesystem.readFileContent(fsAdapter, fileName);
74
+ const extension = pathAdapter.extname(fileName);
75
+ const highlightedContent = await highlight(
76
+ content,
77
+ extension,
78
+ syntaxTheme
79
+ );
80
+ return new SyntaxMsg(highlightedContent);
81
+ } catch (error) {
82
+ return new ErrorMsg(
83
+ error instanceof Error ? error : new Error(String(error))
84
+ );
85
+ }
86
+ };
87
+ }
88
+ var CodeModel = class _CodeModel {
89
+ viewport;
90
+ active;
91
+ filename;
92
+ highlightedContent;
93
+ syntaxTheme;
94
+ filesystem;
95
+ path;
96
+ constructor(options) {
97
+ this.viewport = options.viewport;
98
+ this.active = options.active;
99
+ this.filename = options.filename;
100
+ this.highlightedContent = options.highlightedContent;
101
+ this.syntaxTheme = options.syntaxTheme;
102
+ this.filesystem = options.filesystem;
103
+ this.path = options.path;
104
+ }
105
+ /**
106
+ * Creates a new code model instance.
107
+ * @param options - Configuration options
108
+ * @returns A new CodeModel instance
109
+ */
110
+ static new(options) {
111
+ return new _CodeModel({
112
+ viewport: viewport.ViewportModel.new({
113
+ width: options.width ?? 0,
114
+ height: options.height ?? 0
115
+ }),
116
+ active: options.active ?? false,
117
+ filename: "",
118
+ highlightedContent: "",
119
+ syntaxTheme: options.syntaxTheme ?? "dracula",
120
+ filesystem: options.filesystem,
121
+ path: options.path
122
+ });
123
+ }
124
+ /**
125
+ * Create a copy with updated fields.
126
+ */
127
+ with(partial) {
128
+ return new _CodeModel({
129
+ viewport: partial.viewport ?? this.viewport,
130
+ active: partial.active ?? this.active,
131
+ filename: partial.filename ?? this.filename,
132
+ highlightedContent: partial.highlightedContent ?? this.highlightedContent,
133
+ syntaxTheme: partial.syntaxTheme ?? this.syntaxTheme,
134
+ filesystem: partial.filesystem ?? this.filesystem,
135
+ path: partial.path ?? this.path
136
+ });
137
+ }
138
+ /**
139
+ * Sets the file to display and triggers highlighting.
140
+ * @param filename - Path to the file to display
141
+ * @returns Command to read and highlight the file
142
+ */
143
+ setFileName(filename) {
144
+ const updated = this.with({ filename });
145
+ return [updated, readFileContentCmd(this.filesystem, this.path, filename, this.syntaxTheme)];
146
+ }
147
+ /**
148
+ * Sets whether the component is active (receives input).
149
+ * @param active - Whether the component should be active
150
+ * @returns Updated model
151
+ */
152
+ setIsActive(active) {
153
+ if (this.active === active) return this;
154
+ return this.with({ active });
155
+ }
156
+ /**
157
+ * Sets the syntax highlighting theme.
158
+ * @param theme - The theme name (e.g., "dracula", "monokai")
159
+ * @returns Updated model
160
+ */
161
+ setSyntaxTheme(theme) {
162
+ if (this.syntaxTheme === theme) return this;
163
+ return this.with({ syntaxTheme: theme });
164
+ }
165
+ /**
166
+ * Sets the viewport dimensions.
167
+ * @param width - Width in characters
168
+ * @param height - Height in lines
169
+ * @returns Updated model
170
+ */
171
+ setSize(width, height) {
172
+ const nextViewport = this.viewport.setWidth(width).setHeight(height);
173
+ if (nextViewport === this.viewport) return this;
174
+ return this.with({ viewport: nextViewport });
175
+ }
176
+ /**
177
+ * Scrolls to the top of the viewport.
178
+ * @returns Updated model
179
+ */
180
+ gotoTop() {
181
+ const nextViewport = this.viewport.scrollToTop();
182
+ if (nextViewport === this.viewport) return this;
183
+ return this.with({ viewport: nextViewport });
184
+ }
185
+ /**
186
+ * Tea init hook.
187
+ */
188
+ init() {
189
+ return this.viewport.init();
190
+ }
191
+ /**
192
+ * Handles messages (keyboard navigation when active).
193
+ * @param msg - The message to handle
194
+ * @returns Tuple of updated model and command
195
+ */
196
+ update(msg) {
197
+ if (msg instanceof SyntaxMsg) {
198
+ const content = msg.content;
199
+ const style = new chapstick.Style().width(this.viewport.width).alignHorizontal("left");
200
+ const rendered = style.render(content);
201
+ const nextViewport = this.viewport.setContent(rendered);
202
+ return [
203
+ this.with({
204
+ highlightedContent: rendered,
205
+ viewport: nextViewport
206
+ }),
207
+ null
208
+ ];
209
+ }
210
+ if (msg instanceof ErrorMsg) {
211
+ const errorContent = `Error: ${msg.error.message}`;
212
+ const style = new chapstick.Style().width(this.viewport.width).alignHorizontal("left");
213
+ const rendered = style.render(errorContent);
214
+ const nextViewport = this.viewport.setContent(rendered);
215
+ return [
216
+ this.with({
217
+ highlightedContent: rendered,
218
+ viewport: nextViewport
219
+ }),
220
+ null
221
+ ];
222
+ }
223
+ if (this.active) {
224
+ const [nextViewport, cmd] = this.viewport.update(msg);
225
+ if (nextViewport !== this.viewport) {
226
+ return [this.with({ viewport: nextViewport }), cmd];
227
+ }
228
+ }
229
+ return [this, null];
230
+ }
231
+ /**
232
+ * Returns the rendered string.
233
+ * @returns The viewport view
234
+ */
235
+ view() {
236
+ return this.viewport.view();
237
+ }
238
+ };
239
+
240
+ exports.CodeModel = CodeModel;
241
+ exports.ErrorMsg = ErrorMsg;
242
+ exports.SyntaxMsg = SyntaxMsg;
243
+ exports.highlight = highlight;
244
+ //# sourceMappingURL=index.cjs.map
245
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/messages.ts","../src/model.ts"],"names":["getHighlighter","readFileContent","ViewportModel","Style"],"mappings":"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","file":"index.cjs","sourcesContent":["/**\n * Message containing syntax-highlighted content after async processing.\n * @public\n */\nexport class SyntaxMsg {\n readonly _tag = 'code-syntax'\n constructor(public readonly content: string) {}\n}\n\n/**\n * Message containing an error if file read or highlighting fails.\n * @public\n */\nexport class ErrorMsg {\n readonly _tag = 'code-error'\n constructor(public readonly error: Error) {}\n}\n","import { Style } from '@boba-cli/chapstick'\nimport { readFileContent } from '@boba-cli/filesystem'\nimport { type Cmd, type Msg } from '@boba-cli/tea'\nimport type { FileSystemAdapter, PathAdapter } from '@boba-cli/machine'\nimport { ViewportModel } from '@boba-cli/viewport'\nimport { getHighlighter } from 'shiki'\nimport { ErrorMsg, SyntaxMsg } from './messages.js'\n\n/**\n * Options for creating a code model.\n * @public\n */\nexport interface CodeOptions {\n /** Filesystem adapter for file operations */\n filesystem: FileSystemAdapter\n /** Path adapter for path operations */\n path: PathAdapter\n active?: boolean\n syntaxTheme?: string\n width?: number\n height?: number\n}\n\n/**\n * Highlight returns a syntax-highlighted string of text.\n * @param content - The source code content to highlight\n * @param extension - The file extension (e.g., \".ts\", \".go\")\n * @param theme - The syntax theme to use (default: \"dracula\")\n * @returns Promise resolving to the highlighted content string\n * @public\n */\nexport async function highlight(\n content: string,\n extension: string,\n theme: string = 'dracula',\n): Promise<string> {\n try {\n const highlighter = await getHighlighter({\n themes: [theme],\n langs: [],\n })\n\n // Map extension to language - use a safer approach\n const ext = extension.startsWith('.') ? extension.slice(1) : extension\n let lang = ext || 'text'\n\n // Load the language if needed, fallback to text\n try {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument\n await highlighter.loadLanguage(lang as any)\n } catch {\n // If language loading fails, use 'text' as fallback\n lang = 'text'\n try {\n await highlighter.loadLanguage('text')\n } catch {\n // If even text fails, just return plain content\n return content\n }\n }\n\n // Get tokens from highlighter\n const tokens = highlighter.codeToTokens(content, {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-assignment\n lang: lang as any,\n theme: theme,\n })\n\n // Convert tokens to ANSI colored text\n let result = ''\n for (const line of tokens.tokens) {\n for (const token of line) {\n if (token.color) {\n // Simple ANSI color conversion (this is a basic approach)\n result += `\\x1b[38;2;${hexToRgb(token.color)}m${token.content}\\x1b[0m`\n } else {\n result += token.content\n }\n }\n result += '\\n'\n }\n\n return result.trimEnd()\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error)\n throw new Error(`Syntax highlighting failed: ${errorMessage}`)\n }\n}\n\n/**\n * Convert hex color to RGB values for ANSI\n */\nfunction hexToRgb(hex: string): string {\n const result = /^#?([a-f\\d]{2})([a-f\\d]{2})([a-f\\d]{2})$/i.exec(hex)\n if (!result) return '255;255;255'\n return `${parseInt(result[1]!, 16)};${parseInt(result[2]!, 16)};${parseInt(result[3]!, 16)}`\n}\n\n/**\n * Read file content and highlight it asynchronously.\n */\nfunction readFileContentCmd(\n fsAdapter: FileSystemAdapter,\n pathAdapter: PathAdapter,\n fileName: string,\n syntaxTheme: string,\n): Cmd<Msg> {\n return async (): Promise<Msg> => {\n try {\n const content = await readFileContent(fsAdapter, fileName)\n const extension = pathAdapter.extname(fileName)\n const highlightedContent = await highlight(\n content,\n extension,\n syntaxTheme,\n )\n return new SyntaxMsg(highlightedContent)\n } catch (error) {\n return new ErrorMsg(\n error instanceof Error ? error : new Error(String(error)),\n )\n }\n }\n}\n\n/**\n * Model representing a syntax-highlighted code viewer.\n * @public\n */\nexport class CodeModel {\n readonly viewport: ViewportModel\n readonly active: boolean\n readonly filename: string\n readonly highlightedContent: string\n readonly syntaxTheme: string\n readonly filesystem: FileSystemAdapter\n readonly path: PathAdapter\n\n private constructor(options: {\n viewport: ViewportModel\n active: boolean\n filename: string\n highlightedContent: string\n syntaxTheme: string\n filesystem: FileSystemAdapter\n path: PathAdapter\n }) {\n this.viewport = options.viewport\n this.active = options.active\n this.filename = options.filename\n this.highlightedContent = options.highlightedContent\n this.syntaxTheme = options.syntaxTheme\n this.filesystem = options.filesystem\n this.path = options.path\n }\n\n /**\n * Creates a new code model instance.\n * @param options - Configuration options\n * @returns A new CodeModel instance\n */\n static new(options: CodeOptions): CodeModel {\n return new CodeModel({\n viewport: ViewportModel.new({\n width: options.width ?? 0,\n height: options.height ?? 0,\n }),\n active: options.active ?? false,\n filename: '',\n highlightedContent: '',\n syntaxTheme: options.syntaxTheme ?? 'dracula',\n filesystem: options.filesystem,\n path: options.path,\n })\n }\n\n /**\n * Create a copy with updated fields.\n */\n private with(\n partial: Partial<{\n viewport: ViewportModel\n active: boolean\n filename: string\n highlightedContent: string\n syntaxTheme: string\n filesystem: FileSystemAdapter\n path: PathAdapter\n }>,\n ): CodeModel {\n return new CodeModel({\n viewport: partial.viewport ?? this.viewport,\n active: partial.active ?? this.active,\n filename: partial.filename ?? this.filename,\n highlightedContent: partial.highlightedContent ?? this.highlightedContent,\n syntaxTheme: partial.syntaxTheme ?? this.syntaxTheme,\n filesystem: partial.filesystem ?? this.filesystem,\n path: partial.path ?? this.path,\n })\n }\n\n /**\n * Sets the file to display and triggers highlighting.\n * @param filename - Path to the file to display\n * @returns Command to read and highlight the file\n */\n setFileName(filename: string): [CodeModel, Cmd<Msg>] {\n const updated = this.with({ filename })\n return [updated, readFileContentCmd(this.filesystem, this.path, filename, this.syntaxTheme)]\n }\n\n /**\n * Sets whether the component is active (receives input).\n * @param active - Whether the component should be active\n * @returns Updated model\n */\n setIsActive(active: boolean): CodeModel {\n if (this.active === active) return this\n return this.with({ active })\n }\n\n /**\n * Sets the syntax highlighting theme.\n * @param theme - The theme name (e.g., \"dracula\", \"monokai\")\n * @returns Updated model\n */\n setSyntaxTheme(theme: string): CodeModel {\n if (this.syntaxTheme === theme) return this\n return this.with({ syntaxTheme: theme })\n }\n\n /**\n * Sets the viewport dimensions.\n * @param width - Width in characters\n * @param height - Height in lines\n * @returns Updated model\n */\n setSize(width: number, height: number): CodeModel {\n const nextViewport = this.viewport.setWidth(width).setHeight(height)\n if (nextViewport === this.viewport) return this\n return this.with({ viewport: nextViewport })\n }\n\n /**\n * Scrolls to the top of the viewport.\n * @returns Updated model\n */\n gotoTop(): CodeModel {\n const nextViewport = this.viewport.scrollToTop()\n if (nextViewport === this.viewport) return this\n return this.with({ viewport: nextViewport })\n }\n\n /**\n * Tea init hook.\n */\n init(): Cmd<Msg> {\n return this.viewport.init()\n }\n\n /**\n * Handles messages (keyboard navigation when active).\n * @param msg - The message to handle\n * @returns Tuple of updated model and command\n */\n update(msg: Msg): [CodeModel, Cmd<Msg>] {\n if (msg instanceof SyntaxMsg) {\n const content = msg.content\n // Apply width for consistent line lengths and left-align to pad lines\n // Viewport handles height/scrolling - don't use .height() here as it truncates\n const style = new Style()\n .width(this.viewport.width)\n .alignHorizontal('left')\n const rendered = style.render(content)\n const nextViewport = this.viewport.setContent(rendered)\n return [\n this.with({\n highlightedContent: rendered,\n viewport: nextViewport,\n }),\n null,\n ]\n }\n\n if (msg instanceof ErrorMsg) {\n const errorContent = `Error: ${msg.error.message}`\n const style = new Style()\n .width(this.viewport.width)\n .alignHorizontal('left')\n const rendered = style.render(errorContent)\n const nextViewport = this.viewport.setContent(rendered)\n return [\n this.with({\n highlightedContent: rendered,\n viewport: nextViewport,\n }),\n null,\n ]\n }\n\n if (this.active) {\n const [nextViewport, cmd] = this.viewport.update(msg)\n if (nextViewport !== this.viewport) {\n return [this.with({ viewport: nextViewport }), cmd]\n }\n }\n\n return [this, null]\n }\n\n /**\n * Returns the rendered string.\n * @returns The viewport view\n */\n view(): string {\n return this.viewport.view()\n }\n}\n"]}
@@ -0,0 +1,117 @@
1
+ import { Cmd, Msg } from '@boba-cli/tea';
2
+ import { FileSystemAdapter, PathAdapter } from '@boba-cli/machine';
3
+ import { ViewportModel } from '@boba-cli/viewport';
4
+
5
+ /**
6
+ * Options for creating a code model.
7
+ * @public
8
+ */
9
+ interface CodeOptions {
10
+ /** Filesystem adapter for file operations */
11
+ filesystem: FileSystemAdapter;
12
+ /** Path adapter for path operations */
13
+ path: PathAdapter;
14
+ active?: boolean;
15
+ syntaxTheme?: string;
16
+ width?: number;
17
+ height?: number;
18
+ }
19
+ /**
20
+ * Highlight returns a syntax-highlighted string of text.
21
+ * @param content - The source code content to highlight
22
+ * @param extension - The file extension (e.g., ".ts", ".go")
23
+ * @param theme - The syntax theme to use (default: "dracula")
24
+ * @returns Promise resolving to the highlighted content string
25
+ * @public
26
+ */
27
+ declare function highlight(content: string, extension: string, theme?: string): Promise<string>;
28
+ /**
29
+ * Model representing a syntax-highlighted code viewer.
30
+ * @public
31
+ */
32
+ declare class CodeModel {
33
+ readonly viewport: ViewportModel;
34
+ readonly active: boolean;
35
+ readonly filename: string;
36
+ readonly highlightedContent: string;
37
+ readonly syntaxTheme: string;
38
+ readonly filesystem: FileSystemAdapter;
39
+ readonly path: PathAdapter;
40
+ private constructor();
41
+ /**
42
+ * Creates a new code model instance.
43
+ * @param options - Configuration options
44
+ * @returns A new CodeModel instance
45
+ */
46
+ static new(options: CodeOptions): CodeModel;
47
+ /**
48
+ * Create a copy with updated fields.
49
+ */
50
+ private with;
51
+ /**
52
+ * Sets the file to display and triggers highlighting.
53
+ * @param filename - Path to the file to display
54
+ * @returns Command to read and highlight the file
55
+ */
56
+ setFileName(filename: string): [CodeModel, Cmd<Msg>];
57
+ /**
58
+ * Sets whether the component is active (receives input).
59
+ * @param active - Whether the component should be active
60
+ * @returns Updated model
61
+ */
62
+ setIsActive(active: boolean): CodeModel;
63
+ /**
64
+ * Sets the syntax highlighting theme.
65
+ * @param theme - The theme name (e.g., "dracula", "monokai")
66
+ * @returns Updated model
67
+ */
68
+ setSyntaxTheme(theme: string): CodeModel;
69
+ /**
70
+ * Sets the viewport dimensions.
71
+ * @param width - Width in characters
72
+ * @param height - Height in lines
73
+ * @returns Updated model
74
+ */
75
+ setSize(width: number, height: number): CodeModel;
76
+ /**
77
+ * Scrolls to the top of the viewport.
78
+ * @returns Updated model
79
+ */
80
+ gotoTop(): CodeModel;
81
+ /**
82
+ * Tea init hook.
83
+ */
84
+ init(): Cmd<Msg>;
85
+ /**
86
+ * Handles messages (keyboard navigation when active).
87
+ * @param msg - The message to handle
88
+ * @returns Tuple of updated model and command
89
+ */
90
+ update(msg: Msg): [CodeModel, Cmd<Msg>];
91
+ /**
92
+ * Returns the rendered string.
93
+ * @returns The viewport view
94
+ */
95
+ view(): string;
96
+ }
97
+
98
+ /**
99
+ * Message containing syntax-highlighted content after async processing.
100
+ * @public
101
+ */
102
+ declare class SyntaxMsg {
103
+ readonly content: string;
104
+ readonly _tag = "code-syntax";
105
+ constructor(content: string);
106
+ }
107
+ /**
108
+ * Message containing an error if file read or highlighting fails.
109
+ * @public
110
+ */
111
+ declare class ErrorMsg {
112
+ readonly error: Error;
113
+ readonly _tag = "code-error";
114
+ constructor(error: Error);
115
+ }
116
+
117
+ export { CodeModel, type CodeOptions, ErrorMsg, SyntaxMsg, highlight };
@@ -0,0 +1,117 @@
1
+ import { Cmd, Msg } from '@boba-cli/tea';
2
+ import { FileSystemAdapter, PathAdapter } from '@boba-cli/machine';
3
+ import { ViewportModel } from '@boba-cli/viewport';
4
+
5
+ /**
6
+ * Options for creating a code model.
7
+ * @public
8
+ */
9
+ interface CodeOptions {
10
+ /** Filesystem adapter for file operations */
11
+ filesystem: FileSystemAdapter;
12
+ /** Path adapter for path operations */
13
+ path: PathAdapter;
14
+ active?: boolean;
15
+ syntaxTheme?: string;
16
+ width?: number;
17
+ height?: number;
18
+ }
19
+ /**
20
+ * Highlight returns a syntax-highlighted string of text.
21
+ * @param content - The source code content to highlight
22
+ * @param extension - The file extension (e.g., ".ts", ".go")
23
+ * @param theme - The syntax theme to use (default: "dracula")
24
+ * @returns Promise resolving to the highlighted content string
25
+ * @public
26
+ */
27
+ declare function highlight(content: string, extension: string, theme?: string): Promise<string>;
28
+ /**
29
+ * Model representing a syntax-highlighted code viewer.
30
+ * @public
31
+ */
32
+ declare class CodeModel {
33
+ readonly viewport: ViewportModel;
34
+ readonly active: boolean;
35
+ readonly filename: string;
36
+ readonly highlightedContent: string;
37
+ readonly syntaxTheme: string;
38
+ readonly filesystem: FileSystemAdapter;
39
+ readonly path: PathAdapter;
40
+ private constructor();
41
+ /**
42
+ * Creates a new code model instance.
43
+ * @param options - Configuration options
44
+ * @returns A new CodeModel instance
45
+ */
46
+ static new(options: CodeOptions): CodeModel;
47
+ /**
48
+ * Create a copy with updated fields.
49
+ */
50
+ private with;
51
+ /**
52
+ * Sets the file to display and triggers highlighting.
53
+ * @param filename - Path to the file to display
54
+ * @returns Command to read and highlight the file
55
+ */
56
+ setFileName(filename: string): [CodeModel, Cmd<Msg>];
57
+ /**
58
+ * Sets whether the component is active (receives input).
59
+ * @param active - Whether the component should be active
60
+ * @returns Updated model
61
+ */
62
+ setIsActive(active: boolean): CodeModel;
63
+ /**
64
+ * Sets the syntax highlighting theme.
65
+ * @param theme - The theme name (e.g., "dracula", "monokai")
66
+ * @returns Updated model
67
+ */
68
+ setSyntaxTheme(theme: string): CodeModel;
69
+ /**
70
+ * Sets the viewport dimensions.
71
+ * @param width - Width in characters
72
+ * @param height - Height in lines
73
+ * @returns Updated model
74
+ */
75
+ setSize(width: number, height: number): CodeModel;
76
+ /**
77
+ * Scrolls to the top of the viewport.
78
+ * @returns Updated model
79
+ */
80
+ gotoTop(): CodeModel;
81
+ /**
82
+ * Tea init hook.
83
+ */
84
+ init(): Cmd<Msg>;
85
+ /**
86
+ * Handles messages (keyboard navigation when active).
87
+ * @param msg - The message to handle
88
+ * @returns Tuple of updated model and command
89
+ */
90
+ update(msg: Msg): [CodeModel, Cmd<Msg>];
91
+ /**
92
+ * Returns the rendered string.
93
+ * @returns The viewport view
94
+ */
95
+ view(): string;
96
+ }
97
+
98
+ /**
99
+ * Message containing syntax-highlighted content after async processing.
100
+ * @public
101
+ */
102
+ declare class SyntaxMsg {
103
+ readonly content: string;
104
+ readonly _tag = "code-syntax";
105
+ constructor(content: string);
106
+ }
107
+ /**
108
+ * Message containing an error if file read or highlighting fails.
109
+ * @public
110
+ */
111
+ declare class ErrorMsg {
112
+ readonly error: Error;
113
+ readonly _tag = "code-error";
114
+ constructor(error: Error);
115
+ }
116
+
117
+ export { CodeModel, type CodeOptions, ErrorMsg, SyntaxMsg, highlight };
package/dist/index.js ADDED
@@ -0,0 +1,240 @@
1
+ import { Style } from '@boba-cli/chapstick';
2
+ import { readFileContent } from '@boba-cli/filesystem';
3
+ import { ViewportModel } from '@boba-cli/viewport';
4
+ import { getHighlighter } from 'shiki';
5
+
6
+ // src/model.ts
7
+
8
+ // src/messages.ts
9
+ var SyntaxMsg = class {
10
+ constructor(content) {
11
+ this.content = content;
12
+ }
13
+ _tag = "code-syntax";
14
+ };
15
+ var ErrorMsg = class {
16
+ constructor(error) {
17
+ this.error = error;
18
+ }
19
+ _tag = "code-error";
20
+ };
21
+
22
+ // src/model.ts
23
+ async function highlight(content, extension, theme = "dracula") {
24
+ try {
25
+ const highlighter = await getHighlighter({
26
+ themes: [theme],
27
+ langs: []
28
+ });
29
+ const ext = extension.startsWith(".") ? extension.slice(1) : extension;
30
+ let lang = ext || "text";
31
+ try {
32
+ await highlighter.loadLanguage(lang);
33
+ } catch {
34
+ lang = "text";
35
+ try {
36
+ await highlighter.loadLanguage("text");
37
+ } catch {
38
+ return content;
39
+ }
40
+ }
41
+ const tokens = highlighter.codeToTokens(content, {
42
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-assignment
43
+ lang,
44
+ theme
45
+ });
46
+ let result = "";
47
+ for (const line of tokens.tokens) {
48
+ for (const token of line) {
49
+ if (token.color) {
50
+ result += `\x1B[38;2;${hexToRgb(token.color)}m${token.content}\x1B[0m`;
51
+ } else {
52
+ result += token.content;
53
+ }
54
+ }
55
+ result += "\n";
56
+ }
57
+ return result.trimEnd();
58
+ } catch (error) {
59
+ const errorMessage = error instanceof Error ? error.message : String(error);
60
+ throw new Error(`Syntax highlighting failed: ${errorMessage}`);
61
+ }
62
+ }
63
+ function hexToRgb(hex) {
64
+ const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
65
+ if (!result) return "255;255;255";
66
+ return `${parseInt(result[1], 16)};${parseInt(result[2], 16)};${parseInt(result[3], 16)}`;
67
+ }
68
+ function readFileContentCmd(fsAdapter, pathAdapter, fileName, syntaxTheme) {
69
+ return async () => {
70
+ try {
71
+ const content = await readFileContent(fsAdapter, fileName);
72
+ const extension = pathAdapter.extname(fileName);
73
+ const highlightedContent = await highlight(
74
+ content,
75
+ extension,
76
+ syntaxTheme
77
+ );
78
+ return new SyntaxMsg(highlightedContent);
79
+ } catch (error) {
80
+ return new ErrorMsg(
81
+ error instanceof Error ? error : new Error(String(error))
82
+ );
83
+ }
84
+ };
85
+ }
86
+ var CodeModel = class _CodeModel {
87
+ viewport;
88
+ active;
89
+ filename;
90
+ highlightedContent;
91
+ syntaxTheme;
92
+ filesystem;
93
+ path;
94
+ constructor(options) {
95
+ this.viewport = options.viewport;
96
+ this.active = options.active;
97
+ this.filename = options.filename;
98
+ this.highlightedContent = options.highlightedContent;
99
+ this.syntaxTheme = options.syntaxTheme;
100
+ this.filesystem = options.filesystem;
101
+ this.path = options.path;
102
+ }
103
+ /**
104
+ * Creates a new code model instance.
105
+ * @param options - Configuration options
106
+ * @returns A new CodeModel instance
107
+ */
108
+ static new(options) {
109
+ return new _CodeModel({
110
+ viewport: ViewportModel.new({
111
+ width: options.width ?? 0,
112
+ height: options.height ?? 0
113
+ }),
114
+ active: options.active ?? false,
115
+ filename: "",
116
+ highlightedContent: "",
117
+ syntaxTheme: options.syntaxTheme ?? "dracula",
118
+ filesystem: options.filesystem,
119
+ path: options.path
120
+ });
121
+ }
122
+ /**
123
+ * Create a copy with updated fields.
124
+ */
125
+ with(partial) {
126
+ return new _CodeModel({
127
+ viewport: partial.viewport ?? this.viewport,
128
+ active: partial.active ?? this.active,
129
+ filename: partial.filename ?? this.filename,
130
+ highlightedContent: partial.highlightedContent ?? this.highlightedContent,
131
+ syntaxTheme: partial.syntaxTheme ?? this.syntaxTheme,
132
+ filesystem: partial.filesystem ?? this.filesystem,
133
+ path: partial.path ?? this.path
134
+ });
135
+ }
136
+ /**
137
+ * Sets the file to display and triggers highlighting.
138
+ * @param filename - Path to the file to display
139
+ * @returns Command to read and highlight the file
140
+ */
141
+ setFileName(filename) {
142
+ const updated = this.with({ filename });
143
+ return [updated, readFileContentCmd(this.filesystem, this.path, filename, this.syntaxTheme)];
144
+ }
145
+ /**
146
+ * Sets whether the component is active (receives input).
147
+ * @param active - Whether the component should be active
148
+ * @returns Updated model
149
+ */
150
+ setIsActive(active) {
151
+ if (this.active === active) return this;
152
+ return this.with({ active });
153
+ }
154
+ /**
155
+ * Sets the syntax highlighting theme.
156
+ * @param theme - The theme name (e.g., "dracula", "monokai")
157
+ * @returns Updated model
158
+ */
159
+ setSyntaxTheme(theme) {
160
+ if (this.syntaxTheme === theme) return this;
161
+ return this.with({ syntaxTheme: theme });
162
+ }
163
+ /**
164
+ * Sets the viewport dimensions.
165
+ * @param width - Width in characters
166
+ * @param height - Height in lines
167
+ * @returns Updated model
168
+ */
169
+ setSize(width, height) {
170
+ const nextViewport = this.viewport.setWidth(width).setHeight(height);
171
+ if (nextViewport === this.viewport) return this;
172
+ return this.with({ viewport: nextViewport });
173
+ }
174
+ /**
175
+ * Scrolls to the top of the viewport.
176
+ * @returns Updated model
177
+ */
178
+ gotoTop() {
179
+ const nextViewport = this.viewport.scrollToTop();
180
+ if (nextViewport === this.viewport) return this;
181
+ return this.with({ viewport: nextViewport });
182
+ }
183
+ /**
184
+ * Tea init hook.
185
+ */
186
+ init() {
187
+ return this.viewport.init();
188
+ }
189
+ /**
190
+ * Handles messages (keyboard navigation when active).
191
+ * @param msg - The message to handle
192
+ * @returns Tuple of updated model and command
193
+ */
194
+ update(msg) {
195
+ if (msg instanceof SyntaxMsg) {
196
+ const content = msg.content;
197
+ const style = new Style().width(this.viewport.width).alignHorizontal("left");
198
+ const rendered = style.render(content);
199
+ const nextViewport = this.viewport.setContent(rendered);
200
+ return [
201
+ this.with({
202
+ highlightedContent: rendered,
203
+ viewport: nextViewport
204
+ }),
205
+ null
206
+ ];
207
+ }
208
+ if (msg instanceof ErrorMsg) {
209
+ const errorContent = `Error: ${msg.error.message}`;
210
+ const style = new Style().width(this.viewport.width).alignHorizontal("left");
211
+ const rendered = style.render(errorContent);
212
+ const nextViewport = this.viewport.setContent(rendered);
213
+ return [
214
+ this.with({
215
+ highlightedContent: rendered,
216
+ viewport: nextViewport
217
+ }),
218
+ null
219
+ ];
220
+ }
221
+ if (this.active) {
222
+ const [nextViewport, cmd] = this.viewport.update(msg);
223
+ if (nextViewport !== this.viewport) {
224
+ return [this.with({ viewport: nextViewport }), cmd];
225
+ }
226
+ }
227
+ return [this, null];
228
+ }
229
+ /**
230
+ * Returns the rendered string.
231
+ * @returns The viewport view
232
+ */
233
+ view() {
234
+ return this.viewport.view();
235
+ }
236
+ };
237
+
238
+ export { CodeModel, ErrorMsg, SyntaxMsg, highlight };
239
+ //# sourceMappingURL=index.js.map
240
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/messages.ts","../src/model.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * Message containing syntax-highlighted content after async processing.\n * @public\n */\nexport class SyntaxMsg {\n readonly _tag = 'code-syntax'\n constructor(public readonly content: string) {}\n}\n\n/**\n * Message containing an error if file read or highlighting fails.\n * @public\n */\nexport class ErrorMsg {\n readonly _tag = 'code-error'\n constructor(public readonly error: Error) {}\n}\n","import { Style } from '@boba-cli/chapstick'\nimport { readFileContent } from '@boba-cli/filesystem'\nimport { type Cmd, type Msg } from '@boba-cli/tea'\nimport type { FileSystemAdapter, PathAdapter } from '@boba-cli/machine'\nimport { ViewportModel } from '@boba-cli/viewport'\nimport { getHighlighter } from 'shiki'\nimport { ErrorMsg, SyntaxMsg } from './messages.js'\n\n/**\n * Options for creating a code model.\n * @public\n */\nexport interface CodeOptions {\n /** Filesystem adapter for file operations */\n filesystem: FileSystemAdapter\n /** Path adapter for path operations */\n path: PathAdapter\n active?: boolean\n syntaxTheme?: string\n width?: number\n height?: number\n}\n\n/**\n * Highlight returns a syntax-highlighted string of text.\n * @param content - The source code content to highlight\n * @param extension - The file extension (e.g., \".ts\", \".go\")\n * @param theme - The syntax theme to use (default: \"dracula\")\n * @returns Promise resolving to the highlighted content string\n * @public\n */\nexport async function highlight(\n content: string,\n extension: string,\n theme: string = 'dracula',\n): Promise<string> {\n try {\n const highlighter = await getHighlighter({\n themes: [theme],\n langs: [],\n })\n\n // Map extension to language - use a safer approach\n const ext = extension.startsWith('.') ? extension.slice(1) : extension\n let lang = ext || 'text'\n\n // Load the language if needed, fallback to text\n try {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument\n await highlighter.loadLanguage(lang as any)\n } catch {\n // If language loading fails, use 'text' as fallback\n lang = 'text'\n try {\n await highlighter.loadLanguage('text')\n } catch {\n // If even text fails, just return plain content\n return content\n }\n }\n\n // Get tokens from highlighter\n const tokens = highlighter.codeToTokens(content, {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-assignment\n lang: lang as any,\n theme: theme,\n })\n\n // Convert tokens to ANSI colored text\n let result = ''\n for (const line of tokens.tokens) {\n for (const token of line) {\n if (token.color) {\n // Simple ANSI color conversion (this is a basic approach)\n result += `\\x1b[38;2;${hexToRgb(token.color)}m${token.content}\\x1b[0m`\n } else {\n result += token.content\n }\n }\n result += '\\n'\n }\n\n return result.trimEnd()\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error)\n throw new Error(`Syntax highlighting failed: ${errorMessage}`)\n }\n}\n\n/**\n * Convert hex color to RGB values for ANSI\n */\nfunction hexToRgb(hex: string): string {\n const result = /^#?([a-f\\d]{2})([a-f\\d]{2})([a-f\\d]{2})$/i.exec(hex)\n if (!result) return '255;255;255'\n return `${parseInt(result[1]!, 16)};${parseInt(result[2]!, 16)};${parseInt(result[3]!, 16)}`\n}\n\n/**\n * Read file content and highlight it asynchronously.\n */\nfunction readFileContentCmd(\n fsAdapter: FileSystemAdapter,\n pathAdapter: PathAdapter,\n fileName: string,\n syntaxTheme: string,\n): Cmd<Msg> {\n return async (): Promise<Msg> => {\n try {\n const content = await readFileContent(fsAdapter, fileName)\n const extension = pathAdapter.extname(fileName)\n const highlightedContent = await highlight(\n content,\n extension,\n syntaxTheme,\n )\n return new SyntaxMsg(highlightedContent)\n } catch (error) {\n return new ErrorMsg(\n error instanceof Error ? error : new Error(String(error)),\n )\n }\n }\n}\n\n/**\n * Model representing a syntax-highlighted code viewer.\n * @public\n */\nexport class CodeModel {\n readonly viewport: ViewportModel\n readonly active: boolean\n readonly filename: string\n readonly highlightedContent: string\n readonly syntaxTheme: string\n readonly filesystem: FileSystemAdapter\n readonly path: PathAdapter\n\n private constructor(options: {\n viewport: ViewportModel\n active: boolean\n filename: string\n highlightedContent: string\n syntaxTheme: string\n filesystem: FileSystemAdapter\n path: PathAdapter\n }) {\n this.viewport = options.viewport\n this.active = options.active\n this.filename = options.filename\n this.highlightedContent = options.highlightedContent\n this.syntaxTheme = options.syntaxTheme\n this.filesystem = options.filesystem\n this.path = options.path\n }\n\n /**\n * Creates a new code model instance.\n * @param options - Configuration options\n * @returns A new CodeModel instance\n */\n static new(options: CodeOptions): CodeModel {\n return new CodeModel({\n viewport: ViewportModel.new({\n width: options.width ?? 0,\n height: options.height ?? 0,\n }),\n active: options.active ?? false,\n filename: '',\n highlightedContent: '',\n syntaxTheme: options.syntaxTheme ?? 'dracula',\n filesystem: options.filesystem,\n path: options.path,\n })\n }\n\n /**\n * Create a copy with updated fields.\n */\n private with(\n partial: Partial<{\n viewport: ViewportModel\n active: boolean\n filename: string\n highlightedContent: string\n syntaxTheme: string\n filesystem: FileSystemAdapter\n path: PathAdapter\n }>,\n ): CodeModel {\n return new CodeModel({\n viewport: partial.viewport ?? this.viewport,\n active: partial.active ?? this.active,\n filename: partial.filename ?? this.filename,\n highlightedContent: partial.highlightedContent ?? this.highlightedContent,\n syntaxTheme: partial.syntaxTheme ?? this.syntaxTheme,\n filesystem: partial.filesystem ?? this.filesystem,\n path: partial.path ?? this.path,\n })\n }\n\n /**\n * Sets the file to display and triggers highlighting.\n * @param filename - Path to the file to display\n * @returns Command to read and highlight the file\n */\n setFileName(filename: string): [CodeModel, Cmd<Msg>] {\n const updated = this.with({ filename })\n return [updated, readFileContentCmd(this.filesystem, this.path, filename, this.syntaxTheme)]\n }\n\n /**\n * Sets whether the component is active (receives input).\n * @param active - Whether the component should be active\n * @returns Updated model\n */\n setIsActive(active: boolean): CodeModel {\n if (this.active === active) return this\n return this.with({ active })\n }\n\n /**\n * Sets the syntax highlighting theme.\n * @param theme - The theme name (e.g., \"dracula\", \"monokai\")\n * @returns Updated model\n */\n setSyntaxTheme(theme: string): CodeModel {\n if (this.syntaxTheme === theme) return this\n return this.with({ syntaxTheme: theme })\n }\n\n /**\n * Sets the viewport dimensions.\n * @param width - Width in characters\n * @param height - Height in lines\n * @returns Updated model\n */\n setSize(width: number, height: number): CodeModel {\n const nextViewport = this.viewport.setWidth(width).setHeight(height)\n if (nextViewport === this.viewport) return this\n return this.with({ viewport: nextViewport })\n }\n\n /**\n * Scrolls to the top of the viewport.\n * @returns Updated model\n */\n gotoTop(): CodeModel {\n const nextViewport = this.viewport.scrollToTop()\n if (nextViewport === this.viewport) return this\n return this.with({ viewport: nextViewport })\n }\n\n /**\n * Tea init hook.\n */\n init(): Cmd<Msg> {\n return this.viewport.init()\n }\n\n /**\n * Handles messages (keyboard navigation when active).\n * @param msg - The message to handle\n * @returns Tuple of updated model and command\n */\n update(msg: Msg): [CodeModel, Cmd<Msg>] {\n if (msg instanceof SyntaxMsg) {\n const content = msg.content\n // Apply width for consistent line lengths and left-align to pad lines\n // Viewport handles height/scrolling - don't use .height() here as it truncates\n const style = new Style()\n .width(this.viewport.width)\n .alignHorizontal('left')\n const rendered = style.render(content)\n const nextViewport = this.viewport.setContent(rendered)\n return [\n this.with({\n highlightedContent: rendered,\n viewport: nextViewport,\n }),\n null,\n ]\n }\n\n if (msg instanceof ErrorMsg) {\n const errorContent = `Error: ${msg.error.message}`\n const style = new Style()\n .width(this.viewport.width)\n .alignHorizontal('left')\n const rendered = style.render(errorContent)\n const nextViewport = this.viewport.setContent(rendered)\n return [\n this.with({\n highlightedContent: rendered,\n viewport: nextViewport,\n }),\n null,\n ]\n }\n\n if (this.active) {\n const [nextViewport, cmd] = this.viewport.update(msg)\n if (nextViewport !== this.viewport) {\n return [this.with({ viewport: nextViewport }), cmd]\n }\n }\n\n return [this, null]\n }\n\n /**\n * Returns the rendered string.\n * @returns The viewport view\n */\n view(): string {\n return this.viewport.view()\n }\n}\n"]}
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "@boba-cli/code",
3
+ "description": "Syntax-highlighted code viewer with scrollable viewport for Boba terminal UIs",
4
+ "version": "0.1.0-alpha.2",
5
+ "dependencies": {
6
+ "shiki": "^1.24.2",
7
+ "@boba-cli/chapstick": "0.1.0-alpha.2",
8
+ "@boba-cli/filesystem": "0.1.0-alpha.2",
9
+ "@boba-cli/machine": "0.1.0-alpha.1",
10
+ "@boba-cli/tea": "0.1.0-alpha.1",
11
+ "@boba-cli/viewport": "0.1.0-alpha.2"
12
+ },
13
+ "devDependencies": {
14
+ "@types/node": "^24.10.2",
15
+ "typescript": "5.8.2",
16
+ "vitest": "^4.0.16"
17
+ },
18
+ "engines": {
19
+ "node": ">=20.0.0"
20
+ },
21
+ "exports": {
22
+ ".": {
23
+ "import": {
24
+ "types": "./dist/index.d.ts",
25
+ "default": "./dist/index.js"
26
+ },
27
+ "require": {
28
+ "types": "./dist/index.d.cts",
29
+ "default": "./dist/index.cjs"
30
+ }
31
+ },
32
+ "./package.json": "./package.json"
33
+ },
34
+ "files": [
35
+ "dist"
36
+ ],
37
+ "main": "./dist/index.cjs",
38
+ "module": "./dist/index.js",
39
+ "type": "module",
40
+ "types": "./dist/index.d.ts",
41
+ "scripts": {
42
+ "build": "tsup",
43
+ "check:api-report": "pnpm run generate:api-report",
44
+ "check:eslint": "pnpm run lint",
45
+ "generate:api-report": "api-extractor run --local",
46
+ "lint": "eslint \"{src,test}/**/*.{ts,tsx}\"",
47
+ "test": "vitest run"
48
+ }
49
+ }