bare-tui 0.0.0 → 0.0.2
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 +201 -0
- package/NOTICE +13 -0
- package/README.md +261 -0
- package/ansi.js +34 -0
- package/commands.js +67 -0
- package/components/autocomplete.js +208 -0
- package/components/checkbox.js +68 -0
- package/components/filepicker.js +232 -0
- package/components/focus.js +129 -0
- package/components/help.js +117 -0
- package/components/list.js +172 -0
- package/components/paginator.js +111 -0
- package/components/progress.js +78 -0
- package/components/radio.js +113 -0
- package/components/select.js +165 -0
- package/components/spinner.js +70 -0
- package/components/stopwatch.js +88 -0
- package/components/table.js +130 -0
- package/components/textarea.js +279 -0
- package/components/textinput.js +135 -0
- package/components/timer.js +91 -0
- package/components/viewport.js +104 -0
- package/docs/autocomplete.md +80 -0
- package/docs/checkbox.md +43 -0
- package/docs/filepicker.md +80 -0
- package/docs/focus.md +68 -0
- package/docs/help.md +45 -0
- package/docs/list.md +47 -0
- package/docs/paginator.md +43 -0
- package/docs/progress.md +36 -0
- package/docs/radio.md +49 -0
- package/docs/select.md +54 -0
- package/docs/spinner.md +54 -0
- package/docs/stopwatch.md +52 -0
- package/docs/table.md +52 -0
- package/docs/textarea.md +43 -0
- package/docs/textinput.md +51 -0
- package/docs/timer.md +57 -0
- package/docs/viewport.md +39 -0
- package/index.d.ts +1 -0
- package/index.js +71 -0
- package/key.js +29 -0
- package/messages.js +62 -0
- package/mouse.js +102 -0
- package/package.json +59 -1
- package/program.js +387 -0
- package/renderer.js +67 -0
- package/style.js +473 -0
package/docs/checkbox.md
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# checkbox
|
|
2
|
+
|
|
3
|
+
A single boolean toggle. A focus-gated leaf input: it only reacts when focused,
|
|
4
|
+
so a parent can broadcast keys to several controls and only the focused one
|
|
5
|
+
moves.
|
|
6
|
+
|
|
7
|
+
[← all components](../README.md#components)
|
|
8
|
+
|
|
9
|
+
## Usage
|
|
10
|
+
|
|
11
|
+
```js
|
|
12
|
+
const { checkbox } = require('bare-tui')
|
|
13
|
+
|
|
14
|
+
const agree = checkbox.create({ label: 'I agree', checked: false }).focus()
|
|
15
|
+
|
|
16
|
+
// in update: const [c] = agree.update(msg); this.agree = c
|
|
17
|
+
// in view: agree.view() // "› [x] I agree"
|
|
18
|
+
agree.checked // boolean
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Options
|
|
22
|
+
|
|
23
|
+
| Option | Default | Description |
|
|
24
|
+
| --------------------------------- | ------------- | ------------------------ |
|
|
25
|
+
| `label` | `''` | Text shown after the box |
|
|
26
|
+
| `checked` | `false` | Initial state |
|
|
27
|
+
| `focused` | `false` | Start focused |
|
|
28
|
+
| `checkedGlyph` / `uncheckedGlyph` | `[x]` / `[ ]` | Box characters |
|
|
29
|
+
|
|
30
|
+
## API
|
|
31
|
+
|
|
32
|
+
- `toggle()` / `setChecked(v)` — change state imperatively.
|
|
33
|
+
- `focus()` / `blur()` — gate input. `.checked` / `.focused` are readable.
|
|
34
|
+
|
|
35
|
+
## Keys
|
|
36
|
+
|
|
37
|
+
`space` toggles. **`enter` is deliberately not consumed**, so a parent form can
|
|
38
|
+
keep `enter` for "submit" while a checkbox has focus. Bindings are exported as
|
|
39
|
+
`checkbox.keys` for the [help](help.md) component.
|
|
40
|
+
|
|
41
|
+
A leading `›` marks focus (a blank when blurred), the same idiom
|
|
42
|
+
[radio](radio.md) and [select](select.md) use, so the pointer never shifts the
|
|
43
|
+
line width.
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# filepicker
|
|
2
|
+
|
|
3
|
+
Browse a filesystem and pick a file. Directory reads happen through commands
|
|
4
|
+
(async), and the filesystem dependency is **injected** — so the framework core
|
|
5
|
+
never depends on `bare-fs`, and your file UIs are trivially testable.
|
|
6
|
+
|
|
7
|
+
[← all components](../README.md#components)
|
|
8
|
+
|
|
9
|
+
## Usage
|
|
10
|
+
|
|
11
|
+
```js
|
|
12
|
+
const { filepicker } = require('bare-tui')
|
|
13
|
+
|
|
14
|
+
class App {
|
|
15
|
+
constructor() {
|
|
16
|
+
this.fp = filepicker.create({ height: 14 }) // lazily requires bare-fs
|
|
17
|
+
}
|
|
18
|
+
init() {
|
|
19
|
+
return this.fp.init()
|
|
20
|
+
}
|
|
21
|
+
update(msg) {
|
|
22
|
+
if (msg.type === 'filepicker.select') {
|
|
23
|
+
/* msg.path chosen */ return [this, null]
|
|
24
|
+
}
|
|
25
|
+
const [fp, cmd] = this.fp.update(msg)
|
|
26
|
+
this.fp = fp
|
|
27
|
+
return [this, cmd]
|
|
28
|
+
}
|
|
29
|
+
view() {
|
|
30
|
+
return this.fp.view()
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Dependency injection
|
|
36
|
+
|
|
37
|
+
The component never `require`s `bare-fs`/`bare-path` at module load. You either
|
|
38
|
+
pass them in, or `create()` lazily requires them — so importing the framework
|
|
39
|
+
pulls in nothing, and only constructing a real filepicker loads the filesystem.
|
|
40
|
+
The only surface used is `fs.readdir(dir, { withFileTypes: true }, cb)` and
|
|
41
|
+
`path.join` / `path.dirname`.
|
|
42
|
+
|
|
43
|
+
## Options
|
|
44
|
+
|
|
45
|
+
| Option | Default | Description |
|
|
46
|
+
| ------------ | --------------------------- | ------------------------- |
|
|
47
|
+
| `fs` | lazy `require('bare-fs')` | Filesystem implementation |
|
|
48
|
+
| `path` | lazy `require('bare-path')` | Path implementation |
|
|
49
|
+
| `cwd` | `path.resolve('.')` | Starting directory |
|
|
50
|
+
| `height` | `12` | Visible rows |
|
|
51
|
+
| `showHidden` | `false` | Show dotfiles |
|
|
52
|
+
|
|
53
|
+
## API
|
|
54
|
+
|
|
55
|
+
- `init()` → a command that reads the starting directory.
|
|
56
|
+
- `selectedPath()` — the chosen file path (once selected).
|
|
57
|
+
|
|
58
|
+
## Messages
|
|
59
|
+
|
|
60
|
+
- `{ type: 'filepicker.entries', dir, entries }` — a directory was read.
|
|
61
|
+
- `{ type: 'filepicker.error', dir, error }` — a read failed.
|
|
62
|
+
- `{ type: 'filepicker.select', path }` — a file was chosen.
|
|
63
|
+
|
|
64
|
+
## Keys
|
|
65
|
+
|
|
66
|
+
`↑`/`↓` (`k`/`j`) move, `enter`/`→` (`l`) open a directory or pick a file,
|
|
67
|
+
`backspace`/`←` (`h`) go up.
|
|
68
|
+
|
|
69
|
+
## Testing
|
|
70
|
+
|
|
71
|
+
`filepicker.mock(tree)` returns an in-memory `{ fs, path, root }` from a plain
|
|
72
|
+
object — keys are entry names, an object value is a directory, anything else a
|
|
73
|
+
file:
|
|
74
|
+
|
|
75
|
+
```js
|
|
76
|
+
const m = filepicker.mock({ docs: { 'a.md': null }, 'readme.txt': null })
|
|
77
|
+
const fp = filepicker.create({ fs: m.fs, path: m.path, cwd: m.root })
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
No real disk access — ideal for tests.
|
package/docs/focus.md
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# focus
|
|
2
|
+
|
|
3
|
+
An ordered focus ring across a set of child components. This is connective
|
|
4
|
+
tissue, not a widget — it has **no `view()`**. It owns the one job every
|
|
5
|
+
multi-field screen reimplements: moving focus between children with
|
|
6
|
+
`tab`/`shift+tab`, blurring the old and focusing the new.
|
|
7
|
+
|
|
8
|
+
It relies only on the component contract the built-ins already follow
|
|
9
|
+
(`focus()` / `blur()` and `update(msg) → [model, cmd]`).
|
|
10
|
+
|
|
11
|
+
[← all components](../README.md#components)
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
```js
|
|
16
|
+
const { focus, textinput, radio, checkbox } = require('bare-tui')
|
|
17
|
+
|
|
18
|
+
this.ring = focus.create({
|
|
19
|
+
items: [
|
|
20
|
+
textinput.create({ prompt: '> ' }),
|
|
21
|
+
radio.create({ options: ['a', 'b'] }),
|
|
22
|
+
checkbox.create({ label: 'ok' })
|
|
23
|
+
]
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
update(msg) {
|
|
27
|
+
// Handle global / submit keys FIRST so a focused child can't swallow them.
|
|
28
|
+
if (key.matches(msg, 'ctrl+c')) return [this, quit]
|
|
29
|
+
if (key.matches(msg, 'enter')) return [this, this._submit()]
|
|
30
|
+
// Then let the ring move focus and route the rest to the focused child,
|
|
31
|
+
// threading its Cmd back up.
|
|
32
|
+
const [ring, cmd] = this.ring.update(msg)
|
|
33
|
+
this.ring = ring
|
|
34
|
+
return [this, cmd]
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
view() {
|
|
38
|
+
return this.ring.items.map((it) => it.view()).join('\n')
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Options
|
|
43
|
+
|
|
44
|
+
| Option | Default | Description |
|
|
45
|
+
| ------- | ------------------- | -------------------------------------- |
|
|
46
|
+
| `items` | `[]` | Ordered focusable children |
|
|
47
|
+
| `index` | `0` | Which child starts focused |
|
|
48
|
+
| `keys` | `tab` / `shift+tab` | Navigation bindings (`{ next, prev }`) |
|
|
49
|
+
|
|
50
|
+
On construction the ring syncs the children so exactly the indexed one is
|
|
51
|
+
focused.
|
|
52
|
+
|
|
53
|
+
## API
|
|
54
|
+
|
|
55
|
+
- `focused()` — the active child, or `null`. `.index` / `.items` are readable.
|
|
56
|
+
- `next()` / `prev()` / `focus(i)` — move focus imperatively (wraps).
|
|
57
|
+
- `setItems(items)` — replace the children and re-sync focus.
|
|
58
|
+
- `update(msg)` — handles navigation, then delegates everything else to the
|
|
59
|
+
focused child and threads its Cmd up.
|
|
60
|
+
|
|
61
|
+
## Keys
|
|
62
|
+
|
|
63
|
+
Navigation defaults to `tab`/`shift+tab` **only** — deliberately not the arrows,
|
|
64
|
+
because the focusable children ([radio](radio.md), [select](select.md),
|
|
65
|
+
[list](list.md), [textarea](textarea.md)) use the arrows internally. Pass `keys`
|
|
66
|
+
to override if your children don't. Always handle global and submit keys in the
|
|
67
|
+
parent _before_ calling `ring.update`, so a focused child can't swallow the
|
|
68
|
+
escape hatch.
|
package/docs/help.md
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# help
|
|
2
|
+
|
|
3
|
+
Renders keybinding hints from `key.binding({ keys, help })` objects. It's a
|
|
4
|
+
_view helper_, not a loop model: `view(keymap)` is called from your own `view`.
|
|
5
|
+
Bindings without a `help` entry are skipped, so internal keys stay hidden.
|
|
6
|
+
|
|
7
|
+
[← all components](../README.md#components)
|
|
8
|
+
|
|
9
|
+
## Usage
|
|
10
|
+
|
|
11
|
+
```js
|
|
12
|
+
const { help, list } = require('bare-tui')
|
|
13
|
+
|
|
14
|
+
const h = help.create()
|
|
15
|
+
|
|
16
|
+
// short (one line) — pass any component's exported keymap:
|
|
17
|
+
h.view(list.keys)
|
|
18
|
+
|
|
19
|
+
// full (aligned columns) — toggle with '?', say:
|
|
20
|
+
h.showAll = true
|
|
21
|
+
h.view([[keys.up, keys.down], [keys.quit]])
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
A keymap can be:
|
|
25
|
+
|
|
26
|
+
- an **array** of bindings (short) or array-of-arrays (full columns),
|
|
27
|
+
- an **object** of bindings (its values are used), or
|
|
28
|
+
- an object exposing **`shortHelp()`** / **`fullHelp()`**.
|
|
29
|
+
|
|
30
|
+
## Options
|
|
31
|
+
|
|
32
|
+
| Option | Default | Description |
|
|
33
|
+
| ----------- | ------- | -------------------------------------------------- |
|
|
34
|
+
| `showAll` | `false` | Full multi-column help vs the one-line short form |
|
|
35
|
+
| `width` | `0` | Truncate the short line to this width (`0` = none) |
|
|
36
|
+
| `separator` | `' • '` | Between items in short mode |
|
|
37
|
+
| `styles` | — | `{ key, desc, sep }` functions to restyle parts |
|
|
38
|
+
|
|
39
|
+
## API
|
|
40
|
+
|
|
41
|
+
- `view(keymap)` → the rendered hint string.
|
|
42
|
+
- `setWidth(n)`.
|
|
43
|
+
|
|
44
|
+
Every component that owns keys exports a `keys` keymap (`list.keys`,
|
|
45
|
+
`table.keys`, `viewport.keys`, `paginator.keys`) ready to pass straight in.
|
package/docs/list.md
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# list
|
|
2
|
+
|
|
3
|
+
A selectable, filterable list. Owns selection and a scroll window that follows
|
|
4
|
+
the cursor; filtering is delegated to an embedded [textinput](textinput.md).
|
|
5
|
+
|
|
6
|
+
[← all components](../README.md#components)
|
|
7
|
+
|
|
8
|
+
## Usage
|
|
9
|
+
|
|
10
|
+
```js
|
|
11
|
+
const { list } = require('bare-tui')
|
|
12
|
+
|
|
13
|
+
const l = list.create({
|
|
14
|
+
items: ['apple', 'banana', 'cherry'],
|
|
15
|
+
height: 8,
|
|
16
|
+
title: ' fruit'
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
// in update: const [m, cmd] = l.update(msg); this.list = m
|
|
20
|
+
// pick: l.selectedItem()
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Items can be strings or objects: `title` is shown, `filterValue` (falling back
|
|
24
|
+
to `title`) is matched.
|
|
25
|
+
|
|
26
|
+
## Options
|
|
27
|
+
|
|
28
|
+
| Option | Default | Description |
|
|
29
|
+
| ------------ | ------- | ------------------------------------------- |
|
|
30
|
+
| `items` | `[]` | Strings or `{ title, filterValue }` objects |
|
|
31
|
+
| `height` | `10` | Visible rows |
|
|
32
|
+
| `width` | `0` | Pad rows to this width (`0` = ragged) |
|
|
33
|
+
| `title` | `''` | Optional heading line |
|
|
34
|
+
| `filterable` | `true` | Allow `/` filtering |
|
|
35
|
+
|
|
36
|
+
## API
|
|
37
|
+
|
|
38
|
+
- `selectedItem()` — the underlying item under the cursor (or `null`).
|
|
39
|
+
- `setItems(items)`.
|
|
40
|
+
- `.filtering` — whether the filter input is active. `.visibleCount`,
|
|
41
|
+
`.selected`, `.filter`.
|
|
42
|
+
|
|
43
|
+
## Keys
|
|
44
|
+
|
|
45
|
+
`↑`/`↓` (`k`/`j`) move, `pgup`/`pgdn` page, `/` enters filter mode (type to
|
|
46
|
+
narrow, `enter` keeps it, `esc` clears it). Bindings are exported as `list.keys`
|
|
47
|
+
for the [help](help.md) component.
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# paginator
|
|
2
|
+
|
|
3
|
+
Page state plus an indicator, for paging through a long collection. Holds the
|
|
4
|
+
current page and page size, handles the paging keys, and offers `sliceBounds()`
|
|
5
|
+
so you can carve the visible page out of your items.
|
|
6
|
+
|
|
7
|
+
[← all components](../README.md#components)
|
|
8
|
+
|
|
9
|
+
## Usage
|
|
10
|
+
|
|
11
|
+
```js
|
|
12
|
+
const { paginator } = require('bare-tui')
|
|
13
|
+
|
|
14
|
+
const p = paginator.create({ perPage: 10, total: items.length, type: 'dots' })
|
|
15
|
+
|
|
16
|
+
// in update: const [m] = p.update(msg); this.pager = m
|
|
17
|
+
// in view:
|
|
18
|
+
const [start, end] = p.sliceBounds()
|
|
19
|
+
render(items.slice(start, end))
|
|
20
|
+
p.view() // "●○○○○" (dots) or "1/5" (arabic)
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Options
|
|
24
|
+
|
|
25
|
+
| Option | Default | Description |
|
|
26
|
+
| --------------------------- | ------------- | -------------------------------------- |
|
|
27
|
+
| `perPage` | `10` | Items per page |
|
|
28
|
+
| `total` | `0` | Total item count |
|
|
29
|
+
| `page` | `0` | Initial page (zero-indexed) |
|
|
30
|
+
| `type` | `'arabic'` | `'arabic'` (`1/5`) or `'dots'` (`●○○`) |
|
|
31
|
+
| `activeDot` / `inactiveDot` | `'●'` / `'○'` | Dot characters |
|
|
32
|
+
|
|
33
|
+
## API
|
|
34
|
+
|
|
35
|
+
- `sliceBounds(length?)` → `[start, end)` for the current page.
|
|
36
|
+
- `itemsOnPage(length?)` — count on the current page.
|
|
37
|
+
- `nextPage()` / `prevPage()` / `setPage(n)` / `setTotal(n)`.
|
|
38
|
+
- `.page` / `.totalPages` / `onFirstPage()` / `onLastPage()`.
|
|
39
|
+
|
|
40
|
+
## Keys
|
|
41
|
+
|
|
42
|
+
`←`/`→` (`h`/`l`, `pgup`/`pgdn`) change pages. Bindings are exported as
|
|
43
|
+
`paginator.keys` for the [help](help.md) component.
|
package/docs/progress.md
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# progress
|
|
2
|
+
|
|
3
|
+
A percentage bar. Static like [help](help.md): it holds the look and
|
|
4
|
+
`view(percent)` renders a bar at the given fraction. Drive the percent from your
|
|
5
|
+
model (a tick, a download callback, the OTA updater).
|
|
6
|
+
|
|
7
|
+
[← all components](../README.md#components)
|
|
8
|
+
|
|
9
|
+
## Usage
|
|
10
|
+
|
|
11
|
+
```js
|
|
12
|
+
const { progress } = require('bare-tui')
|
|
13
|
+
|
|
14
|
+
const bar = progress.create({ width: 40, gradient: ['#5A56E0', '#EE6FF8'] })
|
|
15
|
+
|
|
16
|
+
// in view:
|
|
17
|
+
bar.view(0.42) // "████████████░░░░… 42%"
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Options
|
|
21
|
+
|
|
22
|
+
| Option | Default | Description |
|
|
23
|
+
| ---------------- | ------- | ----------------------------------------------- |
|
|
24
|
+
| `width` | `40` | Total width including the percentage label |
|
|
25
|
+
| `full` | `'█'` | Filled cell character |
|
|
26
|
+
| `empty` | `'░'` | Empty cell character |
|
|
27
|
+
| `showPercentage` | `true` | Append a ` NNN%` label (reserves 5 cells) |
|
|
28
|
+
| `color` | `null` | Solid fill color (name, 0–255, or `#hex`) |
|
|
29
|
+
| `gradient` | `null` | `[fromHex, toHex]` interpolated across the fill |
|
|
30
|
+
|
|
31
|
+
## API
|
|
32
|
+
|
|
33
|
+
- `view(percent)` → bar string. `percent` is `0..1` and is clamped.
|
|
34
|
+
- `setWidth(n)` — e.g. on a resize message.
|
|
35
|
+
|
|
36
|
+
The label slot is a fixed width, so the bar doesn't jump as the number changes.
|
package/docs/radio.md
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# radio
|
|
2
|
+
|
|
3
|
+
Single choice from a fixed set of options. A tiny vertical list where the cursor
|
|
4
|
+
_is_ the value — the arrows move the selection directly, so there is no separate
|
|
5
|
+
highlight-then-commit step.
|
|
6
|
+
|
|
7
|
+
[← all components](../README.md#components)
|
|
8
|
+
|
|
9
|
+
## Usage
|
|
10
|
+
|
|
11
|
+
```js
|
|
12
|
+
const { radio } = require('bare-tui')
|
|
13
|
+
|
|
14
|
+
const size = radio
|
|
15
|
+
.create({
|
|
16
|
+
options: ['small', 'medium', 'large'],
|
|
17
|
+
selected: 1
|
|
18
|
+
})
|
|
19
|
+
.focus()
|
|
20
|
+
|
|
21
|
+
// in update: const [r] = size.update(msg); this.size = r
|
|
22
|
+
// in view: size.view()
|
|
23
|
+
size.value() // 'medium'
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Options may be strings or `{ label, value }` objects; `value()` returns the
|
|
27
|
+
underlying value (the label when none is given).
|
|
28
|
+
|
|
29
|
+
## Options
|
|
30
|
+
|
|
31
|
+
| Option | Default | Description |
|
|
32
|
+
| -------------------- | ------------- | ---------------------------- |
|
|
33
|
+
| `options` | `[]` | Strings or `{ label, value}` |
|
|
34
|
+
| `selected` | `0` | Initial index |
|
|
35
|
+
| `focused` | `false` | Start focused |
|
|
36
|
+
| `onGlyph`/`offGlyph` | `(•)` / `( )` | Bullet characters |
|
|
37
|
+
|
|
38
|
+
## API
|
|
39
|
+
|
|
40
|
+
- `value()` — the chosen value, or `null` when there are no options.
|
|
41
|
+
- `selectedOption()` — the chosen `{ label, value }`.
|
|
42
|
+
- `setValue(v)` — select by value (no-op if absent). `setOptions(opts)`.
|
|
43
|
+
|
|
44
|
+
## Keys
|
|
45
|
+
|
|
46
|
+
`↑`/`↓` (`k`/`j`) move the selection. **`enter` is not consumed**, so a parent
|
|
47
|
+
keeps it for "submit". The chosen option always shows a filled bullet (even when
|
|
48
|
+
blurred); a leading `›` marks the focused row. Bindings are exported as
|
|
49
|
+
`radio.keys` for the [help](help.md) component.
|
package/docs/select.md
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# select
|
|
2
|
+
|
|
3
|
+
A compact dropdown over a fixed list of options. Where
|
|
4
|
+
[autocomplete](autocomplete.md) is for _open_ typing with a filtered menu,
|
|
5
|
+
select is for a _closed_ set you pick from: it shows one line (the current
|
|
6
|
+
choice) until you open it, then a menu to choose from.
|
|
7
|
+
|
|
8
|
+
[← all components](../README.md#components)
|
|
9
|
+
|
|
10
|
+
## Usage
|
|
11
|
+
|
|
12
|
+
Like autocomplete, rendering is split in two so the dropdown never reflows the
|
|
13
|
+
layout — `view()` is the one-line control, `menuView()` is the overlay:
|
|
14
|
+
|
|
15
|
+
```js
|
|
16
|
+
const { select } = require('bare-tui')
|
|
17
|
+
|
|
18
|
+
const fruit = select
|
|
19
|
+
.create({
|
|
20
|
+
options: ['apple', 'banana', 'cherry'],
|
|
21
|
+
placeholder: 'pick one'
|
|
22
|
+
})
|
|
23
|
+
.focus()
|
|
24
|
+
|
|
25
|
+
// in view: draw the control, then overlay the menu where it fits
|
|
26
|
+
const line = fruit.view() // always one line, stable width
|
|
27
|
+
const menu = fruit.menuView() // '' when closed; rows when open
|
|
28
|
+
fruit.value() // 'apple', or null while nothing is chosen
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Options
|
|
32
|
+
|
|
33
|
+
| Option | Default | Description |
|
|
34
|
+
| ------------- | ----------- | --------------------------------- |
|
|
35
|
+
| `options` | `[]` | Strings or `{ label, value }` |
|
|
36
|
+
| `selected` | `-1` | Initial index (`-1` = none) |
|
|
37
|
+
| `placeholder` | `'select…'` | Shown when nothing is chosen |
|
|
38
|
+
| `maxVisible` | `6` | Rows before the menu scrolls |
|
|
39
|
+
| `openGlyph` | `'▾'` | Trailing indicator on the control |
|
|
40
|
+
|
|
41
|
+
## API
|
|
42
|
+
|
|
43
|
+
- `value()` — the committed value, or `null`. `selectedOption()` → `{label,value}`.
|
|
44
|
+
- `setValue(v)` / `setOptions(opts)`.
|
|
45
|
+
- `view()` — the closed control line. `menuView()` — the dropdown, or `''`.
|
|
46
|
+
- `.open` — whether the menu is showing. `blur()` also closes it.
|
|
47
|
+
|
|
48
|
+
## Keys
|
|
49
|
+
|
|
50
|
+
The contract mirrors the other field controls. While **closed** it consumes only
|
|
51
|
+
`space` (to open) and never `enter`, so a parent form keeps `enter` for "submit".
|
|
52
|
+
While **open** it owns the menu — `↑`/`↓` move, `enter`/`space` commit, `esc`
|
|
53
|
+
cancels — which is fine because a form won't submit with a menu open. Bindings
|
|
54
|
+
are exported as `select.keys` for the [help](help.md) component.
|
package/docs/spinner.md
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# spinner
|
|
2
|
+
|
|
3
|
+
An animated loading indicator. Command-driven: it animates by re-issuing a tick
|
|
4
|
+
command each frame.
|
|
5
|
+
|
|
6
|
+
[← all components](../README.md#components)
|
|
7
|
+
|
|
8
|
+
## Usage
|
|
9
|
+
|
|
10
|
+
```js
|
|
11
|
+
const { spinner } = require('bare-tui')
|
|
12
|
+
|
|
13
|
+
class App {
|
|
14
|
+
constructor() {
|
|
15
|
+
this.spinner = spinner.create({ fps: 12 })
|
|
16
|
+
}
|
|
17
|
+
init() {
|
|
18
|
+
return this.spinner.init() // start animating
|
|
19
|
+
}
|
|
20
|
+
update(msg) {
|
|
21
|
+
if (msg.type === 'spinner.tick') {
|
|
22
|
+
const [s, cmd] = this.spinner.update(msg)
|
|
23
|
+
this.spinner = s
|
|
24
|
+
return [this, cmd]
|
|
25
|
+
}
|
|
26
|
+
return [this, null]
|
|
27
|
+
}
|
|
28
|
+
view() {
|
|
29
|
+
return `${this.spinner.view()} loading…`
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Options
|
|
35
|
+
|
|
36
|
+
| Option | Default | Description |
|
|
37
|
+
| -------- | -------------- | ------------------------------- |
|
|
38
|
+
| `frames` | `spinner.dots` | Array of frame strings to cycle |
|
|
39
|
+
| `fps` | `10` | Frames per second |
|
|
40
|
+
|
|
41
|
+
Frame presets: `spinner.dots`, `spinner.line`, `spinner.points`.
|
|
42
|
+
|
|
43
|
+
## API
|
|
44
|
+
|
|
45
|
+
- `init()` → a command that starts the animation. Call from your `init`.
|
|
46
|
+
- `update(msg)` → advances on its own `spinner.tick`, returns the next tick
|
|
47
|
+
command.
|
|
48
|
+
- `view()` → the current frame string.
|
|
49
|
+
|
|
50
|
+
## Messages
|
|
51
|
+
|
|
52
|
+
Emits and consumes `{ type: 'spinner.tick', id, tag }`. The `id` + `tag` ensure a
|
|
53
|
+
second spinner's ticks (or a duplicate) can't double-drive this one — route every
|
|
54
|
+
`spinner.tick` to `update` and it sorts itself out.
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# stopwatch
|
|
2
|
+
|
|
3
|
+
Counts elapsed time upward. Command-driven like the [spinner](spinner.md):
|
|
4
|
+
`start()` returns a tick command, and each accepted tick advances `elapsed` and
|
|
5
|
+
re-issues the next.
|
|
6
|
+
|
|
7
|
+
[← all components](../README.md#components)
|
|
8
|
+
|
|
9
|
+
## Usage
|
|
10
|
+
|
|
11
|
+
```js
|
|
12
|
+
const { stopwatch } = require('bare-tui')
|
|
13
|
+
|
|
14
|
+
class App {
|
|
15
|
+
constructor() {
|
|
16
|
+
this.sw = stopwatch.create()
|
|
17
|
+
}
|
|
18
|
+
init() {
|
|
19
|
+
return this.sw.start()
|
|
20
|
+
}
|
|
21
|
+
update(msg) {
|
|
22
|
+
if (msg.type === 'stopwatch.tick') {
|
|
23
|
+
const [sw, cmd] = this.sw.update(msg)
|
|
24
|
+
this.sw = sw
|
|
25
|
+
return [this, cmd]
|
|
26
|
+
}
|
|
27
|
+
return [this, null]
|
|
28
|
+
}
|
|
29
|
+
view() {
|
|
30
|
+
return this.sw.view() // "01:23"
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Options
|
|
36
|
+
|
|
37
|
+
| Option | Default | Description |
|
|
38
|
+
| ---------- | ------- | ---------------------------- |
|
|
39
|
+
| `interval` | `1000` | Milliseconds per tick |
|
|
40
|
+
| `elapsed` | `0` | Initial elapsed milliseconds |
|
|
41
|
+
|
|
42
|
+
## API
|
|
43
|
+
|
|
44
|
+
- `start()` / `stop()` / `toggle()` — return a command (or `null`); thread it up.
|
|
45
|
+
- `reset()` — zero `elapsed`; a running stopwatch keeps ticking.
|
|
46
|
+
- `.elapsed` (ms) / `.running`.
|
|
47
|
+
- `view()` → `MM:SS` (or `H:MM:SS`).
|
|
48
|
+
|
|
49
|
+
## Messages
|
|
50
|
+
|
|
51
|
+
Emits and consumes `{ type: 'stopwatch.tick', id, tag }`; id + tag guard against
|
|
52
|
+
strays, so pause/resume can't double-drive it.
|
package/docs/table.md
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# table
|
|
2
|
+
|
|
3
|
+
Fixed-width columns with selectable, scrolling rows. Cells are truncated/padded
|
|
4
|
+
to their column width (ANSI-aware), the selection is a reverse-video bar, and the
|
|
5
|
+
body scrolls in a fixed-height window.
|
|
6
|
+
|
|
7
|
+
[← all components](../README.md#components)
|
|
8
|
+
|
|
9
|
+
## Usage
|
|
10
|
+
|
|
11
|
+
```js
|
|
12
|
+
const { table } = require('bare-tui')
|
|
13
|
+
|
|
14
|
+
const t = table.create({
|
|
15
|
+
columns: [
|
|
16
|
+
{ title: 'Package', width: 18 },
|
|
17
|
+
{ title: 'Lang', width: 5 }
|
|
18
|
+
],
|
|
19
|
+
rows: [
|
|
20
|
+
['corestore', 'js'],
|
|
21
|
+
['hypercore', 'js']
|
|
22
|
+
],
|
|
23
|
+
height: 8
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
// in update: const [m] = t.update(msg); this.table = m
|
|
27
|
+
// selected: t.selectedRow()
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Options
|
|
31
|
+
|
|
32
|
+
| Option | Default | Description |
|
|
33
|
+
| --------- | ------- | ------------------------------------- |
|
|
34
|
+
| `columns` | `[]` | `{ title, width }` per column |
|
|
35
|
+
| `rows` | `[]` | Arrays of cell values, one per column |
|
|
36
|
+
| `height` | `10` | Visible body rows |
|
|
37
|
+
| `rule` | `'─'` | Character for the header underline |
|
|
38
|
+
|
|
39
|
+
## API
|
|
40
|
+
|
|
41
|
+
- `selectedRow()` — the row array under the cursor (or `null`).
|
|
42
|
+
- `setRows(rows)` / `setColumns(columns)`.
|
|
43
|
+
- `gotoTop()` / `gotoBottom()`.
|
|
44
|
+
- `.cursor` / `.offset` / `.totalWidth`.
|
|
45
|
+
|
|
46
|
+
Renders a stable `2 + height`-row block (header, rule, body), so it sits cleanly
|
|
47
|
+
inside a `style` box.
|
|
48
|
+
|
|
49
|
+
## Keys
|
|
50
|
+
|
|
51
|
+
`↑`/`↓` (`k`/`j`) move, `pgup`/`pgdn` page, `home`/`end` jump. Bindings are
|
|
52
|
+
exported as `table.keys` for the [help](help.md) component.
|
package/docs/textarea.md
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# textarea
|
|
2
|
+
|
|
3
|
+
A multi-line text editor. Soft-wraps to its width, scrolls a fixed-height
|
|
4
|
+
window, and moves the cursor by _visual_ row (so up/down feel right inside
|
|
5
|
+
wrapped text). Input-driven and focus-gated, like [textinput](textinput.md).
|
|
6
|
+
|
|
7
|
+
[← all components](../README.md#components)
|
|
8
|
+
|
|
9
|
+
## Usage
|
|
10
|
+
|
|
11
|
+
```js
|
|
12
|
+
const { textarea } = require('bare-tui')
|
|
13
|
+
|
|
14
|
+
const ta = textarea.create({ width: 60, height: 10, placeholder: 'Type…' }).focus()
|
|
15
|
+
|
|
16
|
+
// in update: const [t, cmd] = ta.update(msg); this.ta = t
|
|
17
|
+
// in view: this.ta.view()
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Options
|
|
21
|
+
|
|
22
|
+
| Option | Default | Description |
|
|
23
|
+
| ------------- | ------- | ----------------------------------------------- |
|
|
24
|
+
| `value` | `''` | Initial contents |
|
|
25
|
+
| `width` | `40` | Wrap width / render width |
|
|
26
|
+
| `height` | `6` | Visible rows |
|
|
27
|
+
| `placeholder` | `''` | Dim text shown when empty |
|
|
28
|
+
| `charLimit` | `0` | Max characters incl. newlines (`0` = unlimited) |
|
|
29
|
+
| `focused` | `false` | Start focused |
|
|
30
|
+
|
|
31
|
+
## API
|
|
32
|
+
|
|
33
|
+
- `focus()` / `blur()`.
|
|
34
|
+
- `setValue(v)` / `reset()`.
|
|
35
|
+
- `setSize(width, height)` — e.g. on a resize message.
|
|
36
|
+
- `.value` — the text (lines joined by `\n`). `.length` — character count.
|
|
37
|
+
|
|
38
|
+
## Keys
|
|
39
|
+
|
|
40
|
+
`←`/`→` move by character (wrapping across lines), `↑`/`↓` by visual row,
|
|
41
|
+
`home`/`end` to the visual row's edges, `enter` splits the line, `backspace`/
|
|
42
|
+
`delete` edit and merge lines, printable characters insert. Renders a
|
|
43
|
+
rectangular `width × height` block, so it drops straight into a `style` box.
|