@oh-my-pi/pi-tui 17.1.7 → 17.1.8

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 CHANGED
@@ -2,6 +2,13 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [17.1.8] - 2026-07-28
6
+
7
+ ### Fixed
8
+
9
+ - Fixed wrapped Markdown list continuations losing their hanging indentation in narrow terminal layouts.
10
+ - Fixed an issue where emergency exits from fullscreen overlays could leave the Kitty keyboard protocol active, corrupting Arrow Up input in the terminal after exiting.
11
+
5
12
  ## [17.1.7] - 2026-07-27
6
13
 
7
14
  ### Added
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@oh-my-pi/pi-tui",
4
- "version": "17.1.7",
4
+ "version": "17.1.8",
5
5
  "description": "Terminal User Interface library with differential rendering for efficient text-based applications",
6
6
  "homepage": "https://omp.sh",
7
7
  "author": "Can Boluk",
@@ -37,8 +37,8 @@
37
37
  "fmt": "biome format --write ."
38
38
  },
39
39
  "dependencies": {
40
- "@oh-my-pi/pi-natives": "17.1.7",
41
- "@oh-my-pi/pi-utils": "17.1.7",
40
+ "@oh-my-pi/pi-natives": "17.1.8",
41
+ "@oh-my-pi/pi-utils": "17.1.8",
42
42
  "lru-cache": "11.5.2",
43
43
  "marked": "^18.0.6"
44
44
  },
@@ -1874,9 +1874,10 @@ export class Markdown implements Component, NativeScrollbackCommittedRows, Nativ
1874
1874
  );
1875
1875
  const tokenLineOffsets = [0];
1876
1876
  for (const line of renderedTokenLines) {
1877
- // Skip wrapping for image protocol lines and OSC 66 sized headings
1878
- // (would corrupt escape sequences / split the indivisible sized span).
1879
- if (TERMINAL.isImageLine(line) || isOsc66Line(line)) {
1877
+ // Lists wrap while their structural prefixes are still available, so
1878
+ // continuation rows retain the correct hanging indent. Re-wrapping the
1879
+ // flattened rows here would discard that structure.
1880
+ if (token.type === "list" || TERMINAL.isImageLine(line) || isOsc66Line(line)) {
1880
1881
  wrappedLines.push(line);
1881
1882
  } else {
1882
1883
  wrappedLines.push(...wrapTextWithAnsi(line, contentWidth));
@@ -2273,7 +2274,7 @@ export class Markdown implements Component, NativeScrollbackCommittedRows, Nativ
2273
2274
  }
2274
2275
 
2275
2276
  case "list": {
2276
- const listLines = this.#renderList(token as ListToken, 0, styleContext);
2277
+ const listLines = this.#renderList(token as ListToken, 0, width, styleContext);
2277
2278
  lines.push(...listLines);
2278
2279
  // Don't add spacing after lists if a space token follows
2279
2280
  // (the space token will handle it)
@@ -2589,46 +2590,60 @@ export class Markdown implements Component, NativeScrollbackCommittedRows, Nativ
2589
2590
  /**
2590
2591
  * Render a list with proper nesting support
2591
2592
  */
2592
- #renderList(token: ListToken, depth: number, styleContext?: InlineStyleContext): string[] {
2593
+ #renderList(token: ListToken, depth: number, width: number, styleContext?: InlineStyleContext): string[] {
2593
2594
  const lines: string[] = [];
2594
2595
  const indent = " ".repeat(depth);
2595
2596
  // Use the list's start property (defaults to 1 for ordered lists)
2596
2597
  const startNumber = token.start ?? 1;
2598
+ const pushWrapped = (text: string, firstPrefix: string, continuationPrefix: string): void => {
2599
+ const prefixWidth = visibleWidth(firstPrefix);
2600
+ if (prefixWidth >= width) {
2601
+ lines.push(truncateToWidth(firstPrefix, width, Ellipsis.Omit));
2602
+ lines.push(...wrapTextWithAnsi(text, Math.max(1, width)));
2603
+ return;
2604
+ }
2605
+ const bodyWidth = width - prefixWidth;
2606
+ const wrapped = wrapTextWithAnsi(text, bodyWidth);
2607
+ if (wrapped.length === 0) {
2608
+ lines.push(firstPrefix);
2609
+ return;
2610
+ }
2611
+ lines.push(firstPrefix + wrapped[0]);
2612
+ for (let lineIndex = 1; lineIndex < wrapped.length; lineIndex++) {
2613
+ lines.push(continuationPrefix + wrapped[lineIndex]);
2614
+ }
2615
+ };
2597
2616
 
2598
2617
  for (let i = 0; i < token.items.length; i++) {
2599
2618
  const item = token.items[i];
2600
2619
  const bullet = token.ordered ? `${startNumber + i}. ` : "- ";
2620
+ const firstPrefix = indent + this.#theme.listBullet(bullet);
2601
2621
  // Continuation rows align under the item text, so the hang matches the
2602
2622
  // actual bullet width (`10. ` is 4 cells, not 2).
2603
- const continuationIndent = indent + padding(bullet.length);
2623
+ const continuationIndent = indent + padding(visibleWidth(bullet));
2604
2624
 
2605
2625
  // Process item tokens; nested-list lines arrive structurally tagged and
2606
2626
  // already carry their own full indent.
2607
- const itemLines = this.#renderListItem(item.tokens || [], depth, styleContext);
2627
+ const itemLines = this.#renderListItem(item.tokens || [], depth, width, styleContext);
2608
2628
 
2609
2629
  if (itemLines.length > 0) {
2610
2630
  const firstLine = itemLines[0]!;
2611
2631
  if (firstLine.nested) {
2612
- // Nested list first - keep as-is (already has full indent)
2613
2632
  lines.push(firstLine.text);
2614
2633
  } else {
2615
- // Regular text content - add indent and bullet
2616
- lines.push(indent + this.#theme.listBullet(bullet) + firstLine.text);
2634
+ pushWrapped(firstLine.text, firstPrefix, continuationIndent);
2617
2635
  }
2618
2636
 
2619
- // Rest of the lines
2620
2637
  for (let j = 1; j < itemLines.length; j++) {
2621
2638
  const line = itemLines[j]!;
2622
2639
  if (line.nested) {
2623
- // Nested list line - already has full indent
2624
2640
  lines.push(line.text);
2625
2641
  } else {
2626
- // Regular content - hang under the item text
2627
- lines.push(continuationIndent + line.text);
2642
+ pushWrapped(line.text, continuationIndent, continuationIndent);
2628
2643
  }
2629
2644
  }
2630
2645
  } else {
2631
- lines.push(indent + this.#theme.listBullet(bullet));
2646
+ lines.push(firstPrefix);
2632
2647
  }
2633
2648
  }
2634
2649
 
@@ -2644,6 +2659,7 @@ export class Markdown implements Component, NativeScrollbackCommittedRows, Nativ
2644
2659
  #renderListItem(
2645
2660
  tokens: Token[],
2646
2661
  parentDepth: number,
2662
+ width: number,
2647
2663
  styleContext?: InlineStyleContext,
2648
2664
  ): Array<{ text: string; nested: boolean }> {
2649
2665
  const lines: Array<{ text: string; nested: boolean }> = [];
@@ -2652,7 +2668,7 @@ export class Markdown implements Component, NativeScrollbackCommittedRows, Nativ
2652
2668
  if (token.type === "list") {
2653
2669
  // Nested list - render with one additional indent level
2654
2670
  // These lines carry their own indent, so tag them for pass-through
2655
- const nestedLines = this.#renderList(token as ListToken, parentDepth + 1, styleContext);
2671
+ const nestedLines = this.#renderList(token as ListToken, parentDepth + 1, width, styleContext);
2656
2672
  for (const nestedLine of nestedLines) {
2657
2673
  lines.push({ text: nestedLine, nested: true });
2658
2674
  }
package/src/terminal.ts CHANGED
@@ -275,17 +275,15 @@ export function emergencyTerminalRestore(): void {
275
275
  restoreTerminalStderr();
276
276
  const terminal = activeTerminal;
277
277
  if (terminal) {
278
- terminal.stop();
279
- // stop() never touches the alternate screen the TUI owns that
280
- // state and exits it on the normal shutdown path. Only crash paths
281
- // with a fullscreen overlay still hold the alt buffer here. The
282
- // leave sequence is gated on the tracked state because it is NOT a
283
- // universally safe no-op: Windows' VT dispatcher homes the cursor
284
- // on DECRST 1049 even when the alt buffer is inactive.
278
+ // Keyboard enhancement state is screen-local: pop the alt-screen
279
+ // frame before leaving it, then let stop() pop omp's main-screen frame.
285
280
  if (altScreenActive) {
286
- terminal.write("\x1b[?1049l");
281
+ const keyboardExit =
282
+ terminal.keyboardEnhancementExitSequence ?? (terminal.kittyEnableSequence ? "\x1b[<u" : "");
283
+ terminal.write(`${keyboardExit}\x1b[?1049l`);
287
284
  altScreenActive = false;
288
285
  }
286
+ terminal.stop();
289
287
  terminal.showCursor(true);
290
288
  } else if (terminalEverStarted && !isTerminalHeadless()) {
291
289
  // Blind restore only if we know a terminal was started but lost track of it
@@ -305,7 +303,7 @@ export function emergencyTerminalRestore(): void {
305
303
  // actually holds it — on Windows, DECRST 1049 on the main
306
304
  // buffer homes the cursor (unconditional CursorRestoreState
307
305
  // with no prior save), corrupting the shell handoff on exit.
308
- (altScreenActive ? "\x1b[?1049l" : "") +
306
+ (altScreenActive ? "\x1b[?1049l\x1b[?1l\x1b>\x1b[<u" : "") + // Leave alt; reset main keyboard
309
307
  "\x1b[?25h", // Show cursor
310
308
  );
311
309
  altScreenActive = false;