@narumitw/pi-jupyter 0.1.1 → 0.1.4
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 +6 -3
- package/extensions/index.ts +110 -14
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -11,6 +11,7 @@ The preview is a static text rendering of notebook cells and selected text outpu
|
|
|
11
11
|
- Watches the selected notebook on disk and refreshes after external saves.
|
|
12
12
|
- Displays markdown cells, code cells, execution counts, common text outputs/errors, and inline image outputs when the terminal supports images.
|
|
13
13
|
- Non-capturing by default, so you can keep typing in Pi while the panel stays visible.
|
|
14
|
+
- Mouse-resizable right-side panel: drag the preview's left border to change its width.
|
|
14
15
|
- Focus mode for scrolling the preview.
|
|
15
16
|
|
|
16
17
|
## Install
|
|
@@ -24,7 +25,7 @@ pi install npm:@narumitw/pi-jupyter
|
|
|
24
25
|
Or pin a version:
|
|
25
26
|
|
|
26
27
|
```bash
|
|
27
|
-
pi install npm:@narumitw/pi-jupyter@0.1.
|
|
28
|
+
pi install npm:@narumitw/pi-jupyter@0.1.3
|
|
28
29
|
```
|
|
29
30
|
|
|
30
31
|
Install for the current project only:
|
|
@@ -33,10 +34,10 @@ Install for the current project only:
|
|
|
33
34
|
pi install npm:@narumitw/pi-jupyter -l
|
|
34
35
|
```
|
|
35
36
|
|
|
36
|
-
|
|
37
|
+
Install from GitHub/tag instead of npm:
|
|
37
38
|
|
|
38
39
|
```bash
|
|
39
|
-
pi install git:github.com/narumiruna/pi-jupyter@v0.1.
|
|
40
|
+
pi install git:github.com/narumiruna/pi-jupyter@v0.1.3
|
|
40
41
|
```
|
|
41
42
|
|
|
42
43
|
If you previously installed the unscoped package, remove it before installing the scoped package:
|
|
@@ -123,6 +124,7 @@ just publish-dry-run
|
|
|
123
124
|
- `Shift+F8` — focus preview for scrolling.
|
|
124
125
|
- `Ctrl+Alt+J` / `Ctrl+Alt+K` — scroll preview down/up without focusing it.
|
|
125
126
|
- `Ctrl+Alt+D` / `Ctrl+Alt+U` — page down/up without focusing it.
|
|
127
|
+
- Drag the preview panel's left border with the mouse to resize it.
|
|
126
128
|
- In focused preview: `↑`, `↓`, `PgUp`, `PgDn`, `Home` or `j`, `k`, `u`, `d`, `g` scroll; `Esc` or `F8` returns focus to the editor.
|
|
127
129
|
|
|
128
130
|
## Notes
|
|
@@ -130,6 +132,7 @@ just publish-dry-run
|
|
|
130
132
|
PNG outputs are rendered as truecolor ANSI thumbnails, so matplotlib-style `image/png` output is visible in Ghostty even inside the right-side overlay. Other image formats use `@mariozechner/pi-tui` terminal image support when available, otherwise they fall back to an image placeholder.
|
|
131
133
|
|
|
132
134
|
The panel auto-hides on narrow terminals (`< 90` columns). Resize wider if it does not appear.
|
|
135
|
+
Mouse resizing uses standard terminal mouse reporting; if your terminal reserves mouse drag for selection, use Shift-drag (or your terminal's selection modifier) to select text instead.
|
|
133
136
|
|
|
134
137
|
If shortcuts conflict with Pi/editor keybindings, use the slash commands:
|
|
135
138
|
|
package/extensions/index.ts
CHANGED
|
@@ -3,11 +3,15 @@ import { readdir, readFile, stat } from "node:fs/promises";
|
|
|
3
3
|
import { basename, relative, resolve } from "node:path";
|
|
4
4
|
import { inflateSync } from "node:zlib";
|
|
5
5
|
import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
6
|
-
import type { Component, OverlayHandle, TUI } from "@mariozechner/pi-tui";
|
|
6
|
+
import type { Component, OverlayHandle, OverlayOptions, TUI } from "@mariozechner/pi-tui";
|
|
7
7
|
import { Image, Key, matchesKey, truncateToWidth, visibleWidth, wrapTextWithAnsi } from "@mariozechner/pi-tui";
|
|
8
8
|
|
|
9
9
|
const PANEL_ID = "jupyter-preview";
|
|
10
10
|
const NOTEBOOK_EXT = ".ipynb";
|
|
11
|
+
const DEFAULT_PANEL_WIDTH_PERCENT = 42;
|
|
12
|
+
const MIN_PANEL_WIDTH = 42;
|
|
13
|
+
const MIN_EDITOR_WIDTH = 24;
|
|
14
|
+
const RIGHT_MARGIN = 1;
|
|
11
15
|
|
|
12
16
|
type NotebookCell = {
|
|
13
17
|
cell_type?: string;
|
|
@@ -39,6 +43,8 @@ type PreviewState = {
|
|
|
39
43
|
lastMtime?: Date;
|
|
40
44
|
lastError?: string;
|
|
41
45
|
model?: Notebook;
|
|
46
|
+
panelWidth?: number;
|
|
47
|
+
resizing?: boolean;
|
|
42
48
|
};
|
|
43
49
|
|
|
44
50
|
export default function jupyterPreview(pi: ExtensionAPI) {
|
|
@@ -53,6 +59,7 @@ export default function jupyterPreview(pi: ExtensionAPI) {
|
|
|
53
59
|
let overlayHandle: OverlayHandle | undefined;
|
|
54
60
|
let closeOverlay: (() => void) | undefined;
|
|
55
61
|
let requestRender: (() => void) | undefined;
|
|
62
|
+
let removeMouseResize: (() => void) | undefined;
|
|
56
63
|
|
|
57
64
|
async function setNotebookPath(rawPath: string, ctx: ExtensionContext): Promise<void> {
|
|
58
65
|
const path = resolveNotebookPath(rawPath, ctx.cwd);
|
|
@@ -90,6 +97,16 @@ export default function jupyterPreview(pi: ExtensionAPI) {
|
|
|
90
97
|
return;
|
|
91
98
|
}
|
|
92
99
|
|
|
100
|
+
const overlayOptions: OverlayOptions = {
|
|
101
|
+
anchor: "right-center",
|
|
102
|
+
width: state.panelWidth ?? `${DEFAULT_PANEL_WIDTH_PERCENT}%`,
|
|
103
|
+
minWidth: MIN_PANEL_WIDTH,
|
|
104
|
+
maxHeight: "96%",
|
|
105
|
+
margin: { right: RIGHT_MARGIN },
|
|
106
|
+
nonCapturing: true,
|
|
107
|
+
visible: (termWidth) => termWidth >= 90,
|
|
108
|
+
};
|
|
109
|
+
|
|
93
110
|
void ctx.ui
|
|
94
111
|
.custom<void>(
|
|
95
112
|
(tui, theme, _keybindings, done) => {
|
|
@@ -99,36 +116,33 @@ export default function jupyterPreview(pi: ExtensionAPI) {
|
|
|
99
116
|
tui.requestRender();
|
|
100
117
|
});
|
|
101
118
|
requestRender = () => tui.requestRender();
|
|
119
|
+
removeMouseResize = installMouseResize(tui, state, overlayOptions, () => tui.requestRender());
|
|
102
120
|
closeOverlay = () => {
|
|
103
121
|
state.visible = false;
|
|
104
122
|
state.focused = false;
|
|
123
|
+
state.resizing = false;
|
|
105
124
|
done(undefined);
|
|
106
125
|
};
|
|
107
126
|
return panel;
|
|
108
127
|
},
|
|
109
128
|
{
|
|
110
129
|
overlay: true,
|
|
111
|
-
overlayOptions
|
|
112
|
-
anchor: "right-center",
|
|
113
|
-
width: "42%",
|
|
114
|
-
minWidth: 42,
|
|
115
|
-
maxHeight: "96%",
|
|
116
|
-
margin: { right: 1 },
|
|
117
|
-
nonCapturing: true,
|
|
118
|
-
visible: (termWidth) => termWidth >= 90,
|
|
119
|
-
},
|
|
130
|
+
overlayOptions,
|
|
120
131
|
onHandle: (handle) => {
|
|
121
132
|
overlayHandle = handle;
|
|
122
133
|
},
|
|
123
134
|
},
|
|
124
135
|
)
|
|
125
136
|
.finally(() => {
|
|
137
|
+
removeMouseResize?.();
|
|
138
|
+
removeMouseResize = undefined;
|
|
126
139
|
overlayHandle = undefined;
|
|
127
140
|
panel = undefined;
|
|
128
141
|
closeOverlay = undefined;
|
|
129
142
|
requestRender = undefined;
|
|
130
143
|
state.visible = false;
|
|
131
144
|
state.focused = false;
|
|
145
|
+
state.resizing = false;
|
|
132
146
|
ctx.ui.setStatus(PANEL_ID, undefined);
|
|
133
147
|
});
|
|
134
148
|
|
|
@@ -138,6 +152,7 @@ export default function jupyterPreview(pi: ExtensionAPI) {
|
|
|
138
152
|
function hidePanel(ctx?: ExtensionContext): void {
|
|
139
153
|
state.visible = false;
|
|
140
154
|
state.focused = false;
|
|
155
|
+
state.resizing = false;
|
|
141
156
|
ctx?.ui.setStatus(PANEL_ID, undefined);
|
|
142
157
|
closeOverlay?.();
|
|
143
158
|
overlayHandle?.hide();
|
|
@@ -310,6 +325,85 @@ export default function jupyterPreview(pi: ExtensionAPI) {
|
|
|
310
325
|
});
|
|
311
326
|
}
|
|
312
327
|
|
|
328
|
+
type MouseEvent = {
|
|
329
|
+
button: number;
|
|
330
|
+
x: number;
|
|
331
|
+
y: number;
|
|
332
|
+
released: boolean;
|
|
333
|
+
};
|
|
334
|
+
|
|
335
|
+
function installMouseResize(
|
|
336
|
+
tui: TUI,
|
|
337
|
+
state: PreviewState,
|
|
338
|
+
overlayOptions: OverlayOptions,
|
|
339
|
+
requestRender: () => void,
|
|
340
|
+
): () => void {
|
|
341
|
+
const terminal = tui.terminal;
|
|
342
|
+
const enableMouse = "\x1b[?1000h\x1b[?1002h\x1b[?1006h";
|
|
343
|
+
const disableMouse = "\x1b[?1006l\x1b[?1002l\x1b[?1000l";
|
|
344
|
+
terminal.write(enableMouse);
|
|
345
|
+
|
|
346
|
+
const removeListener = tui.addInputListener((data) => {
|
|
347
|
+
const event = parseSgrMouseEvent(data);
|
|
348
|
+
if (!event) return undefined;
|
|
349
|
+
|
|
350
|
+
const isPrimaryButton = (event.button & 3) === 0;
|
|
351
|
+
const isMotion = (event.button & 32) !== 0;
|
|
352
|
+
const handleX = getPanelLeftBorderX(terminal.columns, state);
|
|
353
|
+
|
|
354
|
+
if (event.released) {
|
|
355
|
+
state.resizing = false;
|
|
356
|
+
requestRender();
|
|
357
|
+
return { consume: true };
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
if (!state.resizing && isPrimaryButton && Math.abs(event.x - handleX) <= 1) {
|
|
361
|
+
state.resizing = true;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
if (state.resizing && isPrimaryButton && (isMotion || event.x !== handleX)) {
|
|
365
|
+
const nextWidth = clampPanelWidth(terminal.columns - RIGHT_MARGIN - (event.x - 1), terminal.columns);
|
|
366
|
+
state.panelWidth = nextWidth;
|
|
367
|
+
overlayOptions.width = nextWidth;
|
|
368
|
+
requestRender();
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
return { consume: true };
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
return () => {
|
|
375
|
+
state.resizing = false;
|
|
376
|
+
removeListener();
|
|
377
|
+
terminal.write(disableMouse);
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function parseSgrMouseEvent(data: string): MouseEvent | undefined {
|
|
382
|
+
const prefix = `${String.fromCharCode(27)}[<`;
|
|
383
|
+
if (!data.startsWith(prefix)) return undefined;
|
|
384
|
+
const suffix = data.at(-1);
|
|
385
|
+
if (suffix !== "M" && suffix !== "m") return undefined;
|
|
386
|
+
const parts = data.slice(prefix.length, -1).split(";");
|
|
387
|
+
if (parts.length !== 3) return undefined;
|
|
388
|
+
const [button, x, y] = parts.map((part) => Number.parseInt(part, 10));
|
|
389
|
+
if (![button, x, y].every(Number.isFinite)) return undefined;
|
|
390
|
+
return { button, x, y, released: suffix === "m" };
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
function getPanelLeftBorderX(termWidth: number, state: PreviewState): number {
|
|
394
|
+
const width = clampPanelWidth(
|
|
395
|
+
state.panelWidth ?? Math.floor((termWidth * DEFAULT_PANEL_WIDTH_PERCENT) / 100),
|
|
396
|
+
termWidth,
|
|
397
|
+
);
|
|
398
|
+
const zeroBasedCol = termWidth - RIGHT_MARGIN - width;
|
|
399
|
+
return zeroBasedCol + 1;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function clampPanelWidth(width: number, termWidth: number): number {
|
|
403
|
+
const maxWidth = Math.max(MIN_PANEL_WIDTH, termWidth - RIGHT_MARGIN - MIN_EDITOR_WIDTH);
|
|
404
|
+
return Math.max(MIN_PANEL_WIDTH, Math.min(maxWidth, Math.round(width)));
|
|
405
|
+
}
|
|
406
|
+
|
|
313
407
|
class NotebookPreviewPanel implements Component {
|
|
314
408
|
constructor(
|
|
315
409
|
private tui: TUI,
|
|
@@ -350,7 +444,7 @@ class NotebookPreviewPanel implements Component {
|
|
|
350
444
|
const pathLabel = this.state.path
|
|
351
445
|
? relative(this.state.cwd, this.state.path) || basename(this.state.path)
|
|
352
446
|
: "no notebook";
|
|
353
|
-
const title = `${this.state.focused ? "● " : ""}Jupyter Preview`;
|
|
447
|
+
const title = `${this.state.resizing ? "↔ " : this.state.focused ? "● " : ""}Jupyter Preview`;
|
|
354
448
|
const lines: string[] = [border(`╭${"─".repeat(inner)}╮`), pad(` ${accent(title)} ${dim(pathLabel)}`)];
|
|
355
449
|
lines.push(border("├") + border("─".repeat(inner)) + border("┤"));
|
|
356
450
|
|
|
@@ -368,9 +462,11 @@ class NotebookPreviewPanel implements Component {
|
|
|
368
462
|
}
|
|
369
463
|
|
|
370
464
|
lines.push(border("├") + border("─".repeat(inner)) + border("┤"));
|
|
371
|
-
const footer = this.state.
|
|
372
|
-
? "
|
|
373
|
-
:
|
|
465
|
+
const footer = this.state.resizing
|
|
466
|
+
? " Drag to resize width"
|
|
467
|
+
: this.state.focused
|
|
468
|
+
? " ↑↓ PgUp/PgDn or j/k/u/d scroll • Esc/F8 return"
|
|
469
|
+
: " Drag left border resize • Ctrl+Alt+j/k scroll • Shift+F8 focus";
|
|
374
470
|
lines.push(pad(dim(footer)));
|
|
375
471
|
lines.push(border(`╰${"─".repeat(inner)}╯`));
|
|
376
472
|
return lines;
|