@reliverse/rempts-tui 2.3.1
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/README.md +390 -0
- package/dist/components/Form.d.ts +7 -0
- package/dist/components/Form.js +35 -0
- package/dist/components/FormField.d.ts +10 -0
- package/dist/components/FormField.js +47 -0
- package/dist/components/ProgressBar.d.ts +6 -0
- package/dist/components/ProgressBar.js +25 -0
- package/dist/components/SelectField.d.ts +9 -0
- package/dist/components/SelectField.js +30 -0
- package/dist/components/mod.d.ts +6 -0
- package/dist/components/mod.js +12 -0
- package/dist/mod.d.ts +5 -0
- package/dist/mod.js +10 -0
- package/dist/register.d.ts +1 -0
- package/dist/register.js +1 -0
- package/dist/renderer.d.ts +1 -0
- package/dist/renderer.js +24 -0
- package/dist/types.d.ts +9 -0
- package/dist/types.js +0 -0
- package/package.json +39 -0
- package/src/components/Form.tsx +35 -0
- package/src/components/FormField.tsx +47 -0
- package/src/components/ProgressBar.tsx +25 -0
- package/src/components/SelectField.tsx +30 -0
- package/src/components/mod.ts +12 -0
- package/src/mod.ts +18 -0
- package/src/register.ts +1 -0
- package/src/renderer.tsx +24 -0
- package/src/types.ts +10 -0
package/README.md
ADDED
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
# rempts-tui
|
|
2
|
+
|
|
3
|
+
A React-based Terminal User Interface library for Rempts CLI framework, powered by OpenTUI's React renderer.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **React-based Components**: Build TUIs using familiar React patterns and JSX
|
|
8
|
+
- **Component Library**: Pre-built form components like `Form`, `FormField`, `SelectField`, and `ProgressBar`
|
|
9
|
+
- **OpenTUI Integration**: Full access to OpenTUI's React hooks and components
|
|
10
|
+
- **Type Safety**: Complete TypeScript support with proper type inference
|
|
11
|
+
- **Animation Support**: Built-in timeline system for smooth animations
|
|
12
|
+
- **Keyboard Handling**: Easy keyboard event management with `useKeyboard`
|
|
13
|
+
- **First-Class TUI Support**: TUI rendering is a first-class feature, not a plugin
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
bun add rempts-tui react
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Quick Start
|
|
22
|
+
|
|
23
|
+
```typescript
|
|
24
|
+
import { createCLI, defineCommand } from '@reliverse/rempts-core'
|
|
25
|
+
import { registerTuiRenderer } from '@reliverse/rempts-tui'
|
|
26
|
+
|
|
27
|
+
const cli = await createCLI({
|
|
28
|
+
name: 'my-app',
|
|
29
|
+
version: '1.0.0'
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
// Register TUI renderer to enable render() functions
|
|
33
|
+
registerTuiRenderer()
|
|
34
|
+
|
|
35
|
+
// Define a command with TUI using the render property
|
|
36
|
+
const myCommand = defineCommand({
|
|
37
|
+
name: 'deploy',
|
|
38
|
+
description: 'Deploy application',
|
|
39
|
+
render: () => (
|
|
40
|
+
<box title="Deployment" style={{ border: true, padding: 2 }}>
|
|
41
|
+
<text>Deploying...</text>
|
|
42
|
+
</box>
|
|
43
|
+
),
|
|
44
|
+
handler: async () => {
|
|
45
|
+
// CLI mode handler (used with --no-tui or in non-interactive environments)
|
|
46
|
+
console.log('Deploying application...')
|
|
47
|
+
}
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
cli.command(myCommand)
|
|
51
|
+
await cli.run()
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Usage
|
|
55
|
+
|
|
56
|
+
### Basic TUI Component
|
|
57
|
+
|
|
58
|
+
```typescript
|
|
59
|
+
import { defineCommand } from '@reliverse/rempts-core'
|
|
60
|
+
|
|
61
|
+
function MyTUI() {
|
|
62
|
+
return (
|
|
63
|
+
<box title="My App" style={{ border: true, padding: 2 }}>
|
|
64
|
+
<text>Hello from My App!</text>
|
|
65
|
+
</box>
|
|
66
|
+
)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export const myCommand = defineCommand({
|
|
70
|
+
name: 'my-command',
|
|
71
|
+
description: 'My command with TUI',
|
|
72
|
+
render: () => <MyTUI />,
|
|
73
|
+
handler: async () => {
|
|
74
|
+
console.log('Running my-command in CLI mode')
|
|
75
|
+
}
|
|
76
|
+
})
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Using Form Components
|
|
80
|
+
|
|
81
|
+
```typescript
|
|
82
|
+
import { defineCommand } from '@reliverse/rempts-core'
|
|
83
|
+
import { FormField, SelectField } from '@reliverse/rempts-tui'
|
|
84
|
+
import type { SelectOption } from '@opentui/core'
|
|
85
|
+
import { useState } from 'react'
|
|
86
|
+
|
|
87
|
+
function ConfigTUI() {
|
|
88
|
+
const [apiUrl, setApiUrl] = useState('')
|
|
89
|
+
const [region, setRegion] = useState('us-east')
|
|
90
|
+
|
|
91
|
+
const regions: SelectOption[] = [
|
|
92
|
+
{ name: 'US East', value: 'us-east', description: 'US East region' },
|
|
93
|
+
{ name: 'US West', value: 'us-west', description: 'US West region' }
|
|
94
|
+
]
|
|
95
|
+
|
|
96
|
+
return (
|
|
97
|
+
<box title="Configure Settings" border padding={2} style={{ flexDirection: 'column' }}>
|
|
98
|
+
<FormField
|
|
99
|
+
label="API URL"
|
|
100
|
+
name="apiUrl"
|
|
101
|
+
placeholder="https://api.example.com"
|
|
102
|
+
required
|
|
103
|
+
value={apiUrl}
|
|
104
|
+
onChange={setApiUrl}
|
|
105
|
+
/>
|
|
106
|
+
<SelectField
|
|
107
|
+
label="Region"
|
|
108
|
+
name="region"
|
|
109
|
+
options={regions}
|
|
110
|
+
onChange={setRegion}
|
|
111
|
+
/>
|
|
112
|
+
</box>
|
|
113
|
+
)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export const configureCommand = defineCommand({
|
|
117
|
+
name: 'configure',
|
|
118
|
+
description: 'Configure application settings',
|
|
119
|
+
render: () => <ConfigTUI />
|
|
120
|
+
})
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### Using OpenTUI Hooks
|
|
124
|
+
|
|
125
|
+
```typescript
|
|
126
|
+
import { useKeyboard, useTimeline, useTerminalDimensions } from '@reliverse/rempts-tui'
|
|
127
|
+
|
|
128
|
+
function InteractiveTUI({ command }) {
|
|
129
|
+
const [count, setCount] = useState(0)
|
|
130
|
+
const { width, height } = useTerminalDimensions()
|
|
131
|
+
|
|
132
|
+
const timeline = useTimeline({ duration: 2000 })
|
|
133
|
+
|
|
134
|
+
useKeyboard((key) => {
|
|
135
|
+
if (key.name === 'q') {
|
|
136
|
+
process.exit(0)
|
|
137
|
+
}
|
|
138
|
+
if (key.name === 'space') {
|
|
139
|
+
setCount(prev => prev + 1)
|
|
140
|
+
}
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
useEffect(() => {
|
|
144
|
+
timeline.add({ count: 0 }, {
|
|
145
|
+
count: 100,
|
|
146
|
+
duration: 2000,
|
|
147
|
+
onUpdate: (anim) => setCount(anim.targets[0].count)
|
|
148
|
+
})
|
|
149
|
+
}, [])
|
|
150
|
+
|
|
151
|
+
return (
|
|
152
|
+
<box title="Interactive Demo" style={{ border: true, padding: 2 }}>
|
|
153
|
+
<text>Count: {count}</text>
|
|
154
|
+
<text>Terminal: {width}x{height}</text>
|
|
155
|
+
<text>Press SPACE to increment, Q to quit</text>
|
|
156
|
+
</box>
|
|
157
|
+
)
|
|
158
|
+
}
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
## Component Library
|
|
162
|
+
|
|
163
|
+
### Form
|
|
164
|
+
|
|
165
|
+
A container component for building forms with keyboard navigation.
|
|
166
|
+
|
|
167
|
+
```typescript
|
|
168
|
+
<Form
|
|
169
|
+
title="My Form"
|
|
170
|
+
onSubmit={(values) => console.log(values)}
|
|
171
|
+
onCancel={() => process.exit(0)}
|
|
172
|
+
>
|
|
173
|
+
{/* Form fields */}
|
|
174
|
+
</Form>
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
**Props:**
|
|
178
|
+
|
|
179
|
+
- `title: string` - Form title
|
|
180
|
+
- `onSubmit: (values: Record<string, any>) => void` - Submit handler
|
|
181
|
+
- `onCancel?: () => void` - Cancel handler (optional)
|
|
182
|
+
|
|
183
|
+
### FormField
|
|
184
|
+
|
|
185
|
+
A text input field with label and validation.
|
|
186
|
+
|
|
187
|
+
```typescript
|
|
188
|
+
<FormField
|
|
189
|
+
label="Username"
|
|
190
|
+
name="username"
|
|
191
|
+
placeholder="Enter username"
|
|
192
|
+
required
|
|
193
|
+
value={username}
|
|
194
|
+
onChange={setUsername}
|
|
195
|
+
onSubmit={handleSubmit}
|
|
196
|
+
/>
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
**Props:**
|
|
200
|
+
|
|
201
|
+
- `label: string` - Field label
|
|
202
|
+
- `name: string` - Field name
|
|
203
|
+
- `placeholder?: string` - Placeholder text
|
|
204
|
+
- `required?: boolean` - Whether field is required
|
|
205
|
+
- `value?: string` - Current value
|
|
206
|
+
- `onChange?: (value: string) => void` - Change handler
|
|
207
|
+
- `onSubmit?: (value: string) => void` - Submit handler
|
|
208
|
+
|
|
209
|
+
### SelectField
|
|
210
|
+
|
|
211
|
+
A dropdown selection field.
|
|
212
|
+
|
|
213
|
+
```typescript
|
|
214
|
+
<SelectField
|
|
215
|
+
label="Environment"
|
|
216
|
+
name="env"
|
|
217
|
+
options={[
|
|
218
|
+
{ name: 'Development', value: 'dev', description: 'Development environment' },
|
|
219
|
+
{ name: 'Production', value: 'prod', description: 'Production environment' }
|
|
220
|
+
]}
|
|
221
|
+
onChange={setEnvironment}
|
|
222
|
+
/>
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
**Props:**
|
|
226
|
+
|
|
227
|
+
- `label: string` - Field label
|
|
228
|
+
- `name: string` - Field name
|
|
229
|
+
- `options: SelectOption[]` - Available options
|
|
230
|
+
- `required?: boolean` - Whether field is required
|
|
231
|
+
- `onChange?: (value: string) => void` - Change handler
|
|
232
|
+
|
|
233
|
+
### ProgressBar
|
|
234
|
+
|
|
235
|
+
A progress bar component for showing completion status.
|
|
236
|
+
|
|
237
|
+
```typescript
|
|
238
|
+
<ProgressBar
|
|
239
|
+
value={75}
|
|
240
|
+
label="Upload Progress"
|
|
241
|
+
color="#00ff00"
|
|
242
|
+
/>
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
**Props:**
|
|
246
|
+
|
|
247
|
+
- `value: number` - Progress value (0-100)
|
|
248
|
+
- `label?: string` - Progress label
|
|
249
|
+
- `color?: string` - Progress bar color
|
|
250
|
+
|
|
251
|
+
## OpenTUI Hooks
|
|
252
|
+
|
|
253
|
+
The plugin re-exports useful OpenTUI React hooks:
|
|
254
|
+
|
|
255
|
+
### useKeyboard
|
|
256
|
+
|
|
257
|
+
Handle keyboard events.
|
|
258
|
+
|
|
259
|
+
```typescript
|
|
260
|
+
import { useKeyboard } from '@reliverse/rempts-tui'
|
|
261
|
+
|
|
262
|
+
useKeyboard((key) => {
|
|
263
|
+
if (key.name === 'escape') {
|
|
264
|
+
process.exit(0)
|
|
265
|
+
}
|
|
266
|
+
})
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
### useRenderer
|
|
270
|
+
|
|
271
|
+
Access the OpenTUI renderer instance.
|
|
272
|
+
|
|
273
|
+
```typescript
|
|
274
|
+
import { useRenderer } from '@reliverse/rempts-tui'
|
|
275
|
+
|
|
276
|
+
const renderer = useRenderer()
|
|
277
|
+
renderer.console.show()
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
### useTerminalDimensions
|
|
281
|
+
|
|
282
|
+
Get current terminal dimensions.
|
|
283
|
+
|
|
284
|
+
```typescript
|
|
285
|
+
import { useTerminalDimensions } from '@reliverse/rempts-tui'
|
|
286
|
+
|
|
287
|
+
const { width, height } = useTerminalDimensions()
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
### useTimeline
|
|
291
|
+
|
|
292
|
+
Create and manage animations.
|
|
293
|
+
|
|
294
|
+
```typescript
|
|
295
|
+
import { useTimeline } from '@reliverse/rempts-tui'
|
|
296
|
+
|
|
297
|
+
const timeline = useTimeline({ duration: 2000 })
|
|
298
|
+
|
|
299
|
+
timeline.add({ x: 0 }, {
|
|
300
|
+
x: 100,
|
|
301
|
+
duration: 2000,
|
|
302
|
+
onUpdate: (anim) => setX(anim.targets[0].x)
|
|
303
|
+
})
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
### useOnResize
|
|
307
|
+
|
|
308
|
+
Handle terminal resize events.
|
|
309
|
+
|
|
310
|
+
```typescript
|
|
311
|
+
import { useOnResize } from '@reliverse/rempts-tui'
|
|
312
|
+
|
|
313
|
+
useOnResize((width, height) => {
|
|
314
|
+
console.log(`Terminal resized to ${width}x${height}`)
|
|
315
|
+
})
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
## Plugin Configuration
|
|
319
|
+
|
|
320
|
+
```typescript
|
|
321
|
+
import { tuiPlugin } from '@reliverse/rempts-tui'
|
|
322
|
+
|
|
323
|
+
const plugin = tuiPlugin({
|
|
324
|
+
renderer: {
|
|
325
|
+
exitOnCtrlC: false,
|
|
326
|
+
targetFps: 30,
|
|
327
|
+
enableMouseMovement: true
|
|
328
|
+
},
|
|
329
|
+
theme: 'dark',
|
|
330
|
+
autoForm: false
|
|
331
|
+
})
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
**Options:**
|
|
335
|
+
|
|
336
|
+
- `renderer?: CliRendererConfig` - OpenTUI renderer configuration
|
|
337
|
+
- `theme?: 'light' | 'dark' | ThemeConfig` - Theme configuration
|
|
338
|
+
- `autoForm?: boolean` - Enable auto-form generation (disabled for now)
|
|
339
|
+
|
|
340
|
+
## OpenTUI Components
|
|
341
|
+
|
|
342
|
+
You can use any OpenTUI React components directly:
|
|
343
|
+
|
|
344
|
+
```typescript
|
|
345
|
+
import { render } from '@opentui/react'
|
|
346
|
+
|
|
347
|
+
function MyComponent() {
|
|
348
|
+
return (
|
|
349
|
+
<box style={{ border: true, padding: 2 }}>
|
|
350
|
+
<text>Hello World</text>
|
|
351
|
+
<input placeholder="Type here..." />
|
|
352
|
+
<select options={options} />
|
|
353
|
+
</box>
|
|
354
|
+
)
|
|
355
|
+
}
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
Available components:
|
|
359
|
+
|
|
360
|
+
- `<box>` - Container with borders and layout
|
|
361
|
+
- `<text>` - Text display with styling
|
|
362
|
+
- `<input>` - Text input field
|
|
363
|
+
- `<select>` - Dropdown selection
|
|
364
|
+
- `<scrollbox>` - Scrollable container
|
|
365
|
+
- `<ascii-font>` - ASCII art text
|
|
366
|
+
- `<tab-select>` - Tab-based selection
|
|
367
|
+
|
|
368
|
+
## Examples
|
|
369
|
+
|
|
370
|
+
See the `examples/tui-demo` directory for complete examples:
|
|
371
|
+
|
|
372
|
+
- **Deploy Command**: Animated progress bar with timeline
|
|
373
|
+
- **Configure Command**: Form with input and select fields
|
|
374
|
+
|
|
375
|
+
## TypeScript Support
|
|
376
|
+
|
|
377
|
+
The plugin provides full TypeScript support:
|
|
378
|
+
|
|
379
|
+
```typescript
|
|
380
|
+
import type { TuiComponent, TuiComponentProps } from '@reliverse/rempts-tui'
|
|
381
|
+
|
|
382
|
+
const MyTUI: TuiComponent = ({ command, args, store }) => {
|
|
383
|
+
// Fully typed props
|
|
384
|
+
return <box>{command.name}</box>
|
|
385
|
+
}
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
## License
|
|
389
|
+
|
|
390
|
+
MIT
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { useKeyboard } from "@opentui/react";
|
|
2
|
+
import { useCallback, useState } from "react";
|
|
3
|
+
|
|
4
|
+
export interface FormProps {
|
|
5
|
+
title: string;
|
|
6
|
+
onSubmit: (values: Record<string, any>) => void;
|
|
7
|
+
onCancel?: () => void;
|
|
8
|
+
children: React.ReactNode;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function Form({ title, onSubmit, onCancel, children }: FormProps) {
|
|
12
|
+
const [formValues, setFormValues] = useState<Record<string, any>>({});
|
|
13
|
+
|
|
14
|
+
const _handleFieldChange = useCallback((name: string, value: any) => {
|
|
15
|
+
setFormValues((prev) => ({ ...prev, [name]: value }));
|
|
16
|
+
}, []);
|
|
17
|
+
|
|
18
|
+
useKeyboard((key) => {
|
|
19
|
+
if (key.name === "escape" && onCancel) {
|
|
20
|
+
onCancel();
|
|
21
|
+
}
|
|
22
|
+
if (key.name === "enter") {
|
|
23
|
+
onSubmit(formValues);
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<box border padding={2} style={{ flexDirection: "column" }} title={title}>
|
|
29
|
+
{children}
|
|
30
|
+
<box style={{ flexDirection: "row", gap: 2, marginTop: 2 }}>
|
|
31
|
+
<text content="Press Enter to submit, Esc to cancel" />
|
|
32
|
+
</box>
|
|
33
|
+
</box>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export interface FormFieldProps {
|
|
2
|
+
label: string;
|
|
3
|
+
name: string;
|
|
4
|
+
placeholder?: string;
|
|
5
|
+
required?: boolean;
|
|
6
|
+
value?: string;
|
|
7
|
+
onChange?: (value: string) => void;
|
|
8
|
+
onSubmit?: (value: string) => void;
|
|
9
|
+
}
|
|
10
|
+
export declare function FormField({ label, name, placeholder, required, value: initialValue, onChange, onSubmit, }: FormFieldProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
|
|
3
|
+
export interface FormFieldProps {
|
|
4
|
+
label: string;
|
|
5
|
+
name: string;
|
|
6
|
+
placeholder?: string;
|
|
7
|
+
required?: boolean;
|
|
8
|
+
value?: string;
|
|
9
|
+
onChange?: (value: string) => void;
|
|
10
|
+
onSubmit?: (value: string) => void;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function FormField({
|
|
14
|
+
label,
|
|
15
|
+
name,
|
|
16
|
+
placeholder,
|
|
17
|
+
required,
|
|
18
|
+
value: initialValue = "",
|
|
19
|
+
onChange,
|
|
20
|
+
onSubmit,
|
|
21
|
+
}: FormFieldProps) {
|
|
22
|
+
const [_value, setValue] = useState(initialValue);
|
|
23
|
+
|
|
24
|
+
const handleInput = (newValue: string) => {
|
|
25
|
+
setValue(newValue);
|
|
26
|
+
onChange?.(newValue);
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const handleSubmit = (submittedValue: string) => {
|
|
30
|
+
onSubmit?.(submittedValue);
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<box style={{ flexDirection: "column", marginBottom: 1 }}>
|
|
35
|
+
<text content={`${label}${required ? " *" : ""}`} />
|
|
36
|
+
<box border height={3} style={{ marginTop: 0.5 }} title={label}>
|
|
37
|
+
<input
|
|
38
|
+
focused={true}
|
|
39
|
+
onInput={handleInput}
|
|
40
|
+
onSubmit={handleSubmit}
|
|
41
|
+
placeholder={placeholder}
|
|
42
|
+
style={{ focusedBackgroundColor: "#000000" }}
|
|
43
|
+
/>
|
|
44
|
+
</box>
|
|
45
|
+
</box>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export interface ProgressBarProps {
|
|
2
|
+
value: number; // 0-100
|
|
3
|
+
label?: string;
|
|
4
|
+
color?: string;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function ProgressBar({ value, label, color = "#00ff00" }: ProgressBarProps) {
|
|
8
|
+
const clampedValue = Math.max(0, Math.min(100, value));
|
|
9
|
+
|
|
10
|
+
return (
|
|
11
|
+
<box style={{ flexDirection: "column" }}>
|
|
12
|
+
{label && <text content={label} />}
|
|
13
|
+
<box style={{ backgroundColor: "#333", height: 3, marginTop: 0.5 }}>
|
|
14
|
+
<box
|
|
15
|
+
style={{
|
|
16
|
+
width: `${clampedValue}%`,
|
|
17
|
+
backgroundColor: color,
|
|
18
|
+
height: 1,
|
|
19
|
+
}}
|
|
20
|
+
/>
|
|
21
|
+
</box>
|
|
22
|
+
<text content={`${Math.floor(clampedValue)}%`} style={{ marginTop: 0.5 }} />
|
|
23
|
+
</box>
|
|
24
|
+
);
|
|
25
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { SelectOption } from "@opentui/core";
|
|
2
|
+
export interface SelectFieldProps {
|
|
3
|
+
label: string;
|
|
4
|
+
name: string;
|
|
5
|
+
options: SelectOption[];
|
|
6
|
+
required?: boolean;
|
|
7
|
+
onChange?: (value: string) => void;
|
|
8
|
+
}
|
|
9
|
+
export declare function SelectField({ label, name, options, required, onChange }: SelectFieldProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { SelectOption } from "@opentui/core";
|
|
2
|
+
import { useState } from "react";
|
|
3
|
+
|
|
4
|
+
export interface SelectFieldProps {
|
|
5
|
+
label: string;
|
|
6
|
+
name: string;
|
|
7
|
+
options: SelectOption[];
|
|
8
|
+
required?: boolean;
|
|
9
|
+
onChange?: (value: string) => void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function SelectField({ label, name, options, required, onChange }: SelectFieldProps) {
|
|
13
|
+
const [_selectedIndex, setSelectedIndex] = useState(0);
|
|
14
|
+
|
|
15
|
+
const handleChange = (index: number, option: SelectOption | null) => {
|
|
16
|
+
setSelectedIndex(index);
|
|
17
|
+
if (option) {
|
|
18
|
+
onChange?.(option.value);
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<box style={{ flexDirection: "column", marginBottom: 1 }}>
|
|
24
|
+
<text content={`${label}${required ? " *" : ""}`} />
|
|
25
|
+
<box border height={8} style={{ marginTop: 0.5 }}>
|
|
26
|
+
<select focused={true} onChange={handleChange} options={options} />
|
|
27
|
+
</box>
|
|
28
|
+
</box>
|
|
29
|
+
);
|
|
30
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { bold, fg, italic, TextAttributes, t } from "@opentui/core";
|
|
2
|
+
export { useKeyboard, useOnResize, useRenderer, useTerminalDimensions, useTimeline, } from "@opentui/react";
|
|
3
|
+
export * from "./Form.js";
|
|
4
|
+
export * from "./FormField.js";
|
|
5
|
+
export * from "./ProgressBar.js";
|
|
6
|
+
export * from "./SelectField.js";
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export { bold, fg, italic, TextAttributes, t } from "@opentui/core";
|
|
2
|
+
export {
|
|
3
|
+
useKeyboard,
|
|
4
|
+
useOnResize,
|
|
5
|
+
useRenderer,
|
|
6
|
+
useTerminalDimensions,
|
|
7
|
+
useTimeline
|
|
8
|
+
} from "@opentui/react";
|
|
9
|
+
export * from "./Form.js";
|
|
10
|
+
export * from "./FormField.js";
|
|
11
|
+
export * from "./ProgressBar.js";
|
|
12
|
+
export * from "./SelectField.js";
|
package/dist/mod.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export type { CliRendererConfig, KeyEvent, SelectOption } from "@opentui/core";
|
|
2
|
+
export { bold, fg, italic, TextAttributes, t } from "@opentui/core";
|
|
3
|
+
export { useKeyboard, useOnResize, useRenderer, useTerminalDimensions, useTimeline, } from "@opentui/react";
|
|
4
|
+
export * from "./components/mod.js";
|
|
5
|
+
export { registerTuiRenderer } from "./renderer.js";
|
package/dist/mod.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export { bold, fg, italic, TextAttributes, t } from "@opentui/core";
|
|
2
|
+
export {
|
|
3
|
+
useKeyboard,
|
|
4
|
+
useOnResize,
|
|
5
|
+
useRenderer,
|
|
6
|
+
useTerminalDimensions,
|
|
7
|
+
useTimeline
|
|
8
|
+
} from "@opentui/react";
|
|
9
|
+
export * from "./components/mod.js";
|
|
10
|
+
export { registerTuiRenderer } from "./renderer.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { registerTuiRenderer as registerTui } from "./renderer.js";
|
package/dist/register.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { registerTuiRenderer as registerTui } from "./renderer.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function registerTuiRenderer(): void;
|
package/dist/renderer.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { createCliRenderer } from "@opentui/core";
|
|
2
|
+
import { createRoot } from "@opentui/react";
|
|
3
|
+
import type { RenderArgs } from "@reliverse/rempts-core";
|
|
4
|
+
import { registerTuiRenderer as coreRegisterTuiRenderer } from "@reliverse/rempts-core";
|
|
5
|
+
import type { ReactElement } from "react";
|
|
6
|
+
|
|
7
|
+
export function registerTuiRenderer(): void {
|
|
8
|
+
coreRegisterTuiRenderer(async (args: RenderArgs<any, any>) => {
|
|
9
|
+
const component = args.command.render?.(args);
|
|
10
|
+
|
|
11
|
+
if (!component) {
|
|
12
|
+
throw new Error("TUI render result is missing. Ensure command.render returns JSX.");
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const renderer = await createCliRenderer({
|
|
16
|
+
exitOnCtrlC: args.rendererOptions?.exitOnCtrlC ?? true,
|
|
17
|
+
targetFps: args.rendererOptions?.targetFps ?? 30,
|
|
18
|
+
enableMouseMovement: args.rendererOptions?.enableMouseMovement ?? true,
|
|
19
|
+
...args.rendererOptions,
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
createRoot(renderer).render(component as ReactElement);
|
|
23
|
+
});
|
|
24
|
+
}
|
package/dist/types.d.ts
ADDED
package/dist/types.js
ADDED
|
File without changes
|
package/package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@reliverse/rempts-tui",
|
|
3
|
+
"version": "2.3.1",
|
|
4
|
+
"description": "Terminal User Interface plugin for Rempts CLI framework",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"cli",
|
|
7
|
+
"interactive",
|
|
8
|
+
"rempts",
|
|
9
|
+
"terminal",
|
|
10
|
+
"tui",
|
|
11
|
+
"ui"
|
|
12
|
+
],
|
|
13
|
+
"license": "MIT",
|
|
14
|
+
"author": "bun dler Contributors",
|
|
15
|
+
"files": [
|
|
16
|
+
"dist",
|
|
17
|
+
"src"
|
|
18
|
+
],
|
|
19
|
+
"type": "module",
|
|
20
|
+
"main": "src/mod.ts",
|
|
21
|
+
"exports": {
|
|
22
|
+
".": {
|
|
23
|
+
"types": "./dist/mod.d.ts",
|
|
24
|
+
"default": "./dist/mod.js"
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"@opentui/core": "^0.1.72",
|
|
29
|
+
"@opentui/react": "^0.1.72",
|
|
30
|
+
"@reliverse/rempts-core": "2.3.1"
|
|
31
|
+
},
|
|
32
|
+
"peerDependencies": {
|
|
33
|
+
"@opentui/core": "^0.1.72",
|
|
34
|
+
"react": "^19.2.3"
|
|
35
|
+
},
|
|
36
|
+
"publishConfig": {
|
|
37
|
+
"access": "public"
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { useKeyboard } from "@opentui/react";
|
|
2
|
+
import { useCallback, useState } from "react";
|
|
3
|
+
|
|
4
|
+
export interface FormProps {
|
|
5
|
+
title: string;
|
|
6
|
+
onSubmit: (values: Record<string, any>) => void;
|
|
7
|
+
onCancel?: () => void;
|
|
8
|
+
children: React.ReactNode;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function Form({ title, onSubmit, onCancel, children }: FormProps) {
|
|
12
|
+
const [formValues, setFormValues] = useState<Record<string, any>>({});
|
|
13
|
+
|
|
14
|
+
const _handleFieldChange = useCallback((name: string, value: any) => {
|
|
15
|
+
setFormValues((prev) => ({ ...prev, [name]: value }));
|
|
16
|
+
}, []);
|
|
17
|
+
|
|
18
|
+
useKeyboard((key) => {
|
|
19
|
+
if (key.name === "escape" && onCancel) {
|
|
20
|
+
onCancel();
|
|
21
|
+
}
|
|
22
|
+
if (key.name === "enter") {
|
|
23
|
+
onSubmit(formValues);
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<box border padding={2} style={{ flexDirection: "column" }} title={title}>
|
|
29
|
+
{children}
|
|
30
|
+
<box style={{ flexDirection: "row", gap: 2, marginTop: 2 }}>
|
|
31
|
+
<text content="Press Enter to submit, Esc to cancel" />
|
|
32
|
+
</box>
|
|
33
|
+
</box>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
|
|
3
|
+
export interface FormFieldProps {
|
|
4
|
+
label: string;
|
|
5
|
+
name: string;
|
|
6
|
+
placeholder?: string;
|
|
7
|
+
required?: boolean;
|
|
8
|
+
value?: string;
|
|
9
|
+
onChange?: (value: string) => void;
|
|
10
|
+
onSubmit?: (value: string) => void;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function FormField({
|
|
14
|
+
label,
|
|
15
|
+
name,
|
|
16
|
+
placeholder,
|
|
17
|
+
required,
|
|
18
|
+
value: initialValue = "",
|
|
19
|
+
onChange,
|
|
20
|
+
onSubmit,
|
|
21
|
+
}: FormFieldProps) {
|
|
22
|
+
const [_value, setValue] = useState(initialValue);
|
|
23
|
+
|
|
24
|
+
const handleInput = (newValue: string) => {
|
|
25
|
+
setValue(newValue);
|
|
26
|
+
onChange?.(newValue);
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const handleSubmit = (submittedValue: string) => {
|
|
30
|
+
onSubmit?.(submittedValue);
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<box style={{ flexDirection: "column", marginBottom: 1 }}>
|
|
35
|
+
<text content={`${label}${required ? " *" : ""}`} />
|
|
36
|
+
<box border height={3} style={{ marginTop: 0.5 }} title={label}>
|
|
37
|
+
<input
|
|
38
|
+
focused={true}
|
|
39
|
+
onInput={handleInput}
|
|
40
|
+
onSubmit={handleSubmit}
|
|
41
|
+
placeholder={placeholder}
|
|
42
|
+
style={{ focusedBackgroundColor: "#000000" }}
|
|
43
|
+
/>
|
|
44
|
+
</box>
|
|
45
|
+
</box>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export interface ProgressBarProps {
|
|
2
|
+
value: number; // 0-100
|
|
3
|
+
label?: string;
|
|
4
|
+
color?: string;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function ProgressBar({ value, label, color = "#00ff00" }: ProgressBarProps) {
|
|
8
|
+
const clampedValue = Math.max(0, Math.min(100, value));
|
|
9
|
+
|
|
10
|
+
return (
|
|
11
|
+
<box style={{ flexDirection: "column" }}>
|
|
12
|
+
{label && <text content={label} />}
|
|
13
|
+
<box style={{ backgroundColor: "#333", height: 3, marginTop: 0.5 }}>
|
|
14
|
+
<box
|
|
15
|
+
style={{
|
|
16
|
+
width: `${clampedValue}%`,
|
|
17
|
+
backgroundColor: color,
|
|
18
|
+
height: 1,
|
|
19
|
+
}}
|
|
20
|
+
/>
|
|
21
|
+
</box>
|
|
22
|
+
<text content={`${Math.floor(clampedValue)}%`} style={{ marginTop: 0.5 }} />
|
|
23
|
+
</box>
|
|
24
|
+
);
|
|
25
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { SelectOption } from "@opentui/core";
|
|
2
|
+
import { useState } from "react";
|
|
3
|
+
|
|
4
|
+
export interface SelectFieldProps {
|
|
5
|
+
label: string;
|
|
6
|
+
name: string;
|
|
7
|
+
options: SelectOption[];
|
|
8
|
+
required?: boolean;
|
|
9
|
+
onChange?: (value: string) => void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function SelectField({ label, name, options, required, onChange }: SelectFieldProps) {
|
|
13
|
+
const [_selectedIndex, setSelectedIndex] = useState(0);
|
|
14
|
+
|
|
15
|
+
const handleChange = (index: number, option: SelectOption | null) => {
|
|
16
|
+
setSelectedIndex(index);
|
|
17
|
+
if (option) {
|
|
18
|
+
onChange?.(option.value);
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<box style={{ flexDirection: "column", marginBottom: 1 }}>
|
|
24
|
+
<text content={`${label}${required ? " *" : ""}`} />
|
|
25
|
+
<box border height={8} style={{ marginTop: 0.5 }}>
|
|
26
|
+
<select focused={true} onChange={handleChange} options={options} />
|
|
27
|
+
</box>
|
|
28
|
+
</box>
|
|
29
|
+
);
|
|
30
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export { bold, fg, italic, TextAttributes, t } from "@opentui/core";
|
|
2
|
+
export {
|
|
3
|
+
useKeyboard,
|
|
4
|
+
useOnResize,
|
|
5
|
+
useRenderer,
|
|
6
|
+
useTerminalDimensions,
|
|
7
|
+
useTimeline,
|
|
8
|
+
} from "@opentui/react";
|
|
9
|
+
export * from "./Form";
|
|
10
|
+
export * from "./FormField";
|
|
11
|
+
export * from "./ProgressBar";
|
|
12
|
+
export * from "./SelectField";
|
package/src/mod.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// Export TUI renderer registration
|
|
2
|
+
|
|
3
|
+
// Re-export useful OpenTUI core types and utilities
|
|
4
|
+
export type { CliRendererConfig, KeyEvent, SelectOption } from "@opentui/core";
|
|
5
|
+
// Re-export text styling utilities
|
|
6
|
+
export { bold, fg, italic, TextAttributes, t } from "@opentui/core";
|
|
7
|
+
|
|
8
|
+
// Re-export useful OpenTUI React hooks and types
|
|
9
|
+
export {
|
|
10
|
+
useKeyboard,
|
|
11
|
+
useOnResize,
|
|
12
|
+
useRenderer,
|
|
13
|
+
useTerminalDimensions,
|
|
14
|
+
useTimeline,
|
|
15
|
+
} from "@opentui/react";
|
|
16
|
+
// Export component library
|
|
17
|
+
export * from "./components/mod";
|
|
18
|
+
export { registerTuiRenderer } from "./renderer";
|
package/src/register.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { registerTuiRenderer as registerTui } from "./renderer";
|
package/src/renderer.tsx
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { createCliRenderer } from "@opentui/core";
|
|
2
|
+
import { createRoot } from "@opentui/react";
|
|
3
|
+
import type { RenderArgs } from "@reliverse/rempts-core";
|
|
4
|
+
import { registerTuiRenderer as coreRegisterTuiRenderer } from "@reliverse/rempts-core";
|
|
5
|
+
import type { ReactElement } from "react";
|
|
6
|
+
|
|
7
|
+
export function registerTuiRenderer(): void {
|
|
8
|
+
coreRegisterTuiRenderer(async (args: RenderArgs<any, any>) => {
|
|
9
|
+
const component = args.command.render?.(args);
|
|
10
|
+
|
|
11
|
+
if (!component) {
|
|
12
|
+
throw new Error("TUI render result is missing. Ensure command.render returns JSX.");
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const renderer = await createCliRenderer({
|
|
16
|
+
exitOnCtrlC: args.rendererOptions?.exitOnCtrlC ?? true,
|
|
17
|
+
targetFps: args.rendererOptions?.targetFps ?? 30,
|
|
18
|
+
enableMouseMovement: args.rendererOptions?.enableMouseMovement ?? true,
|
|
19
|
+
...args.rendererOptions,
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
createRoot(renderer).render(component as ReactElement);
|
|
23
|
+
});
|
|
24
|
+
}
|