@zakmandhro/bunti 0.1.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/LICENSE +21 -0
- package/README.md +104 -0
- package/package.json +54 -0
- package/src/colors.ts +255 -0
- package/src/components/Button.ts +104 -0
- package/src/components/Card.ts +53 -0
- package/src/components/Header.ts +65 -0
- package/src/components/Input.ts +124 -0
- package/src/components/index.ts +4 -0
- package/src/data/glyphs.ts +30 -0
- package/src/detect.ts +60 -0
- package/src/dsl.ts +661 -0
- package/src/icons.ts +186 -0
- package/src/index.ts +72 -0
- package/src/layout.ts +639 -0
- package/src/render.ts +302 -0
- package/src/state.ts +148 -0
- package/src/utils.ts +165 -0
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import type { BuntiContext } from '../dsl';
|
|
2
|
+
import type { StyleOptions } from '../layout';
|
|
3
|
+
|
|
4
|
+
export interface InputProps extends StyleOptions {
|
|
5
|
+
id: string;
|
|
6
|
+
label?: string;
|
|
7
|
+
placeholder?: string;
|
|
8
|
+
value?: string;
|
|
9
|
+
type?: 'text' | 'password';
|
|
10
|
+
onChange?: (value: string) => void;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Tactical Input Component
|
|
15
|
+
* Managed state HOC with cursor simulation, keyboard interception, and mouse focus.
|
|
16
|
+
*/
|
|
17
|
+
export function Input(ctx: BuntiContext, props: InputProps) {
|
|
18
|
+
const {
|
|
19
|
+
box,
|
|
20
|
+
color,
|
|
21
|
+
focusable,
|
|
22
|
+
state,
|
|
23
|
+
useState,
|
|
24
|
+
offsetX,
|
|
25
|
+
offsetY,
|
|
26
|
+
mouseX,
|
|
27
|
+
mouseY,
|
|
28
|
+
isMouseDown,
|
|
29
|
+
} = ctx;
|
|
30
|
+
|
|
31
|
+
// 1. Mouse Hit-Testing
|
|
32
|
+
const _finalLabelLen = props.label ? props.label.length + 1 : 0;
|
|
33
|
+
const w = props.width || 40;
|
|
34
|
+
const h = props.height || 3;
|
|
35
|
+
|
|
36
|
+
// Calculate absolute coordinates based on parent offsets and current flow cursor
|
|
37
|
+
// Assuming 100% width, x offset is just parent's offsetX.
|
|
38
|
+
const absX = offsetX;
|
|
39
|
+
const absY = offsetY + ctx.cursorY;
|
|
40
|
+
|
|
41
|
+
const isHovered =
|
|
42
|
+
mouseX >= absX &&
|
|
43
|
+
mouseX < absX + (w as number) &&
|
|
44
|
+
mouseY >= absY &&
|
|
45
|
+
mouseY < absY + (h as number);
|
|
46
|
+
|
|
47
|
+
// If clicked, force focus state
|
|
48
|
+
if (isHovered && isMouseDown && state.mouseButton === 0) {
|
|
49
|
+
state.focusedId = props.id;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// 2. Register in the global focus loop
|
|
53
|
+
const isSelected = focusable(props.id);
|
|
54
|
+
|
|
55
|
+
// 3. Manage internal state
|
|
56
|
+
const [value, setValue] = useState(props.id, props.value || '');
|
|
57
|
+
|
|
58
|
+
// 4. Handle Keyboard Interaction (only when focused)
|
|
59
|
+
if (isSelected && state.lastKey) {
|
|
60
|
+
const key = state.lastKey;
|
|
61
|
+
|
|
62
|
+
if (key === 'backspace') {
|
|
63
|
+
if (value.length > 0) {
|
|
64
|
+
const newValue = value.slice(0, -1);
|
|
65
|
+
setValue(newValue);
|
|
66
|
+
if (props.onChange) props.onChange(newValue);
|
|
67
|
+
}
|
|
68
|
+
} else if (
|
|
69
|
+
key === 'enter' ||
|
|
70
|
+
key === 'tab' ||
|
|
71
|
+
key === 'escape' ||
|
|
72
|
+
key === 'up' ||
|
|
73
|
+
key === 'down' ||
|
|
74
|
+
key === 'left' ||
|
|
75
|
+
key === 'right'
|
|
76
|
+
) {
|
|
77
|
+
// System keys: ignore
|
|
78
|
+
} else if (key.length === 1) {
|
|
79
|
+
// Standard character input
|
|
80
|
+
const newValue = value + key;
|
|
81
|
+
setValue(newValue);
|
|
82
|
+
if (props.onChange) props.onChange(newValue);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// 5. Resolve Theme
|
|
87
|
+
const neutralGray = { r: 217, g: 216, b: 213 };
|
|
88
|
+
const borderCol = isSelected ? 'black' : isHovered ? 'ash' : neutralGray;
|
|
89
|
+
const _bgColor = { r: 255, g: 255, b: 255 };
|
|
90
|
+
const textColor = 'black';
|
|
91
|
+
|
|
92
|
+
// 6. Render
|
|
93
|
+
return box(
|
|
94
|
+
{
|
|
95
|
+
width: w,
|
|
96
|
+
height: h,
|
|
97
|
+
border: 'rounded',
|
|
98
|
+
borderColor: borderCol,
|
|
99
|
+
padding: [0, 1],
|
|
100
|
+
align: 'left',
|
|
101
|
+
valign: 'middle',
|
|
102
|
+
},
|
|
103
|
+
({ text }) => {
|
|
104
|
+
// Label
|
|
105
|
+
if (props.label) {
|
|
106
|
+
text(color.dim(`${props.label} `));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Value Display with simulated cursor
|
|
110
|
+
if (value.length === 0 && props.placeholder) {
|
|
111
|
+
text(color.dim(props.placeholder));
|
|
112
|
+
} else {
|
|
113
|
+
const displayValue =
|
|
114
|
+
props.type === 'password' ? '*'.repeat(value.length) : value;
|
|
115
|
+
text(color.fg(textColor, displayValue));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Cursor (blinking)
|
|
119
|
+
if (isSelected && ctx.flicker(0.8)) {
|
|
120
|
+
text(color.black('█'));
|
|
121
|
+
}
|
|
122
|
+
},
|
|
123
|
+
);
|
|
124
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bunti Curated Glyph Registry (Nerd Font v3)
|
|
3
|
+
*/
|
|
4
|
+
export const GLYPHS: Record<string, string> = {
|
|
5
|
+
// Languages & Tech
|
|
6
|
+
js: '\u{E781}', // nf-dev-javascript
|
|
7
|
+
ts: '\u{E628}', // nf-dev-typescript
|
|
8
|
+
python: '\u{E73C}', // nf-dev-python
|
|
9
|
+
rust: '\u{E7A8}', // nf-dev-rust
|
|
10
|
+
go: '\u{E627}', // nf-dev-go
|
|
11
|
+
node: '\u{E718}', // nf-dev-nodejs
|
|
12
|
+
bun: '\u{E22F}', // nf-seti-terminal (best proxy)
|
|
13
|
+
|
|
14
|
+
// Git & Source
|
|
15
|
+
git: '\u{F02A2}', // nf-md-git
|
|
16
|
+
branch: '\u{E725}', // nf-oct-git_branch
|
|
17
|
+
commit: '\u{E729}', // nf-oct-git_commit
|
|
18
|
+
pull: '\u{E728}', // nf-oct-git_pull_request
|
|
19
|
+
merge: '\u{E727}', // nf-oct-git_merge
|
|
20
|
+
|
|
21
|
+
// System & UI
|
|
22
|
+
folder: '\u{F07B}', // nf-fa-folder
|
|
23
|
+
file: '\u{F15B}', // nf-fa-file
|
|
24
|
+
lock: '\u{F023}', // nf-fa-lock
|
|
25
|
+
search: '\u{F002}', // nf-fa-search
|
|
26
|
+
settings: '\u{F013}', // nf-fa-cog
|
|
27
|
+
terminal: '\u{F489}', // nf-oct-terminal
|
|
28
|
+
heart: '\u{F004}', // nf-fa-heart
|
|
29
|
+
star: '\u{F005}', // nf-fa-star
|
|
30
|
+
};
|
package/src/detect.ts
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bunti Terminal Capability Detection
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export interface TerminalCapabilities {
|
|
6
|
+
nerdFont: boolean;
|
|
7
|
+
glyphProtocol: boolean;
|
|
8
|
+
unicode: boolean;
|
|
9
|
+
color: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Detects terminal capabilities using environment variables and
|
|
14
|
+
* modern protocol handshakes.
|
|
15
|
+
*/
|
|
16
|
+
export async function detectCapabilities(): Promise<TerminalCapabilities> {
|
|
17
|
+
const caps: TerminalCapabilities = {
|
|
18
|
+
nerdFont: false,
|
|
19
|
+
glyphProtocol: false,
|
|
20
|
+
unicode: true,
|
|
21
|
+
color: true,
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
// 1. Environment Variable Heuristics
|
|
25
|
+
const term = process.env.TERM_PROGRAM || '';
|
|
26
|
+
const termEmulator = process.env.TERMINAL_EMULATOR || '';
|
|
27
|
+
const nerdEnv =
|
|
28
|
+
process.env.NERD_FONTS || process.env.NERD_FONT || process.env.BUNTI_NF;
|
|
29
|
+
|
|
30
|
+
// Optimistic list of terminals known to support modern fonts
|
|
31
|
+
const modernTerms = [
|
|
32
|
+
'Ghostty',
|
|
33
|
+
'WezTerm',
|
|
34
|
+
'iTerm.app',
|
|
35
|
+
'WarpTerminal',
|
|
36
|
+
'Apple_Terminal',
|
|
37
|
+
'vscode',
|
|
38
|
+
'Hyper',
|
|
39
|
+
'Rio',
|
|
40
|
+
'Term7',
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
if (nerdEnv === '1' || nerdEnv === 'true' || nerdEnv === 'yes') {
|
|
44
|
+
caps.nerdFont = true;
|
|
45
|
+
} else if (modernTerms.includes(term) || modernTerms.includes(termEmulator)) {
|
|
46
|
+
caps.nerdFont = true;
|
|
47
|
+
} else if (process.env.LC_TERMINAL === 'iTerm2') {
|
|
48
|
+
caps.nerdFont = true;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// 2. Glyph Protocol Handshake (Ghostty 1.3+, Rio, WezTerm)
|
|
52
|
+
// We send the Support Query and wait briefly for a response.
|
|
53
|
+
// Note: This is an optimistic check for now, can be expanded to
|
|
54
|
+
// a full async TTY listener if needed.
|
|
55
|
+
if (term === 'Ghostty') {
|
|
56
|
+
caps.glyphProtocol = true;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return caps;
|
|
60
|
+
}
|