Skip to content

Commit 94f4e62

Browse files
committed
chore(pi-tui): sync vendored pi-tui to upstream v1.0.1
1 parent af23bce commit 94f4e62

45 files changed

Lines changed: 2942 additions & 448 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@pymodel/pythinker-code": patch
3+
---
4+
5+
Update the terminal UI library to upstream pi v1.0.1, which keeps WezTerm images while scrolling, converts non-PNG images for Kitty, and fixes ANSI color order when a styled line is cut.

‎packages/pi-tui/README.md‎

Lines changed: 98 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,91 @@ tui.requestRender(); // Request a re-render
6868
tui.onDebug = () => console.log("Debug triggered");
6969
```
7070

71+
### Colors and terminal styles
72+
73+
Colors are values that can be converted or mixed before terminal rendering:
74+
75+
```typescript
76+
import {
77+
colorToRgb,
78+
foregroundAnsi,
79+
getTerminalColorMode,
80+
mixColors,
81+
parseColor,
82+
rgbColor,
83+
styleText,
84+
} from "@earendil-works/pi-tui";
85+
86+
const accent = parseColor("oklch(70% 0.12 220)");
87+
const background = parseColor("#20242a");
88+
const foreground = mixColors(accent, background, 0.2);
89+
90+
const text = styleText(
91+
"Ready",
92+
{ fg: foreground, bg: background, bold: true },
93+
getTerminalColorMode(),
94+
);
95+
```
96+
97+
`Color` is an indexed ANSI color, an sRGB color, or an OKLCH color. Every color converts to sRGB, so color math such as `mixColors()` always works. Indices 0-15 follow the user's terminal palette, so their sRGB values are approximations. `styleText()` converts colors to truecolor or 256-color output based on the requested terminal mode.
98+
99+
`parseColor()` also accepts OKHSL, as in `okhsl(250 60% 55%)`; `okhslColor()` builds it in code and `colorToOkhsl()` reads any color's OKHSL channels. OKHSL saturation is relative to the most the sRGB gamut allows at the hue and lightness, so every value is in gamut and equal saturation looks equally colorful across hues. OKHSL colors are converted to sRGB when created.
100+
101+
Conversions are not cached. OKLCH colors, especially ones outside the sRGB gamut, are more expensive to convert than sRGB or indexed colors. For colors used on every render, convert once and reuse the result:
102+
103+
```typescript
104+
const { r, g, b } = colorToRgb(mixColors(accent, background, 0.2));
105+
const foreground = rgbColor(r, g, b); // cheap to render repeatedly
106+
const foregroundCode = foregroundAnsi(foreground, getTerminalColorMode());
107+
```
108+
109+
### Alternate-screen viewport layouts
110+
111+
`TuiAltScreen` can render an explicit terminal-height layout. `VStack` and `HStack` allocate constrained regions, while `ScrollView` owns scrolling for one region. These semantics are intentionally unavailable on `TuiMainScreen`, where the terminal owns scrollback.
112+
113+
```typescript
114+
import {
115+
Container,
116+
isViewportTUI,
117+
ScrollView,
118+
Text,
119+
VStack,
120+
} from "@earendil-works/pi-tui";
121+
122+
const transcript = new Container();
123+
transcript.addChild(new Text("History"));
124+
125+
const editorAndFooter = new VStack([
126+
editor,
127+
new Text("status"),
128+
]);
129+
130+
if (isViewportTUI(tui)) {
131+
tui.setLayoutRoot(new VStack([
132+
{
133+
component: new ScrollView(transcript, {
134+
follow: "end",
135+
primary: true,
136+
overscroll: "chain",
137+
}),
138+
basis: 0,
139+
grow: 1,
140+
minSize: 1,
141+
},
142+
{
143+
component: editorAndFooter,
144+
basis: "auto",
145+
shrink: 1,
146+
minSize: 1,
147+
},
148+
]));
149+
}
150+
```
151+
152+
Stack entries support `basis`, `grow`, `shrink`, `minSize`, `maxSize`, and responsive `visible` callbacks. Mouse-wheel input targets the scroll view under the pointer and unused delta chains to outer scroll views by default. The primary scroll view receives the alternate-screen keyboard navigation actions and wheel input over non-scrollable regions. It can also jump between OSC 133 semantic prompt markers, matching common terminal prompt-navigation shortcuts. Press `Ctrl+Shift+F` to open or close its bordered search panel. The panel shows the configured previous/next shortcuts and provides clickable arrow controls; by default, `Enter`/`Ctrl+G` and `Shift+Enter`/`Ctrl+Shift+G` move between matches, and `Escape` also closes search. `TuiAltScreenOptions.searchMatchStyle` and `searchCurrentMatchStyle` customize match highlighting, while `searchNavigationButtonStyle` styles each arrow button and receives its hover state. `TuiAltScreenOptions.scrollToEndIndicator` renders a clickable label centered on the last row of a `follow: "end"` primary scroll view while it is scrolled away from the end; clicking it resumes end-following.
153+
154+
Layout geometry is rebuilt for each requested frame. Stateful components are retained, and their existing rendered-line caches remain effective. Calling `render(width)` directly on these layout components produces an unbounded document, which is also used when alt mode restores the main screen.
155+
71156
### Overlays
72157

73158
Overlays render components on top of existing content without replacing it. Useful for dialogs, menus, and modal UI.
@@ -154,15 +239,17 @@ All components implement:
154239
interface Component {
155240
render(width: number): string[];
156241
handleInput?(data: string): void;
157-
invalidate?(): void;
242+
handleMouse?(event: TuiMouseEvent): TuiMouseEventResult | undefined;
243+
invalidate(): void;
158244
}
159245
```
160246

161247
| Method | Description |
162248
|--------|-------------|
163249
| `render(width)` | Returns an array of strings, one per line. Each line **must not exceed `width`** or the TUI will error. Use `truncateToWidth()` or manual wrapping to ensure this. |
164250
| `handleInput?(data)` | Called when the component has focus and receives keyboard input. The `data` string contains raw terminal input (may include ANSI escape sequences). |
165-
| `invalidate?()` | Called to clear any cached render state. Components should re-render from scratch on the next `render()` call. |
251+
| `handleMouse?(event)` | Called by `TuiAltScreen` for normalized pointer input targeted at the component. |
252+
| `invalidate()` | Required. Clear any cached render state so the next `render()` starts from scratch. Components without cached render state can use an empty implementation. |
166253

167254
The TUI appends a full SGR reset and OSC 8 reset at the end of each rendered line. Styles do not carry across lines. If you emit multi-line text with styling, reapply styles per line or use `wrapTextWithAnsi()` so styles are preserved for each wrapped line.
168255

@@ -181,6 +268,8 @@ class MyInput implements Component, Focusable {
181268
// Emit marker right before the fake cursor
182269
return [`> ${beforeCursor}${marker}\x1b[7m${atCursor}\x1b[27m${afterCursor}`];
183270
}
271+
272+
invalidate(): void {}
184273
}
185274
```
186275

@@ -678,6 +767,8 @@ class MyInteractiveComponent implements Component {
678767
return truncateToWidth(prefix + item, width);
679768
});
680769
}
770+
771+
invalidate(): void {}
681772
}
682773
```
683774
@@ -709,6 +800,8 @@ class MyComponent implements Component {
709800
// Pad to exact width (optional, for backgrounds)
710801
return [line + " ".repeat(width - visible)];
711802
}
803+
804+
invalidate(): void {}
712805
}
713806
```
714807
@@ -766,7 +859,7 @@ See `test/chat-simple.ts` for a complete chat interface example with:
766859
767860
Run it:
768861
```bash
769-
npx tsx test/chat-simple.ts
862+
node test/chat-simple.ts
770863
```
771864
772865
## Development
@@ -779,13 +872,13 @@ npm install
779872
npm run check
780873

781874
# Run the demo
782-
npx tsx test/chat-simple.ts
875+
node test/chat-simple.ts
783876
```
784877
785878
### Debug logging
786879
787880
Set `PI_TUI_WRITE_LOG` to capture the raw ANSI stream written to stdout.
788881
789882
```bash
790-
PI_TUI_WRITE_LOG=/tmp/tui-ansi.log npx tsx test/chat-simple.ts
883+
PI_TUI_WRITE_LOG=/tmp/tui-ansi.log node test/chat-simple.ts
791884
```

‎packages/pi-tui/UPSTREAM.md‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ Update this section after every sync. Do not reuse the previous range.
88

99
- **Repo:** `https://github.com/earendil-works/pi.git`
1010
- **Subtree:** `packages/tui`
11-
- **Commit:** `53816d7dcc5ebe3a0eedec3cd07196c3a66d83fd` (2026-09-14; v0.85.1 plus upstream main through this commit)
11+
- **Commit:** `a7229ddc21810d6245105978033b7df645ecc2f7` (2026-10-03; upstream tag v1.0.1)
12+
- **Darwin prebuilds:** rebuilt locally from `native/darwin/src/darwin-platform.m` with `native/darwin/build.sh`, not copied from upstream. Exports and linked libraries match the upstream v1.0.1 prebuilds.
1213
- **This commit is an upstream marker.** It may not exist in this repo's object database.
1314

1415
## Reconstruct the fork

‎packages/pi-tui/native/clipboard.h‎

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,17 @@
77
#include <string.h>
88
#endif
99

10-
typedef enum { CLIPBOARD_TEXT, CLIPBOARD_IMAGE, CLIPBOARD_WRITE } clipboard_operation;
11-
typedef enum { CLIPBOARD_UNAVAILABLE, CLIPBOARD_EMPTY, CLIPBOARD_UTF8, CLIPBOARD_LATIN1, CLIPBOARD_UTF16, CLIPBOARD_BUFFER } clipboard_format;
10+
typedef enum { CLIPBOARD_TEXT, CLIPBOARD_IMAGE, CLIPBOARD_WRITE, CLIPBOARD_FILES } clipboard_operation;
11+
// CLIPBOARD_PATHS stores concatenated, NUL-terminated UTF-8 paths in clipboard_job.data.
12+
typedef enum {
13+
CLIPBOARD_UNAVAILABLE,
14+
CLIPBOARD_EMPTY,
15+
CLIPBOARD_UTF8,
16+
CLIPBOARD_LATIN1,
17+
CLIPBOARD_UTF16,
18+
CLIPBOARD_BUFFER,
19+
CLIPBOARD_PATHS
20+
} clipboard_format;
1221

1322
typedef struct {
1423
napi_async_work work;
@@ -63,6 +72,33 @@ static void PI_NAPI_CALL execute_clipboard_work(napi_env env, void* data) {
6372
clipboard_execute(data);
6473
}
6574

75+
static int create_clipboard_paths_value(
76+
napi_env env,
77+
const clipboard_job* job,
78+
napi_create_string_utf8_fn create_string,
79+
napi_value* result
80+
) {
81+
napi_create_array_fn create_array = (napi_create_array_fn)node_symbol("napi_create_array");
82+
napi_set_element_fn set_element = (napi_set_element_fn)node_symbol("napi_set_element");
83+
if (!create_array || !set_element || create_array(env, result) != 0) return 1;
84+
85+
const char* cursor = job->data;
86+
const char* end = cursor + job->length;
87+
for (uint32_t index = 0; cursor < end; index++) {
88+
const char* terminator = cursor;
89+
while (terminator < end && *terminator != '\0') terminator++;
90+
if (terminator == end) return 1;
91+
92+
napi_value item = 0;
93+
if (create_string(env, cursor, (size_t)(terminator - cursor), &item) != 0 ||
94+
set_element(env, *result, index, item) != 0) {
95+
return 1;
96+
}
97+
cursor = terminator + 1;
98+
}
99+
return 0;
100+
}
101+
66102
static void PI_NAPI_CALL complete_clipboard_work(napi_env env, int status, void* data) {
67103
clipboard_job* job = data;
68104
napi_create_string_utf8_fn create_string = (napi_create_string_utf8_fn)node_symbol("napi_create_string_utf8");
@@ -71,6 +107,8 @@ static void PI_NAPI_CALL complete_clipboard_work(napi_env env, int status, void*
71107
if (!job->error && job->operation != CLIPBOARD_WRITE) {
72108
if (job->format == CLIPBOARD_EMPTY) {
73109
result = null_value(env);
110+
} else if (job->format == CLIPBOARD_PATHS) {
111+
status = create_clipboard_paths_value(env, job, create_string, &result);
74112
} else if (job->format == CLIPBOARD_BUFFER) {
75113
napi_create_buffer_copy_fn create_buffer = (napi_create_buffer_copy_fn)node_symbol("napi_create_buffer_copy");
76114
status = create_buffer(env, job->length, job->data, 0, &result);

‎packages/pi-tui/native/darwin/README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Darwin native prebuilds
22

3-
Provides modifier-key state and asynchronous text/image clipboard access using AppKit.
3+
Provides modifier-key state and asynchronous text, image, and file-path clipboard access using AppKit.
44

55
## Building
66

Binary file not shown.
Binary file not shown.

‎packages/pi-tui/native/darwin/src/darwin-platform.m‎

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,40 @@ static void clipboard_execute(clipboard_job* job) {
6666
} else {
6767
clipboard_copy(job, text.UTF8String, [text lengthOfBytesUsingEncoding:NSUTF8StringEncoding], CLIPBOARD_UTF8);
6868
}
69+
} else if (job->operation == CLIPBOARD_FILES) {
70+
NSArray* urls = [pasteboard readObjectsForClasses:@[[NSURL class]]
71+
options:@{NSPasteboardURLReadingFileURLsOnlyKey: @YES}];
72+
size_t total = 0;
73+
for (NSURL* url in urls) {
74+
const char* path = url.fileSystemRepresentation;
75+
if (!path) continue;
76+
size_t length = strlen(path);
77+
if (length == SIZE_MAX || total > SIZE_MAX - length - 1) {
78+
job->error = "Clipboard file paths are too large";
79+
return;
80+
}
81+
total += length + 1;
82+
}
83+
if (total == 0) {
84+
job->format = CLIPBOARD_EMPTY;
85+
return;
86+
}
87+
char* paths = clipboard_alloc(total);
88+
if (!paths) {
89+
job->error = "Out of memory";
90+
return;
91+
}
92+
char* cursor = paths;
93+
for (NSURL* url in urls) {
94+
const char* path = url.fileSystemRepresentation;
95+
if (!path) continue;
96+
size_t length = strlen(path);
97+
memcpy(cursor, path, length + 1);
98+
cursor += length + 1;
99+
}
100+
job->data = paths;
101+
job->length = total;
102+
job->format = CLIPBOARD_PATHS;
69103
} else {
70104
if (![pasteboard availableTypeFromArray:@[ NSPasteboardTypePNG, NSPasteboardTypeTIFF ]]) {
71105
job->format = CLIPBOARD_EMPTY;
@@ -84,10 +118,15 @@ static void clipboard_execute(clipboard_job* job) {
84118
}
85119
}
86120

121+
static napi_value PI_NAPI_CALL get_clipboard_file_paths(napi_env env, napi_callback_info info) {
122+
return queue_clipboard(env, info, CLIPBOARD_FILES);
123+
}
124+
87125
PI_NAPI_EXPORT napi_value napi_register_module_v1(napi_env env, napi_value exports) {
88126
set_function_export(env, exports, "isModifierPressed", is_modifier_pressed);
89127
set_function_export(env, exports, "getText", get_clipboard_text);
90128
set_function_export(env, exports, "setText", set_clipboard_text);
91129
set_function_export(env, exports, "getImage", get_clipboard_image);
130+
set_function_export(env, exports, "getFilePaths", get_clipboard_file_paths);
92131
return exports;
93132
}

‎packages/pi-tui/native/napi.h‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ typedef void* napi_deferred;
2626
typedef void (PI_NAPI_CALL *napi_async_execute_callback)(napi_env, void*);
2727
typedef void (PI_NAPI_CALL *napi_async_complete_callback)(napi_env, int, void*);
2828
typedef int (PI_NAPI_CALL *napi_create_async_work_fn)(napi_env, napi_value, napi_value, napi_async_execute_callback, napi_async_complete_callback, void*, napi_async_work*);
29+
typedef int (PI_NAPI_CALL *napi_create_array_fn)(napi_env, napi_value*);
2930
typedef int (PI_NAPI_CALL *napi_async_work_fn)(napi_env, napi_async_work);
3031
typedef int (PI_NAPI_CALL *napi_create_promise_fn)(napi_env, napi_deferred*, napi_value*);
3132
typedef int (PI_NAPI_CALL *napi_settle_deferred_fn)(napi_env, napi_deferred, napi_value);
@@ -41,6 +42,7 @@ typedef int (PI_NAPI_CALL *napi_get_value_fn)(napi_env, napi_value*);
4142
typedef int (PI_NAPI_CALL *napi_get_value_string_utf8_fn)(napi_env, napi_value, char*, size_t, size_t*);
4243
typedef int (PI_NAPI_CALL *napi_get_value_string_utf16_fn)(napi_env, napi_value, uint16_t*, size_t, size_t*);
4344
typedef int (PI_NAPI_CALL *napi_set_named_property_fn)(napi_env, napi_value, const char*, napi_value);
45+
typedef int (PI_NAPI_CALL *napi_set_element_fn)(napi_env, napi_value, uint32_t, napi_value);
4446
typedef int (PI_NAPI_CALL *napi_throw_error_fn)(napi_env, const char*, const char*);
4547

4648
static void* node_symbol(const char* name) {

0 commit comments

Comments
 (0)