new_pull
This commit is contained in:
@@ -2,6 +2,15 @@
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [0.82.1] - 2026-07-25
|
||||
|
||||
## [0.82.0] - 2026-07-24
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed debug and crash logs to use the configured TUI log directory, including `PI_CODING_AGENT_DIR`, instead of always writing under `~/.pi/agent` ([#6958](https://github.com/earendil-works/pi/pull/6958) by [@davidbrai](https://github.com/davidbrai)).
|
||||
- Fixed narrow terminals crashing when the editor's bottom scroll indicator exceeded the terminal width ([#7015](https://github.com/earendil-works/pi/pull/7015) by [@christianklotz](https://github.com/christianklotz)).
|
||||
|
||||
## [0.81.1] - 2026-07-21
|
||||
|
||||
## [0.81.0] - 2026-07-21
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@earendil-works/pi-tui",
|
||||
"version": "0.81.1",
|
||||
"version": "0.82.1",
|
||||
"description": "Terminal User Interface library with differential rendering for efficient text-based applications",
|
||||
"type": "module",
|
||||
"main": "dist/index.js",
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
getGraphemeSegmenter,
|
||||
getWordSegmenter,
|
||||
isWhitespaceChar,
|
||||
truncateToWidth,
|
||||
sliceByColumn,
|
||||
visibleWidth,
|
||||
} from "../utils.ts";
|
||||
import { findWordBackward, findWordForward } from "../word-navigation.ts";
|
||||
@@ -256,6 +256,17 @@ function buildDebouncePattern(triggerCharacters: string[]): RegExp {
|
||||
return new RegExp(`(?:^|[ \\t])(?:@(?:"[^"]*|[^\\s]*)|[${escapedWithoutAt.join("")}][^\\s]*)$`);
|
||||
}
|
||||
|
||||
function createScrollBorder(direction: "↑" | "↓", hiddenLineCount: number, width: number): string {
|
||||
const availableWidth = Math.max(0, width);
|
||||
const indicator = `─── ${direction} ${hiddenLineCount} more `;
|
||||
const remaining = availableWidth - visibleWidth(indicator);
|
||||
if (remaining >= 0) return indicator + "─".repeat(remaining);
|
||||
|
||||
const ellipsis = "...".slice(0, availableWidth);
|
||||
const indicatorWidth = availableWidth - visibleWidth(ellipsis);
|
||||
return sliceByColumn(indicator, 0, indicatorWidth, true) + ellipsis;
|
||||
}
|
||||
|
||||
export class Editor implements Component, Focusable {
|
||||
private state: EditorState = {
|
||||
lines: [""],
|
||||
@@ -513,13 +524,8 @@ export class Editor implements Component, Focusable {
|
||||
|
||||
// Render top border (with scroll indicator if scrolled down)
|
||||
if (this.scrollOffset > 0) {
|
||||
const indicator = `─── ↑ ${this.scrollOffset} more `;
|
||||
const remaining = width - visibleWidth(indicator);
|
||||
if (remaining >= 0) {
|
||||
result.push(this.borderColor(indicator + "─".repeat(remaining)));
|
||||
} else {
|
||||
result.push(this.borderColor(truncateToWidth(indicator, width)));
|
||||
}
|
||||
const border = createScrollBorder("↑", this.scrollOffset, width);
|
||||
result.push(this.borderColor(border));
|
||||
} else {
|
||||
result.push(horizontal.repeat(width));
|
||||
}
|
||||
@@ -575,9 +581,8 @@ export class Editor implements Component, Focusable {
|
||||
// Render bottom border (with scroll indicator if more content below)
|
||||
const linesBelow = layoutLines.length - (this.scrollOffset + visibleLines.length);
|
||||
if (linesBelow > 0) {
|
||||
const indicator = `─── ↓ ${linesBelow} more `;
|
||||
const remaining = width - visibleWidth(indicator);
|
||||
result.push(this.borderColor(indicator + "─".repeat(Math.max(0, remaining))));
|
||||
const border = createScrollBorder("↓", linesBelow, width);
|
||||
result.push(this.borderColor(border));
|
||||
} else {
|
||||
result.push(horizontal.repeat(width));
|
||||
}
|
||||
|
||||
@@ -699,6 +699,31 @@ describe("Editor component", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("Scroll indicators", () => {
|
||||
it("keeps truncated scroll indicators within width and preserves their color (issue #6962)", () => {
|
||||
const width = 10;
|
||||
const borderColor = (text: string) => `\x1b[35m${text}\x1b[39m`;
|
||||
const editor = new Editor(createTestTUI(width), { ...defaultEditorTheme, borderColor });
|
||||
editor.setText(Array.from({ length: 20 }, (_, index) => `line ${index}`).join("\n"));
|
||||
|
||||
// Render once to initialize wrapping, then move the cursor so content remains above and below the viewport.
|
||||
editor.render(width);
|
||||
for (let index = 0; index < 10; index++) editor.handleInput("\x1b[A");
|
||||
|
||||
const lines = editor.render(width);
|
||||
const topBorder = lines[0]!;
|
||||
const bottomBorder = lines.at(-1)!;
|
||||
|
||||
assert.match(stripVTControlCharacters(topBorder), /^─── ↑/);
|
||||
assert.match(stripVTControlCharacters(bottomBorder), /^─── ↓/);
|
||||
assert.strictEqual(topBorder, borderColor(stripVTControlCharacters(topBorder)));
|
||||
assert.strictEqual(bottomBorder, borderColor(stripVTControlCharacters(bottomBorder)));
|
||||
for (const line of lines) {
|
||||
assert.strictEqual(visibleWidth(line), width, `line exceeds width ${width}: ${JSON.stringify(line)}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("Grapheme-aware text wrapping", () => {
|
||||
it("wraps lines correctly when text contains wide emojis", () => {
|
||||
const editor = new Editor(createTestTUI(), defaultEditorTheme);
|
||||
|
||||
Reference in New Issue
Block a user