@oh-my-pi/pi-tui 17.3.8 → 17.4.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 +7 -0
- package/dist/types/components/composer/borderless.d.ts +8 -0
- package/dist/types/components/composer/box.d.ts +2 -0
- package/dist/types/components/composer/claude.d.ts +8 -0
- package/dist/types/components/composer/field.d.ts +3 -0
- package/dist/types/components/composer/index.d.ts +9 -0
- package/dist/types/components/composer/pi.d.ts +2 -0
- package/dist/types/components/composer/rail.d.ts +3 -0
- package/dist/types/components/composer/registry.d.ts +12 -0
- package/dist/types/components/composer/rule.d.ts +7 -0
- package/dist/types/components/composer/types.d.ts +90 -0
- package/dist/types/components/editor.d.ts +10 -9
- package/dist/types/components/settings-list.d.ts +7 -1
- package/dist/types/index.d.ts +1 -0
- package/dist/types/utils.d.ts +0 -1
- package/package.json +3 -3
- package/src/components/composer/borderless.ts +37 -0
- package/src/components/composer/box.ts +85 -0
- package/src/components/composer/claude.ts +39 -0
- package/src/components/composer/field.ts +47 -0
- package/src/components/composer/index.ts +9 -0
- package/src/components/composer/pi.ts +37 -0
- package/src/components/composer/rail.ts +48 -0
- package/src/components/composer/registry.ts +46 -0
- package/src/components/composer/rule.ts +55 -0
- package/src/components/composer/types.ts +98 -0
- package/src/components/editor.ts +103 -92
- package/src/components/settings-list.ts +43 -11
- package/src/index.ts +1 -0
- package/src/utils.ts +53 -58
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [17.4.0] - 2026-08-20
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Added composer border styles (`box`, `claude`, `pi`, `borderless`) via `ComposerStyle` objects and `getComposerStyle`, unifying chrome geometry and rendering across the editor and previews.
|
|
10
|
+
- Added support for warning risk notes and row markers in settings lists.
|
|
11
|
+
|
|
5
12
|
## [17.3.8] - 2026-08-19
|
|
6
13
|
|
|
7
14
|
### Fixed
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Chrome-free composer: a bare `❯ ` prompt with no rules or borders. Also the
|
|
3
|
+
* effective style whenever a host calls `setBorderVisible(false)` (hook
|
|
4
|
+
* editors, agents hub). The status bar renders as a plain standalone bottom
|
|
5
|
+
* bar with both segment groups.
|
|
6
|
+
*/
|
|
7
|
+
import type { ComposerStyle } from "./types.js";
|
|
8
|
+
export declare const borderlessComposerStyle: ComposerStyle;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claude Code-like composer: full-width horizontal rules above and below a
|
|
3
|
+
* borderless `❯ ` prompt. The right status group rides the top rule as a
|
|
4
|
+
* bg chip near the right edge (`─────── hi ─`); the left group renders as a
|
|
5
|
+
* plain standalone bottom bar that yields its row to the autocomplete menu.
|
|
6
|
+
*/
|
|
7
|
+
import type { ComposerStyle } from "./types.js";
|
|
8
|
+
export declare const claudeComposerStyle: ComposerStyle;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export * from "./borderless.js";
|
|
2
|
+
export * from "./box.js";
|
|
3
|
+
export * from "./claude.js";
|
|
4
|
+
export * from "./field.js";
|
|
5
|
+
export * from "./pi.js";
|
|
6
|
+
export * from "./rail.js";
|
|
7
|
+
export * from "./registry.js";
|
|
8
|
+
export * from "./rule.js";
|
|
9
|
+
export * from "./types.js";
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { ComposerStyle, EditorBorderStyle } from "./types.js";
|
|
2
|
+
/** Whether an id names a composer style shipped by pi-tui. */
|
|
3
|
+
export declare function isBuiltinComposerStyle(id: string): boolean;
|
|
4
|
+
/**
|
|
5
|
+
* Register one extension-owned composer style for this process.
|
|
6
|
+
*
|
|
7
|
+
* Built-in ids and duplicate extension ids are rejected. The returned disposer
|
|
8
|
+
* removes only this registration.
|
|
9
|
+
*/
|
|
10
|
+
export declare function registerComposerStyle(style: ComposerStyle): () => void;
|
|
11
|
+
/** Style object for a composer shape; unknown ids fall back to `box`. */
|
|
12
|
+
export declare function getComposerStyle(id: EditorBorderStyle): ComposerStyle;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { ComposerChromeContext, ComposerStyle } from "./types.js";
|
|
2
|
+
/** Draw a full-width rule with status content docked at its right edge.
|
|
3
|
+
* Over-wide content is truncated (keeping one rule cell on each side) rather
|
|
4
|
+
* than dropped, so the chip survives narrow terminals and previews. */
|
|
5
|
+
export declare function renderTopRule(ctx: ComposerChromeContext): string;
|
|
6
|
+
/** Composer style with one status-bearing top rule and no bottom chrome. */
|
|
7
|
+
export declare const ruleComposerStyle: ComposerStyle;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Composer chrome contract. A {@link ComposerStyle} owns everything about how
|
|
3
|
+
* the input editor's frame looks — top/bottom chrome, per-row side chrome,
|
|
4
|
+
* default padding and prompt gutter — plus metadata telling the host where the
|
|
5
|
+
* status bar attaches. The editor, the /settings preview, and the setup-wizard
|
|
6
|
+
* preview all render through the same style object, so the three surfaces can
|
|
7
|
+
* never drift apart.
|
|
8
|
+
*/
|
|
9
|
+
import type { SymbolTheme } from "../../symbols.js";
|
|
10
|
+
/** Box-drawing glyph set used for composer chrome (the theme's `boxRound`). */
|
|
11
|
+
export type ComposerBox = SymbolTheme["boxRound"];
|
|
12
|
+
/** Built-in composer shape identifiers shipped by pi-tui. */
|
|
13
|
+
export declare const BUILTIN_EDITOR_BORDER_STYLES: readonly ["box", "claude", "pi", "borderless", "rule", "field", "rail"];
|
|
14
|
+
/** Identifier for a built-in composer shape. */
|
|
15
|
+
export type BuiltinEditorBorderStyle = (typeof BUILTIN_EDITOR_BORDER_STYLES)[number];
|
|
16
|
+
/** Composer shape identifier; extensions may register additional strings. */
|
|
17
|
+
export type EditorBorderStyle = string;
|
|
18
|
+
/** Pre-rendered status content injected into the top chrome. */
|
|
19
|
+
export interface EditorTopBorder {
|
|
20
|
+
/** The status content (already styled) */
|
|
21
|
+
content: string;
|
|
22
|
+
/** Visible width of the content */
|
|
23
|
+
width: number;
|
|
24
|
+
/** Optional logical revision that changes independently of available width. */
|
|
25
|
+
revision?: number;
|
|
26
|
+
}
|
|
27
|
+
/** Inputs shared by every chrome row. */
|
|
28
|
+
export interface ComposerChromeContext {
|
|
29
|
+
/** Full terminal width available to the composer. */
|
|
30
|
+
width: number;
|
|
31
|
+
/** Horizontal padding inside the side chrome. */
|
|
32
|
+
paddingX: number;
|
|
33
|
+
borderColor: (str: string) => string;
|
|
34
|
+
/** Stable accent used by shape-defining chrome such as field caps and rails. */
|
|
35
|
+
accentColor: (str: string) => string;
|
|
36
|
+
/** Background fill for composer surfaces; preserves the fill across nested SGR resets. */
|
|
37
|
+
surfaceColor: (str: string) => string;
|
|
38
|
+
/** Box-drawing glyph set (theme's `boxRound`). */
|
|
39
|
+
box: ComposerBox;
|
|
40
|
+
/** Status content for the top chrome; box embeds it after the corner while
|
|
41
|
+
* rule-based styles dock it against the right edge. */
|
|
42
|
+
topBorder?: EditorTopBorder;
|
|
43
|
+
}
|
|
44
|
+
/** Inputs for one content row. */
|
|
45
|
+
export interface ComposerRowContext extends ComposerChromeContext {
|
|
46
|
+
/** Fully decorated row text (cursor glyph / IME marker included). */
|
|
47
|
+
text: string;
|
|
48
|
+
/** Spaces padding the text out to the content width. */
|
|
49
|
+
pad: string;
|
|
50
|
+
/** Prompt gutter cells for this row ("" when the style has none). */
|
|
51
|
+
gutter: string;
|
|
52
|
+
isLastRow: boolean;
|
|
53
|
+
/** Cells the end-of-line cursor overflowed into the right chrome (box). */
|
|
54
|
+
cursorOverflow: number;
|
|
55
|
+
/** Emit an empty right chrome after the cursor so terminal-local IME
|
|
56
|
+
* preedit cannot shift the frame (box last row). */
|
|
57
|
+
imeSafeCursorTail: boolean;
|
|
58
|
+
/** Row lies inside the right-border scrollbar thumb (box). */
|
|
59
|
+
scrollbarThumb: boolean;
|
|
60
|
+
}
|
|
61
|
+
export interface ComposerStyle {
|
|
62
|
+
readonly id: EditorBorderStyle;
|
|
63
|
+
/** Content rows carry left/right border glyphs; drives the cursor-reserve
|
|
64
|
+
* column, IME-safe layout, and the right-border scrollbar. */
|
|
65
|
+
readonly sideBorders: boolean;
|
|
66
|
+
/** Rows consumed by top+bottom chrome (drives maxHeight budgeting). */
|
|
67
|
+
readonly verticalChrome: 0 | 1 | 2;
|
|
68
|
+
/** Where the host should attach the status bar: embedded in the top border,
|
|
69
|
+
* docked onto a top rule, or detached into a standalone bottom bar. */
|
|
70
|
+
readonly statusAttachment: "top-border" | "top-rule-chip" | "none";
|
|
71
|
+
/** Which segment groups the standalone bottom status bar shows. */
|
|
72
|
+
readonly bottomBar: "none" | "left" | "full";
|
|
73
|
+
/** Insert a blank spacer row between the editor and the standalone bottom
|
|
74
|
+
* bar. Styles without bottom chrome need it so the bar doesn't sit flush
|
|
75
|
+
* against the last input row. */
|
|
76
|
+
readonly bottomBarGap: boolean;
|
|
77
|
+
/** Default prompt gutter when the host sets none. */
|
|
78
|
+
readonly defaultPromptGutter: string | undefined;
|
|
79
|
+
/** Default horizontal padding; `themePaddingX` is the theme's request. */
|
|
80
|
+
defaultPaddingX(themePaddingX: number | undefined): number;
|
|
81
|
+
/** Cells consumed per side on content rows (border glyph + padding). */
|
|
82
|
+
sideChromeWidth(paddingX: number): number;
|
|
83
|
+
/** Top chrome row; `undefined` renders none. */
|
|
84
|
+
renderTop(ctx: ComposerChromeContext): string | undefined;
|
|
85
|
+
/** Chrome-wrapped content row; box's IME-safe last row emits two rows. */
|
|
86
|
+
renderRow(ctx: ComposerRowContext): string[];
|
|
87
|
+
/** Bottom chrome row; `undefined` renders none (box merges the bottom
|
|
88
|
+
* border into its last content row). */
|
|
89
|
+
renderBottom(ctx: ComposerChromeContext): string | undefined;
|
|
90
|
+
}
|
|
@@ -1,23 +1,21 @@
|
|
|
1
1
|
import { type AutocompleteProvider } from "../autocomplete.js";
|
|
2
2
|
import type { SymbolTheme } from "../symbols.js";
|
|
3
3
|
import { type Component, type Focusable } from "../tui.js";
|
|
4
|
+
import { type EditorBorderStyle, type EditorTopBorder } from "./composer/index.js";
|
|
5
|
+
export type { EditorBorderStyle, EditorTopBorder };
|
|
4
6
|
import { type SelectListTheme } from "./select-list.js";
|
|
5
7
|
export interface EditorTheme {
|
|
6
8
|
borderColor: (str: string) => string;
|
|
9
|
+
/** Stable accent for composer chrome that should not follow the mutable border state. */
|
|
10
|
+
accentColor?: (str: string) => string;
|
|
11
|
+
/** Background fill used by filled composer styles. */
|
|
12
|
+
surfaceColor?: (str: string) => string;
|
|
7
13
|
selectList: SelectListTheme;
|
|
8
14
|
symbols: SymbolTheme;
|
|
9
15
|
editorPaddingX?: number;
|
|
10
16
|
/** Style function for inline hint/ghost text (dim text after cursor) */
|
|
11
17
|
hintStyle?: (text: string) => string;
|
|
12
18
|
}
|
|
13
|
-
export interface EditorTopBorder {
|
|
14
|
-
/** The status content (already styled) */
|
|
15
|
-
content: string;
|
|
16
|
-
/** Visible width of the content */
|
|
17
|
-
width: number;
|
|
18
|
-
/** Optional logical revision that changes independently of available width. */
|
|
19
|
-
revision?: number;
|
|
20
|
-
}
|
|
21
19
|
interface HistoryEntry {
|
|
22
20
|
prompt: string;
|
|
23
21
|
}
|
|
@@ -86,6 +84,10 @@ export declare class Editor implements Component, Focusable {
|
|
|
86
84
|
*/
|
|
87
85
|
setBorderVisible(borderVisible: boolean): void;
|
|
88
86
|
setPromptGutter(promptGutter: string | undefined): void;
|
|
87
|
+
getBorderStyle(): EditorBorderStyle;
|
|
88
|
+
setBorderStyle(style: EditorBorderStyle): void;
|
|
89
|
+
/** True while the autocomplete/slash-command menu is open below the editor. */
|
|
90
|
+
isAutocompleteActive(): boolean;
|
|
89
91
|
/**
|
|
90
92
|
* Get the available width for top border content given a total terminal width.
|
|
91
93
|
* Accounts for the border characters and horizontal padding when visible.
|
|
@@ -163,4 +165,3 @@ export declare class Editor implements Component, Focusable {
|
|
|
163
165
|
insertPaste(content: string): void;
|
|
164
166
|
isShowingAutocomplete(): boolean;
|
|
165
167
|
}
|
|
166
|
-
export {};
|
|
@@ -7,6 +7,8 @@ export interface SettingItem {
|
|
|
7
7
|
label: string;
|
|
8
8
|
/** Optional description shown when selected */
|
|
9
9
|
description?: string;
|
|
10
|
+
/** Optional risk note shown in warning styling above the description, with a glyph on the row. */
|
|
11
|
+
warning?: string;
|
|
10
12
|
/** Current value to display (right side) */
|
|
11
13
|
currentValue: string;
|
|
12
14
|
/** If provided, Enter/Space cycles through these values */
|
|
@@ -22,6 +24,10 @@ export interface SettingsListTheme {
|
|
|
22
24
|
label: (text: string, selected: boolean, changed: boolean) => string;
|
|
23
25
|
value: (text: string, selected: boolean, changed: boolean) => string;
|
|
24
26
|
description: (text: string) => string;
|
|
27
|
+
/** Style for risk notes and the row warning glyph. Falls back to `description` when omitted. */
|
|
28
|
+
warning?: (text: string) => string;
|
|
29
|
+
/** Glyph marking rows that carry a `warning`. Omitted hides the row marker. */
|
|
30
|
+
warningMark?: string;
|
|
25
31
|
cursor: string;
|
|
26
32
|
hint: (text: string) => string;
|
|
27
33
|
/** Style for section heading rows (dimmed when outside the active section). Falls back to `hint` when omitted. */
|
|
@@ -55,7 +61,7 @@ export interface SettingsListOptions {
|
|
|
55
61
|
/** Fixed split-sidebar width (columns incl. indent+gap); default derives from section names. */
|
|
56
62
|
sidebarWidth?: number;
|
|
57
63
|
}
|
|
58
|
-
/** Searchable text for a setting item: label, id, value, description, and cycle values. */
|
|
64
|
+
/** Searchable text for a setting item: label, id, value, description, warning, and cycle values. */
|
|
59
65
|
export declare function getSettingItemFilterText(item: SettingItem): string;
|
|
60
66
|
export declare class SettingsList implements Component {
|
|
61
67
|
#private;
|
package/dist/types/index.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export * from "./autocomplete.js";
|
|
2
2
|
export * from "./components/box.js";
|
|
3
3
|
export * from "./components/cancellable-loader.js";
|
|
4
|
+
export * from "./components/composer/index.js";
|
|
4
5
|
export * from "./components/editor.js";
|
|
5
6
|
export * from "./components/image.js";
|
|
6
7
|
export * from "./components/input.js";
|
package/dist/types/utils.d.ts
CHANGED
|
@@ -47,7 +47,6 @@ export declare function getSegmenter(): Intl.Segmenter;
|
|
|
47
47
|
*/
|
|
48
48
|
export declare function visibleWidth(str: string): number;
|
|
49
49
|
/**
|
|
50
|
-
* True when a row carries a Kitty OSC 66 text-sizing span (`\x1b]66;…`).
|
|
51
50
|
* Scaled spans must bypass wrapping/padding and, when scaled up, reserve the
|
|
52
51
|
* terminal rows their multicell glyphs flow into.
|
|
53
52
|
*/
|
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.
|
|
4
|
+
"version": "17.4.0",
|
|
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.
|
|
41
|
-
"@oh-my-pi/pi-utils": "17.
|
|
40
|
+
"@oh-my-pi/pi-natives": "17.4.0",
|
|
41
|
+
"@oh-my-pi/pi-utils": "17.4.0"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
44
|
"ghostty-web": "^0.4.0"
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Chrome-free composer: a bare `❯ ` prompt with no rules or borders. Also the
|
|
3
|
+
* effective style whenever a host calls `setBorderVisible(false)` (hook
|
|
4
|
+
* editors, agents hub). The status bar renders as a plain standalone bottom
|
|
5
|
+
* bar with both segment groups.
|
|
6
|
+
*/
|
|
7
|
+
import type { ComposerRowContext, ComposerStyle } from "./types";
|
|
8
|
+
|
|
9
|
+
export const borderlessComposerStyle: ComposerStyle = {
|
|
10
|
+
id: "borderless",
|
|
11
|
+
sideBorders: false,
|
|
12
|
+
verticalChrome: 0,
|
|
13
|
+
statusAttachment: "none",
|
|
14
|
+
bottomBar: "full",
|
|
15
|
+
bottomBarGap: false,
|
|
16
|
+
defaultPromptGutter: "❯ ",
|
|
17
|
+
|
|
18
|
+
defaultPaddingX(): number {
|
|
19
|
+
return 0;
|
|
20
|
+
},
|
|
21
|
+
|
|
22
|
+
sideChromeWidth(): number {
|
|
23
|
+
return 0;
|
|
24
|
+
},
|
|
25
|
+
|
|
26
|
+
renderTop(): undefined {
|
|
27
|
+
return undefined;
|
|
28
|
+
},
|
|
29
|
+
|
|
30
|
+
renderRow(ctx: ComposerRowContext): string[] {
|
|
31
|
+
return [ctx.gutter + ctx.text + ctx.pad];
|
|
32
|
+
},
|
|
33
|
+
|
|
34
|
+
renderBottom(): undefined {
|
|
35
|
+
return undefined;
|
|
36
|
+
},
|
|
37
|
+
};
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The classic omp composer: rounded frame, status line embedded in the top
|
|
3
|
+
* border, and the last content row merged into the bottom border
|
|
4
|
+
* (`╰─ text … ─╯`), keeping a one-line prompt at two rows total.
|
|
5
|
+
*/
|
|
6
|
+
import { padding, truncateToWidth, visibleWidth } from "../../utils";
|
|
7
|
+
import type { ComposerChromeContext, ComposerRowContext, ComposerStyle } from "./types";
|
|
8
|
+
|
|
9
|
+
export const boxComposerStyle: ComposerStyle = {
|
|
10
|
+
id: "box",
|
|
11
|
+
sideBorders: true,
|
|
12
|
+
verticalChrome: 2,
|
|
13
|
+
statusAttachment: "top-border",
|
|
14
|
+
bottomBar: "none",
|
|
15
|
+
bottomBarGap: false,
|
|
16
|
+
defaultPromptGutter: undefined,
|
|
17
|
+
|
|
18
|
+
defaultPaddingX(themePaddingX: number | undefined): number {
|
|
19
|
+
return Math.max(0, themePaddingX ?? 2);
|
|
20
|
+
},
|
|
21
|
+
|
|
22
|
+
sideChromeWidth(paddingX: number): number {
|
|
23
|
+
return paddingX + 1;
|
|
24
|
+
},
|
|
25
|
+
|
|
26
|
+
renderTop(ctx: ComposerChromeContext): string {
|
|
27
|
+
const { box, paddingX, width, borderColor, topBorder } = ctx;
|
|
28
|
+
const topLeft = borderColor(`${box.topLeft}${box.horizontal.repeat(paddingX)}`);
|
|
29
|
+
const topRight = borderColor(`${box.horizontal.repeat(paddingX)}${box.topRight}`);
|
|
30
|
+
const topFillWidth = Math.max(0, width - this.sideChromeWidth(paddingX) * 2);
|
|
31
|
+
if (!topBorder) {
|
|
32
|
+
return topLeft + borderColor(box.horizontal.repeat(topFillWidth)) + topRight;
|
|
33
|
+
}
|
|
34
|
+
const { content, width: statusWidth } = topBorder;
|
|
35
|
+
if (statusWidth <= topFillWidth) {
|
|
36
|
+
// Status fits - add fill after it
|
|
37
|
+
const fillWidth = topFillWidth - statusWidth;
|
|
38
|
+
return topLeft + content + borderColor(box.horizontal.repeat(fillWidth)) + topRight;
|
|
39
|
+
}
|
|
40
|
+
// Status too long - truncate it
|
|
41
|
+
const truncated = truncateToWidth(content, Math.max(0, topFillWidth - 1));
|
|
42
|
+
const truncatedWidth = visibleWidth(truncated);
|
|
43
|
+
const fillWidth = Math.max(0, topFillWidth - truncatedWidth);
|
|
44
|
+
return topLeft + truncated + borderColor(box.horizontal.repeat(fillWidth)) + topRight;
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
renderRow(ctx: ComposerRowContext): string[] {
|
|
48
|
+
const { box, paddingX, width, borderColor, text, pad, isLastRow } = ctx;
|
|
49
|
+
// When the end-of-line cursor glyph (or a wide trailing grapheme) extends
|
|
50
|
+
// past the content width, shrink the right chrome by the exact overflow
|
|
51
|
+
// count: drop padding spaces first, then the trailing `─`, but never the
|
|
52
|
+
// corner/vertical bar itself.
|
|
53
|
+
const rightChromeCells = Math.max(1, paddingX + 1 - ctx.cursorOverflow);
|
|
54
|
+
if (isLastRow && ctx.imeSafeCursorTail) {
|
|
55
|
+
// Terminal frontends render IME marked text locally before committed
|
|
56
|
+
// bytes reach the application. Keep the end-of-input cursor row empty
|
|
57
|
+
// to its right so insertion cannot shift box chrome onto the next row.
|
|
58
|
+
const leftBorder = borderColor(`${box.vertical}${padding(paddingX)}`);
|
|
59
|
+
const bottomBorder = borderColor(
|
|
60
|
+
`${box.bottomLeft}${box.horizontal.repeat(Math.max(0, width - 2))}${box.bottomRight}`,
|
|
61
|
+
);
|
|
62
|
+
return [leftBorder + text, bottomBorder];
|
|
63
|
+
}
|
|
64
|
+
if (isLastRow) {
|
|
65
|
+
const bottomLeft = borderColor(`${box.bottomLeft}${box.horizontal}${padding(Math.max(0, paddingX - 1))}`);
|
|
66
|
+
const rightPad = Math.max(0, rightChromeCells - 2);
|
|
67
|
+
const includeHorizontal = rightChromeCells >= 2;
|
|
68
|
+
const bottomRightAdjusted = borderColor(
|
|
69
|
+
`${padding(rightPad)}${includeHorizontal ? box.horizontal : ""}${box.bottomRight}`,
|
|
70
|
+
);
|
|
71
|
+
return [`${bottomLeft}${text}${pad}${bottomRightAdjusted}`];
|
|
72
|
+
}
|
|
73
|
+
const leftBorder = borderColor(`${box.vertical}${padding(paddingX)}`);
|
|
74
|
+
// When the scrollbar is active, replace the right border vertical with a
|
|
75
|
+
// thumb glyph (█) inside the thumb range, keeping the track (│) elsewhere.
|
|
76
|
+
const rightGlyph = ctx.scrollbarThumb ? "█" : box.vertical;
|
|
77
|
+
const rightBorder = borderColor(`${padding(Math.max(0, rightChromeCells - 1))}${rightGlyph}`);
|
|
78
|
+
return [leftBorder + text + pad + rightBorder];
|
|
79
|
+
},
|
|
80
|
+
|
|
81
|
+
renderBottom(): undefined {
|
|
82
|
+
// The bottom border is merged into the last content row.
|
|
83
|
+
return undefined;
|
|
84
|
+
},
|
|
85
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claude Code-like composer: full-width horizontal rules above and below a
|
|
3
|
+
* borderless `❯ ` prompt. The right status group rides the top rule as a
|
|
4
|
+
* bg chip near the right edge (`─────── hi ─`); the left group renders as a
|
|
5
|
+
* plain standalone bottom bar that yields its row to the autocomplete menu.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { renderTopRule } from "./rule";
|
|
9
|
+
import type { ComposerChromeContext, ComposerRowContext, ComposerStyle } from "./types";
|
|
10
|
+
|
|
11
|
+
export const claudeComposerStyle: ComposerStyle = {
|
|
12
|
+
id: "claude",
|
|
13
|
+
sideBorders: false,
|
|
14
|
+
verticalChrome: 2,
|
|
15
|
+
statusAttachment: "top-rule-chip",
|
|
16
|
+
bottomBar: "left",
|
|
17
|
+
bottomBarGap: false,
|
|
18
|
+
defaultPromptGutter: "❯ ",
|
|
19
|
+
|
|
20
|
+
defaultPaddingX(): number {
|
|
21
|
+
return 0;
|
|
22
|
+
},
|
|
23
|
+
|
|
24
|
+
sideChromeWidth(paddingX: number): number {
|
|
25
|
+
return paddingX;
|
|
26
|
+
},
|
|
27
|
+
|
|
28
|
+
renderTop(ctx: ComposerChromeContext): string {
|
|
29
|
+
return renderTopRule(ctx);
|
|
30
|
+
},
|
|
31
|
+
|
|
32
|
+
renderRow(ctx: ComposerRowContext): string[] {
|
|
33
|
+
return [ctx.gutter + ctx.text + ctx.pad];
|
|
34
|
+
},
|
|
35
|
+
|
|
36
|
+
renderBottom(ctx: ComposerChromeContext): string {
|
|
37
|
+
return ctx.borderColor(ctx.box.horizontal.repeat(ctx.width));
|
|
38
|
+
},
|
|
39
|
+
};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compact filled composer: every input row is a one-line field with accent end
|
|
3
|
+
* caps and a subtle surface fill. The complete status line remains below it.
|
|
4
|
+
*/
|
|
5
|
+
import { padding } from "../../utils";
|
|
6
|
+
import type { ComposerRowContext, ComposerStyle } from "./types";
|
|
7
|
+
|
|
8
|
+
const LEFT_CAP = "▐";
|
|
9
|
+
const RIGHT_CAP = "▌";
|
|
10
|
+
|
|
11
|
+
/** One-row filled field with accent caps. */
|
|
12
|
+
export const fieldComposerStyle: ComposerStyle = {
|
|
13
|
+
id: "field",
|
|
14
|
+
sideBorders: true,
|
|
15
|
+
verticalChrome: 0,
|
|
16
|
+
statusAttachment: "none",
|
|
17
|
+
bottomBar: "full",
|
|
18
|
+
bottomBarGap: true,
|
|
19
|
+
defaultPromptGutter: undefined,
|
|
20
|
+
|
|
21
|
+
defaultPaddingX(): number {
|
|
22
|
+
return 1;
|
|
23
|
+
},
|
|
24
|
+
|
|
25
|
+
sideChromeWidth(paddingX: number): number {
|
|
26
|
+
return paddingX + 1;
|
|
27
|
+
},
|
|
28
|
+
|
|
29
|
+
renderTop(): undefined {
|
|
30
|
+
return undefined;
|
|
31
|
+
},
|
|
32
|
+
|
|
33
|
+
renderRow(ctx: ComposerRowContext): string[] {
|
|
34
|
+
const left = ctx.accentColor(LEFT_CAP);
|
|
35
|
+
const leftFill = padding(ctx.paddingX) + ctx.gutter + ctx.text;
|
|
36
|
+
if (ctx.imeSafeCursorTail) return [left + ctx.surfaceColor(leftFill)];
|
|
37
|
+
|
|
38
|
+
const rightChromeCells = Math.max(1, ctx.paddingX + 1 - ctx.cursorOverflow);
|
|
39
|
+
const interior = leftFill + ctx.pad + padding(rightChromeCells - 1);
|
|
40
|
+
const rightGlyph = ctx.scrollbarThumb ? "█" : RIGHT_CAP;
|
|
41
|
+
return [left + ctx.surfaceColor(interior) + ctx.accentColor(rightGlyph)];
|
|
42
|
+
},
|
|
43
|
+
|
|
44
|
+
renderBottom(): undefined {
|
|
45
|
+
return undefined;
|
|
46
|
+
},
|
|
47
|
+
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Upstream-pi composer: full-width horizontal rules above and below plain
|
|
3
|
+
* padded text — no side borders, no prompt gutter. The status bar renders as
|
|
4
|
+
* a plain standalone bottom bar with both segment groups.
|
|
5
|
+
*/
|
|
6
|
+
import { padding } from "../../utils";
|
|
7
|
+
import type { ComposerChromeContext, ComposerRowContext, ComposerStyle } from "./types";
|
|
8
|
+
|
|
9
|
+
export const piComposerStyle: ComposerStyle = {
|
|
10
|
+
id: "pi",
|
|
11
|
+
sideBorders: false,
|
|
12
|
+
verticalChrome: 2,
|
|
13
|
+
statusAttachment: "none",
|
|
14
|
+
bottomBar: "full",
|
|
15
|
+
bottomBarGap: false,
|
|
16
|
+
defaultPromptGutter: undefined,
|
|
17
|
+
|
|
18
|
+
defaultPaddingX(): number {
|
|
19
|
+
return 1;
|
|
20
|
+
},
|
|
21
|
+
|
|
22
|
+
sideChromeWidth(paddingX: number): number {
|
|
23
|
+
return paddingX;
|
|
24
|
+
},
|
|
25
|
+
|
|
26
|
+
renderTop(ctx: ComposerChromeContext): string {
|
|
27
|
+
return ctx.borderColor(ctx.box.horizontal.repeat(ctx.width));
|
|
28
|
+
},
|
|
29
|
+
|
|
30
|
+
renderRow(ctx: ComposerRowContext): string[] {
|
|
31
|
+
return [padding(this.sideChromeWidth(ctx.paddingX)) + ctx.gutter + ctx.text + ctx.pad];
|
|
32
|
+
},
|
|
33
|
+
|
|
34
|
+
renderBottom(ctx: ComposerChromeContext): string {
|
|
35
|
+
return ctx.borderColor(ctx.box.horizontal.repeat(ctx.width));
|
|
36
|
+
},
|
|
37
|
+
};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Filled composer with one strong accent rail on the left. The asymmetric
|
|
3
|
+
* silhouette separates input from transcript without enclosing it in a box.
|
|
4
|
+
*/
|
|
5
|
+
import { padding } from "../../utils";
|
|
6
|
+
import type { ComposerRowContext, ComposerStyle } from "./types";
|
|
7
|
+
|
|
8
|
+
const ACCENT_RAIL = "▎";
|
|
9
|
+
|
|
10
|
+
/** Filled composer surface anchored by a single left accent rail. */
|
|
11
|
+
export const railComposerStyle: ComposerStyle = {
|
|
12
|
+
id: "rail",
|
|
13
|
+
sideBorders: true,
|
|
14
|
+
verticalChrome: 0,
|
|
15
|
+
statusAttachment: "none",
|
|
16
|
+
bottomBar: "full",
|
|
17
|
+
bottomBarGap: true,
|
|
18
|
+
defaultPromptGutter: undefined,
|
|
19
|
+
|
|
20
|
+
defaultPaddingX(): number {
|
|
21
|
+
return 1;
|
|
22
|
+
},
|
|
23
|
+
|
|
24
|
+
sideChromeWidth(paddingX: number): number {
|
|
25
|
+
return paddingX + 1;
|
|
26
|
+
},
|
|
27
|
+
|
|
28
|
+
renderTop(): undefined {
|
|
29
|
+
return undefined;
|
|
30
|
+
},
|
|
31
|
+
|
|
32
|
+
renderRow(ctx: ComposerRowContext): string[] {
|
|
33
|
+
const rail = ctx.accentColor(ACCENT_RAIL);
|
|
34
|
+
const leftFill = padding(ctx.paddingX) + ctx.gutter + ctx.text;
|
|
35
|
+
if (ctx.imeSafeCursorTail) return [rail + ctx.surfaceColor(leftFill)];
|
|
36
|
+
|
|
37
|
+
const rightFillCells = Math.max(0, ctx.paddingX + 1 - ctx.cursorOverflow);
|
|
38
|
+
if (ctx.scrollbarThumb && rightFillCells > 0) {
|
|
39
|
+
const interior = leftFill + ctx.pad + padding(rightFillCells - 1);
|
|
40
|
+
return [rail + ctx.surfaceColor(interior) + ctx.accentColor("█")];
|
|
41
|
+
}
|
|
42
|
+
return [rail + ctx.surfaceColor(leftFill + ctx.pad + padding(rightFillCells))];
|
|
43
|
+
},
|
|
44
|
+
|
|
45
|
+
renderBottom(): undefined {
|
|
46
|
+
return undefined;
|
|
47
|
+
},
|
|
48
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { borderlessComposerStyle } from "./borderless";
|
|
2
|
+
import { boxComposerStyle } from "./box";
|
|
3
|
+
import { claudeComposerStyle } from "./claude";
|
|
4
|
+
import { fieldComposerStyle } from "./field";
|
|
5
|
+
import { piComposerStyle } from "./pi";
|
|
6
|
+
import { railComposerStyle } from "./rail";
|
|
7
|
+
import { ruleComposerStyle } from "./rule";
|
|
8
|
+
import type { ComposerStyle, EditorBorderStyle } from "./types";
|
|
9
|
+
|
|
10
|
+
const BUILTIN_COMPOSER_STYLES: Readonly<Record<string, ComposerStyle>> = {
|
|
11
|
+
box: boxComposerStyle,
|
|
12
|
+
claude: claudeComposerStyle,
|
|
13
|
+
pi: piComposerStyle,
|
|
14
|
+
borderless: borderlessComposerStyle,
|
|
15
|
+
rule: ruleComposerStyle,
|
|
16
|
+
field: fieldComposerStyle,
|
|
17
|
+
rail: railComposerStyle,
|
|
18
|
+
};
|
|
19
|
+
const extensionComposerStyles = new Map<string, ComposerStyle>();
|
|
20
|
+
|
|
21
|
+
/** Whether an id names a composer style shipped by pi-tui. */
|
|
22
|
+
export function isBuiltinComposerStyle(id: string): boolean {
|
|
23
|
+
return Object.hasOwn(BUILTIN_COMPOSER_STYLES, id);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Register one extension-owned composer style for this process.
|
|
28
|
+
*
|
|
29
|
+
* Built-in ids and duplicate extension ids are rejected. The returned disposer
|
|
30
|
+
* removes only this registration.
|
|
31
|
+
*/
|
|
32
|
+
export function registerComposerStyle(style: ComposerStyle): () => void {
|
|
33
|
+
const id = style.id.trim();
|
|
34
|
+
if (id.length === 0 || id !== style.id) throw new TypeError("Composer style id must be a non-empty trimmed string");
|
|
35
|
+
if (isBuiltinComposerStyle(id)) throw new Error(`Cannot replace built-in composer style "${id}"`);
|
|
36
|
+
if (extensionComposerStyles.has(id)) throw new Error(`Composer style "${id}" is already registered`);
|
|
37
|
+
extensionComposerStyles.set(id, style);
|
|
38
|
+
return () => {
|
|
39
|
+
if (extensionComposerStyles.get(id) === style) extensionComposerStyles.delete(id);
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Style object for a composer shape; unknown ids fall back to `box`. */
|
|
44
|
+
export function getComposerStyle(id: EditorBorderStyle): ComposerStyle {
|
|
45
|
+
return extensionComposerStyles.get(id) ?? BUILTIN_COMPOSER_STYLES[id] ?? boxComposerStyle;
|
|
46
|
+
}
|