opencode-latex 0.1.0 → 0.2.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/CHANGELOG.md +13 -0
- package/README.md +24 -4
- package/package.json +1 -1
- package/src/opentui-math.ts +68 -27
- package/src/tui.ts +83 -3
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project are documented here.
|
|
4
4
|
|
|
5
|
+
## 0.2.0 - 2026-09-19
|
|
6
|
+
|
|
7
|
+
- Adds a **LaTeX: Set default render mode** command to the command palette and `/latex-render-mode` slash command.
|
|
8
|
+
- Persists the selected automatic, Kitty, or Unicode-cell mode across TUI restarts.
|
|
9
|
+
- Applies the selected default whenever LaTeX formulas are rendered.
|
|
10
|
+
- Allows the saved preference to be cleared so plugin configuration becomes authoritative again.
|
|
11
|
+
|
|
12
|
+
## 0.1.1 - 2026-09-19
|
|
13
|
+
|
|
14
|
+
- Removes visible right and bottom seams around SIXEL-rendered formulas in embedded xterm.js terminals such as `obsidian-opencode`.
|
|
15
|
+
- Pads SIXEL images to complete terminal-cell boundaries while preserving the formula's pixel scale.
|
|
16
|
+
- Adds regression coverage for sub-cell image dimensions and background compositing.
|
|
17
|
+
|
|
5
18
|
## 0.1.0 - 2026-09-19
|
|
6
19
|
|
|
7
20
|
Initial public release.
|
package/README.md
CHANGED
|
@@ -9,6 +9,16 @@ Render readable LaTeX in OpenCode v2 terminal conversations.
|
|
|
9
9
|
|
|
10
10
|
Graphical output uses Kitty graphics in terminals such as Ghostty and Kitty, or SIXEL in compatible xterm.js terminals such as `obsidian-opencode`. Other terminals and multiplexers receive a selectable Unicode-cell fallback.
|
|
11
11
|
|
|
12
|
+
## Demo
|
|
13
|
+
|
|
14
|
+

|
|
15
|
+
|
|
16
|
+
*Rendered equations stay aligned with the surrounding conversation.*
|
|
17
|
+
|
|
18
|
+

|
|
19
|
+
|
|
20
|
+
*Larger matrices and derivations remain crisp and readable.*
|
|
21
|
+
|
|
12
22
|
> [!NOTE]
|
|
13
23
|
> This is an initial `0.1.x` release. Kitty output preserves real PNG transparency. SIXEL output is composited against the covered OpenTUI cell backgrounds because SIXEL cannot reproduce Kitty's alpha compositing exactly.
|
|
14
24
|
|
|
@@ -23,7 +33,7 @@ Graphical output uses Kitty graphics in terminals such as Ghostty and Kitty, or
|
|
|
23
33
|
Install the package globally through OpenCode:
|
|
24
34
|
|
|
25
35
|
```sh
|
|
26
|
-
opencode plugin add opencode-latex@0.
|
|
36
|
+
opencode plugin add opencode-latex@0.2.0
|
|
27
37
|
```
|
|
28
38
|
|
|
29
39
|
Alternatively, add it to `opencode.jsonc`:
|
|
@@ -31,7 +41,7 @@ Alternatively, add it to `opencode.jsonc`:
|
|
|
31
41
|
```jsonc
|
|
32
42
|
{
|
|
33
43
|
"$schema": "https://opencode.ai/config.json",
|
|
34
|
-
"plugins": ["opencode-latex@0.
|
|
44
|
+
"plugins": ["opencode-latex@0.2.0"]
|
|
35
45
|
}
|
|
36
46
|
```
|
|
37
47
|
|
|
@@ -72,6 +82,16 @@ I^2 &= \int_0^{2\pi}\int_0^\infty e^{-r^2}r\,dr\,d\theta \\
|
|
|
72
82
|
|
|
73
83
|
Use the `latex` fence exactly; this plugin registers that language without replacing other Markdown renderers.
|
|
74
84
|
|
|
85
|
+
### Choose a default render mode
|
|
86
|
+
|
|
87
|
+
Open the command palette with `Ctrl+P` and select **LaTeX: Set default render mode**, or run:
|
|
88
|
+
|
|
89
|
+
```text
|
|
90
|
+
/latex-render-mode
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Choose automatic graphics detection, forced Kitty graphics, or portable Unicode cells. The selection is saved across TUI restarts and applies whenever formulas are rendered. Choose **Use configured default** to return to the `graphicsMode` value from plugin configuration.
|
|
94
|
+
|
|
75
95
|
## Options
|
|
76
96
|
|
|
77
97
|
Options are optional:
|
|
@@ -80,7 +100,7 @@ Options are optional:
|
|
|
80
100
|
{
|
|
81
101
|
"plugins": [
|
|
82
102
|
{
|
|
83
|
-
"package": "opencode-latex@0.
|
|
103
|
+
"package": "opencode-latex@0.2.0",
|
|
84
104
|
"options": {
|
|
85
105
|
"color": "#d4d4d4",
|
|
86
106
|
"graphicsMode": "auto",
|
|
@@ -93,7 +113,7 @@ Options are optional:
|
|
|
93
113
|
```
|
|
94
114
|
|
|
95
115
|
- `color` sets the equation foreground. Its default follows OpenCode's light or dark theme.
|
|
96
|
-
- `graphicsMode` selects `"auto"`, forced `"kitty"`, or portable `"cells"` rendering. The default is `"auto"
|
|
116
|
+
- `graphicsMode` selects `"auto"`, forced `"kitty"`, or portable `"cells"` rendering. The default is `"auto"`; a saved command-palette selection overrides it until **Use configured default** is selected.
|
|
97
117
|
- `fontSize` sets the graphical MathJax font size. The default is `20`.
|
|
98
118
|
- `pixelRatio` increases raster sharpness without changing the intended cell footprint. The default is `2`.
|
|
99
119
|
|
package/package.json
CHANGED
package/src/opentui-math.ts
CHANGED
|
@@ -49,6 +49,12 @@ interface RuntimeNativeImage {
|
|
|
49
49
|
readonly height: number
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
interface PreparedRuntimeImage {
|
|
53
|
+
readonly image: RuntimeNativeImage
|
|
54
|
+
readonly pixelWidth: number
|
|
55
|
+
readonly pixelHeight: number
|
|
56
|
+
}
|
|
57
|
+
|
|
52
58
|
/**
|
|
53
59
|
* A narrow fork of opentui-math's graphical renderable. MathJax still creates
|
|
54
60
|
* the PNG, while OpenTUI's image buffer owns Kitty/SIXEL transport and cleanup.
|
|
@@ -231,23 +237,33 @@ export class GraphicalLatexRenderable extends LatexRenderable {
|
|
|
231
237
|
const x = originX + Math.floor((this.width - columns) / 2)
|
|
232
238
|
const y = originY + Math.floor((this.height - rows) / 2)
|
|
233
239
|
const protocol = this.requestedImageProtocol()
|
|
234
|
-
const
|
|
235
|
-
? this.ensureSixelRuntimeImage(
|
|
236
|
-
|
|
237
|
-
|
|
240
|
+
const prepared = this.effectiveGraphicsProtocol === "sixel"
|
|
241
|
+
? this.ensureSixelRuntimeImage(
|
|
242
|
+
buffer,
|
|
243
|
+
x,
|
|
244
|
+
y,
|
|
245
|
+
columns,
|
|
246
|
+
rows,
|
|
247
|
+
pixelWidth,
|
|
248
|
+
pixelHeight,
|
|
249
|
+
Math.max(1, Math.round(columns * cellWidth)),
|
|
250
|
+
Math.max(1, Math.round(rows * cellHeight)),
|
|
251
|
+
)
|
|
252
|
+
: prepareRuntimeImage(this.ensureRuntimeImage(buffer), pixelWidth, pixelHeight)
|
|
253
|
+
if (!prepared) return
|
|
238
254
|
|
|
239
255
|
buffer.drawImage(
|
|
240
|
-
|
|
256
|
+
prepared.image as never,
|
|
241
257
|
x,
|
|
242
258
|
y,
|
|
243
259
|
columns,
|
|
244
260
|
rows,
|
|
245
|
-
pixelWidth,
|
|
246
|
-
pixelHeight,
|
|
261
|
+
prepared.pixelWidth,
|
|
262
|
+
prepared.pixelHeight,
|
|
247
263
|
0,
|
|
248
264
|
0,
|
|
249
|
-
|
|
250
|
-
|
|
265
|
+
prepared.image.width,
|
|
266
|
+
prepared.image.height,
|
|
251
267
|
protocol,
|
|
252
268
|
)
|
|
253
269
|
}
|
|
@@ -394,38 +410,55 @@ export class GraphicalLatexRenderable extends LatexRenderable {
|
|
|
394
410
|
y: number,
|
|
395
411
|
columns: number,
|
|
396
412
|
rows: number,
|
|
397
|
-
|
|
413
|
+
contentPixelWidth: number,
|
|
414
|
+
contentPixelHeight: number,
|
|
415
|
+
cellPixelWidth: number,
|
|
416
|
+
cellPixelHeight: number,
|
|
417
|
+
): PreparedRuntimeImage | undefined {
|
|
398
418
|
const source = this.ensureRuntimeImage(buffer)
|
|
399
419
|
if (!source) return undefined
|
|
400
420
|
|
|
401
421
|
const backgrounds = readCellBackgrounds(buffer, x, y, columns, rows)
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
422
|
+
const canvasWidth = Math.max(
|
|
423
|
+
source.width,
|
|
424
|
+
Math.ceil(source.width * cellPixelWidth / contentPixelWidth),
|
|
425
|
+
)
|
|
426
|
+
const canvasHeight = Math.max(
|
|
427
|
+
source.height,
|
|
428
|
+
Math.ceil(source.height * cellPixelHeight / contentPixelHeight),
|
|
429
|
+
)
|
|
430
|
+
const backdropKey = [
|
|
431
|
+
x,
|
|
432
|
+
y,
|
|
433
|
+
columns,
|
|
434
|
+
rows,
|
|
435
|
+
canvasWidth,
|
|
436
|
+
canvasHeight,
|
|
437
|
+
hashBytes(backgrounds),
|
|
438
|
+
].join(":")
|
|
408
439
|
if (this.sixelRuntimeImage?.lib === source.lib && this.sixelBackdropKey === backdropKey) {
|
|
409
|
-
return this.sixelRuntimeImage
|
|
440
|
+
return prepareRuntimeImage(this.sixelRuntimeImage, cellPixelWidth, cellPixelHeight)
|
|
410
441
|
}
|
|
411
442
|
|
|
412
443
|
this.disposeSixelRuntimeImage()
|
|
413
|
-
const pixels = new Uint8Array(
|
|
414
|
-
const copyStatus = source.lib.imageCopyPixels(source.ptr, pixels,
|
|
415
|
-
if (copyStatus !== 0) return source
|
|
416
|
-
|
|
417
|
-
compositeCellBackgrounds(pixels,
|
|
418
|
-
const created = source.lib.imageCreateFromRgba(pixels,
|
|
419
|
-
if (created.status !== 0 || !created.handle)
|
|
444
|
+
const pixels = new Uint8Array(canvasWidth * canvasHeight * 4)
|
|
445
|
+
const copyStatus = source.lib.imageCopyPixels(source.ptr, pixels, canvasWidth * 4, false)
|
|
446
|
+
if (copyStatus !== 0) return prepareRuntimeImage(source, contentPixelWidth, contentPixelHeight)
|
|
447
|
+
|
|
448
|
+
compositeCellBackgrounds(pixels, canvasWidth, canvasHeight, backgrounds, columns, rows)
|
|
449
|
+
const created = source.lib.imageCreateFromRgba(pixels, canvasWidth, canvasHeight, canvasWidth * 4)
|
|
450
|
+
if (created.status !== 0 || !created.handle) {
|
|
451
|
+
return prepareRuntimeImage(source, contentPixelWidth, contentPixelHeight)
|
|
452
|
+
}
|
|
420
453
|
|
|
421
454
|
this.sixelRuntimeImage = {
|
|
422
455
|
lib: source.lib,
|
|
423
456
|
ptr: created.handle,
|
|
424
|
-
width:
|
|
425
|
-
height:
|
|
457
|
+
width: canvasWidth,
|
|
458
|
+
height: canvasHeight,
|
|
426
459
|
}
|
|
427
460
|
this.sixelBackdropKey = backdropKey
|
|
428
|
-
return this.sixelRuntimeImage
|
|
461
|
+
return prepareRuntimeImage(this.sixelRuntimeImage, cellPixelWidth, cellPixelHeight)
|
|
429
462
|
}
|
|
430
463
|
|
|
431
464
|
private disposeRuntimeImage(): void {
|
|
@@ -444,6 +477,14 @@ export class GraphicalLatexRenderable extends LatexRenderable {
|
|
|
444
477
|
}
|
|
445
478
|
}
|
|
446
479
|
|
|
480
|
+
function prepareRuntimeImage(
|
|
481
|
+
image: RuntimeNativeImage | undefined,
|
|
482
|
+
pixelWidth: number,
|
|
483
|
+
pixelHeight: number,
|
|
484
|
+
): PreparedRuntimeImage | undefined {
|
|
485
|
+
return image ? { image, pixelWidth, pixelHeight } : undefined
|
|
486
|
+
}
|
|
487
|
+
|
|
447
488
|
function readCellBackgrounds(
|
|
448
489
|
buffer: OptimizedBuffer,
|
|
449
490
|
x: number,
|
package/src/tui.ts
CHANGED
|
@@ -4,6 +4,38 @@ import { GraphicalLatexRenderable } from "./opentui-math.js"
|
|
|
4
4
|
const LANGUAGE = "latex"
|
|
5
5
|
const GRAPHICS_MODES = ["auto", "kitty", "cells"] as const
|
|
6
6
|
type GraphicsMode = typeof GRAPHICS_MODES[number]
|
|
7
|
+
type RenderModeChoice = GraphicsMode | "configured"
|
|
8
|
+
|
|
9
|
+
interface Settings {
|
|
10
|
+
graphicsMode: GraphicsMode | null
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const RENDER_MODE_OPTIONS: ReadonlyArray<{
|
|
14
|
+
title: string
|
|
15
|
+
value: RenderModeChoice
|
|
16
|
+
description: string
|
|
17
|
+
}> = [
|
|
18
|
+
{
|
|
19
|
+
title: "Use configured default",
|
|
20
|
+
value: "configured",
|
|
21
|
+
description: "Follow graphicsMode from plugin configuration",
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
title: "Automatic",
|
|
25
|
+
value: "auto",
|
|
26
|
+
description: "Use Kitty or SIXEL when available, otherwise Unicode cells",
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
title: "Kitty graphics",
|
|
30
|
+
value: "kitty",
|
|
31
|
+
description: "Force the Kitty graphics protocol",
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
title: "Unicode cells",
|
|
35
|
+
value: "cells",
|
|
36
|
+
description: "Use the portable terminal-cell renderer",
|
|
37
|
+
},
|
|
38
|
+
]
|
|
7
39
|
|
|
8
40
|
function positiveNumber(value: unknown, fallback: number): number {
|
|
9
41
|
return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : fallback
|
|
@@ -15,6 +47,16 @@ function graphicsMode(value: unknown): GraphicsMode {
|
|
|
15
47
|
: "auto"
|
|
16
48
|
}
|
|
17
49
|
|
|
50
|
+
function savedGraphicsMode(value: unknown): GraphicsMode | null {
|
|
51
|
+
return typeof value === "string" && GRAPHICS_MODES.includes(value as GraphicsMode)
|
|
52
|
+
? value as GraphicsMode
|
|
53
|
+
: null
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function renderModeTitle(mode: GraphicsMode): string {
|
|
57
|
+
return RENDER_MODE_OPTIONS.find((option) => option.value === mode)?.title ?? mode
|
|
58
|
+
}
|
|
59
|
+
|
|
18
60
|
export function normalizeLatexSource(source: string): string {
|
|
19
61
|
return source.replace(/\\+/g, (run, offset: number) => {
|
|
20
62
|
if (run.length >= 4 && run.length % 2 === 0) return "\\".repeat(run.length / 2)
|
|
@@ -32,20 +74,58 @@ export default Plugin.define({
|
|
|
32
74
|
: context.themeMode === "light" ? "#24292f" : "#d4d4d4"
|
|
33
75
|
const fontSize = positiveNumber(context.options.fontSize, 20)
|
|
34
76
|
const pixelRatio = positiveNumber(context.options.pixelRatio, 2)
|
|
35
|
-
const
|
|
77
|
+
const configuredMode = graphicsMode(context.options.graphicsMode)
|
|
78
|
+
const [settings, updateSettings] = context.storage.store<Settings>("settings", {
|
|
79
|
+
initial: { graphicsMode: null },
|
|
80
|
+
})
|
|
81
|
+
const effectiveMode = (): GraphicsMode =>
|
|
82
|
+
savedGraphicsMode(settings.graphicsMode) ?? configuredMode
|
|
83
|
+
|
|
84
|
+
context.keymap.layer(() => ({
|
|
85
|
+
mode: "global",
|
|
86
|
+
commands: [
|
|
87
|
+
{
|
|
88
|
+
id: "opencode.latex.render-mode",
|
|
89
|
+
title: "Set default render mode",
|
|
90
|
+
description: "Choose how LaTeX formulas are rendered",
|
|
91
|
+
group: "LaTeX",
|
|
92
|
+
palette: true,
|
|
93
|
+
slash: { name: "latex-render-mode" },
|
|
94
|
+
run: async () => {
|
|
95
|
+
const selected = await context.ui.dialog.select<RenderModeChoice>({
|
|
96
|
+
title: "LaTeX render mode",
|
|
97
|
+
current: savedGraphicsMode(settings.graphicsMode) ?? "configured",
|
|
98
|
+
options: RENDER_MODE_OPTIONS,
|
|
99
|
+
})
|
|
100
|
+
if (selected === undefined) return
|
|
101
|
+
|
|
102
|
+
const preference = selected === "configured" ? null : selected
|
|
103
|
+
await updateSettings((draft) => {
|
|
104
|
+
draft.graphicsMode = preference
|
|
105
|
+
})
|
|
106
|
+
const mode = preference ?? configuredMode
|
|
107
|
+
context.ui.toast.show({
|
|
108
|
+
message: `LaTeX render mode: ${renderModeTitle(mode)}`,
|
|
109
|
+
variant: "success",
|
|
110
|
+
})
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
],
|
|
114
|
+
}))
|
|
36
115
|
|
|
37
116
|
return context.markdown.registerCodeBlockRenderer(LANGUAGE, (token, render) => {
|
|
38
117
|
try {
|
|
39
|
-
|
|
118
|
+
const renderable = new GraphicalLatexRenderable(context.renderer, {
|
|
40
119
|
content: normalizeLatexSource(token.text),
|
|
41
120
|
displayMode: true,
|
|
42
121
|
fallback: "source",
|
|
43
122
|
foregroundColor: color,
|
|
44
123
|
graphicsForegroundColor: color,
|
|
45
|
-
graphicsMode:
|
|
124
|
+
graphicsMode: effectiveMode(),
|
|
46
125
|
fontSize,
|
|
47
126
|
pixelRatio,
|
|
48
127
|
})
|
|
128
|
+
return renderable
|
|
49
129
|
} catch {
|
|
50
130
|
return render.defaultRender()
|
|
51
131
|
}
|