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.
Files changed (48) hide show
  1. package/LICENSE +201 -0
  2. package/NOTICE +13 -0
  3. package/README.md +261 -0
  4. package/ansi.js +34 -0
  5. package/commands.js +67 -0
  6. package/components/autocomplete.js +208 -0
  7. package/components/checkbox.js +68 -0
  8. package/components/filepicker.js +232 -0
  9. package/components/focus.js +129 -0
  10. package/components/help.js +117 -0
  11. package/components/list.js +172 -0
  12. package/components/paginator.js +111 -0
  13. package/components/progress.js +78 -0
  14. package/components/radio.js +113 -0
  15. package/components/select.js +165 -0
  16. package/components/spinner.js +70 -0
  17. package/components/stopwatch.js +88 -0
  18. package/components/table.js +130 -0
  19. package/components/textarea.js +279 -0
  20. package/components/textinput.js +135 -0
  21. package/components/timer.js +91 -0
  22. package/components/viewport.js +104 -0
  23. package/docs/autocomplete.md +80 -0
  24. package/docs/checkbox.md +43 -0
  25. package/docs/filepicker.md +80 -0
  26. package/docs/focus.md +68 -0
  27. package/docs/help.md +45 -0
  28. package/docs/list.md +47 -0
  29. package/docs/paginator.md +43 -0
  30. package/docs/progress.md +36 -0
  31. package/docs/radio.md +49 -0
  32. package/docs/select.md +54 -0
  33. package/docs/spinner.md +54 -0
  34. package/docs/stopwatch.md +52 -0
  35. package/docs/table.md +52 -0
  36. package/docs/textarea.md +43 -0
  37. package/docs/textinput.md +51 -0
  38. package/docs/timer.md +57 -0
  39. package/docs/viewport.md +39 -0
  40. package/index.d.ts +1 -0
  41. package/index.js +71 -0
  42. package/key.js +29 -0
  43. package/messages.js +62 -0
  44. package/mouse.js +102 -0
  45. package/package.json +59 -1
  46. package/program.js +387 -0
  47. package/renderer.js +67 -0
  48. package/style.js +473 -0
@@ -0,0 +1,51 @@
1
+ # textinput
2
+
3
+ A single-line editable text field. Input-driven: no commands, just state folded
4
+ from key messages. Only consumes keys while focused, so a parent can host several
5
+ and route to whichever has focus.
6
+
7
+ [← all components](../README.md#components)
8
+
9
+ ## Usage
10
+
11
+ ```js
12
+ const { textinput } = require('bare-tui')
13
+
14
+ class Form {
15
+ constructor() {
16
+ this.name = textinput.create({ placeholder: 'name', prompt: '> ' }).focus()
17
+ }
18
+ update(msg) {
19
+ const [f, cmd] = this.name.update(msg)
20
+ this.name = f
21
+ return [this, cmd]
22
+ }
23
+ view() {
24
+ return this.name.view()
25
+ }
26
+ }
27
+ ```
28
+
29
+ ## Options
30
+
31
+ | Option | Default | Description |
32
+ | ------------- | ---------- | ------------------------------------------- |
33
+ | `value` | `''` | Initial text |
34
+ | `placeholder` | `''` | Dim text shown when empty |
35
+ | `prompt` | `''` | Prefix drawn before the value (e.g. `'> '`) |
36
+ | `charLimit` | `0` | Max length (`0` = unlimited) |
37
+ | `echoMode` | `'normal'` | `'password'` masks the value |
38
+ | `maskChar` | `'•'` | Mask character in password mode |
39
+ | `focused` | `false` | Start focused |
40
+
41
+ ## API
42
+
43
+ - `focus()` / `blur()` — toggle whether keys are consumed.
44
+ - `setValue(v)` / `reset()` — set or clear the text.
45
+ - `.value` — the current string.
46
+
47
+ ## Keys
48
+
49
+ `←`/`→` move, `home`/`end` (and `ctrl+a`/`ctrl+e`) jump, `backspace`/`delete`
50
+ edit, printable characters insert at the cursor. The field draws its own
51
+ reverse-video cursor (the Program hides the real one).
package/docs/timer.md ADDED
@@ -0,0 +1,57 @@
1
+ # timer
2
+
3
+ Counts a duration down to zero. Command-driven like the
4
+ [stopwatch](stopwatch.md); when it reaches zero it stops and emits a one-shot
5
+ timeout message.
6
+
7
+ [← all components](../README.md#components)
8
+
9
+ ## Usage
10
+
11
+ ```js
12
+ const { timer } = require('bare-tui')
13
+
14
+ class App {
15
+ constructor() {
16
+ this.timer = timer.create({ timeout: 10000 }) // 10s
17
+ }
18
+ init() {
19
+ return this.timer.start()
20
+ }
21
+ update(msg) {
22
+ if (msg.type === 'timer.timeout') {
23
+ /* done */ return [this, null]
24
+ }
25
+ if (msg.type === 'timer.tick') {
26
+ const [t, cmd] = this.timer.update(msg)
27
+ this.timer = t
28
+ return [this, cmd]
29
+ }
30
+ return [this, null]
31
+ }
32
+ view() {
33
+ return this.timer.view() // "00:09"
34
+ }
35
+ }
36
+ ```
37
+
38
+ ## Options
39
+
40
+ | Option | Default | Description |
41
+ | ---------- | ------- | -------------------------------- |
42
+ | `timeout` | `0` | Initial duration in milliseconds |
43
+ | `interval` | `1000` | Milliseconds per tick |
44
+
45
+ ## API
46
+
47
+ - `start()` / `stop()` / `toggle()` — return a command (or `null`). Won't start
48
+ from zero.
49
+ - `reset()` — restore the initial duration.
50
+ - `.timeout` (ms remaining) / `.running` / `.timedOut`.
51
+ - `view()` → `MM:SS`.
52
+
53
+ ## Messages
54
+
55
+ Consumes `{ type: 'timer.tick', id, tag }`; on reaching zero its `update` returns
56
+ a command that emits `{ type: 'timer.timeout', id }` — handle that to react to
57
+ the timeout.
@@ -0,0 +1,39 @@
1
+ # viewport
2
+
3
+ A scrollable window over content taller than the available space. Renders a
4
+ fixed `height`-row window and always emits exactly that many lines (padding short
5
+ content), so surrounding layout stays stable.
6
+
7
+ [← all components](../README.md#components)
8
+
9
+ ## Usage
10
+
11
+ ```js
12
+ const { viewport } = require('bare-tui')
13
+
14
+ const vp = viewport.create({ width: 40, height: 10 })
15
+ vp.setContent(longText)
16
+
17
+ // in update: const [v] = vp.update(msg); this.vp = v
18
+ // in view: this.vp.view()
19
+ ```
20
+
21
+ ## Options
22
+
23
+ | Option | Default | Description |
24
+ | -------- | ------- | -------------------------------------------------- |
25
+ | `width` | `0` | Truncate lines to this width (`0` = no truncation) |
26
+ | `height` | `0` | Visible rows |
27
+
28
+ ## API
29
+
30
+ - `setContent(string)` — set the scrollable text.
31
+ - `scrollUp(n)` / `scrollDown(n)` / `gotoTop()` / `gotoBottom()` /
32
+ `setYOffset(n)`.
33
+ - `.atTop` / `.atBottom` / `.scrollPercent` / `.maxOffset` / `.yOffset`.
34
+
35
+ ## Keys
36
+
37
+ `↑`/`↓` (`k`/`j`) by line, `pgup`/`pgdn` (`b`/`f`) by page, `ctrl+u`/`ctrl+d`
38
+ half-page, `home`/`end` to the ends. Bindings are exported as `viewport.keys`
39
+ for the [help](help.md) component.
package/index.d.ts ADDED
@@ -0,0 +1 @@
1
+ declare module '@holepunchto/bare-tui'
package/index.js ADDED
@@ -0,0 +1,71 @@
1
+ // tea — a tiny Elm Architecture runtime for Bare terminals.
2
+ //
3
+ // Built on Bare's native primitives (bare-tty for raw IO + window size,
4
+ // bare-ansi-escapes for key decoding and escape sequences), so it runs
5
+ // anywhere Bare runs and pulls in no Node-only dependencies. The API is shaped
6
+ // after Charm's Bubble Tea (Model/Cmd/Msg/Program) with the intent of growing
7
+ // into a drop-out framework.
8
+ //
9
+ // const { Program, quit } = require('./lib/tea')
10
+ //
11
+ // class App {
12
+ // init() { return null }
13
+ // update(msg) {
14
+ // if (msg.type === 'key' && String(msg) === 'q') return [this, quit]
15
+ // return [this, null]
16
+ // }
17
+ // view() { return 'press q to quit' }
18
+ // }
19
+ //
20
+ // new Program(new App()).run()
21
+ const Program = require('./program')
22
+ const commands = require('./commands')
23
+ const messages = require('./messages')
24
+ const key = require('./key')
25
+ const ansi = require('./ansi')
26
+ const { style } = require('./style')
27
+ const spinner = require('./components/spinner')
28
+ const textinput = require('./components/textinput')
29
+ const autocomplete = require('./components/autocomplete')
30
+ const textarea = require('./components/textarea')
31
+ const viewport = require('./components/viewport')
32
+ const list = require('./components/list')
33
+ const table = require('./components/table')
34
+ const help = require('./components/help')
35
+ const progress = require('./components/progress')
36
+ const paginator = require('./components/paginator')
37
+ const stopwatch = require('./components/stopwatch')
38
+ const timer = require('./components/timer')
39
+ const filepicker = require('./components/filepicker')
40
+ const checkbox = require('./components/checkbox')
41
+ const radio = require('./components/radio')
42
+ const select = require('./components/select')
43
+ const focus = require('./components/focus')
44
+
45
+ module.exports = {
46
+ Program,
47
+ ...commands, // quit, batch, sequence, tick, every, suspend
48
+ KeyMsg: messages.KeyMsg,
49
+ key, // key.matches(msg, ...chords | bindings), key.binding({ keys, help })
50
+ ansi,
51
+ style, // style().bold().border(style.borders.rounded).render(...) + style.joinHorizontal/Vertical
52
+
53
+ // Components — each a composable { init?, update, view } model.
54
+ spinner, // spinner.create({ frames, fps })
55
+ textinput, // textinput.create({ placeholder, prompt, charLimit, echoMode })
56
+ autocomplete, // autocomplete.create({ prompt, placeholder, suggestions, trigger }) — input + suggestion menu
57
+ textarea, // textarea.create({ width, height, placeholder, charLimit }) — multi-line
58
+ viewport, // viewport.create({ width, height }) — scrollable window
59
+ list, // list.create({ items, height, width, title }) — selectable + filterable
60
+ table, // table.create({ columns, rows, height }) — selectable scrolling rows
61
+ help, // help.create() — renders keybinding hints; view(keymap)
62
+ progress, // progress.create({ width, gradient }) — view(percent)
63
+ paginator, // paginator.create({ perPage, total, type }) — page state + indicator
64
+ stopwatch, // stopwatch.create({ interval }) — counts up; start/stop/toggle
65
+ timer, // timer.create({ timeout, interval }) — counts down; emits timer.timeout
66
+ filepicker, // filepicker.create({ fs, path, cwd }) — browse + pick; filepicker.mock(tree)
67
+ checkbox, // checkbox.create({ label, checked }) — boolean toggle (space)
68
+ radio, // radio.create({ options, selected }) — single choice; value()
69
+ select, // select.create({ options, placeholder }) — dropdown; view() + menuView()
70
+ focus // focus.create({ items }) — ordered focus ring across child components
71
+ }
package/key.js ADDED
@@ -0,0 +1,29 @@
1
+ // Key matching helpers — the ergonomic front door to KeyMsg in update().
2
+ //
3
+ // const { key } = require('./lib/tea')
4
+ // if (key.matches(msg, 'q', 'ctrl+c')) return [model, quit]
5
+ //
6
+ // matches() is null/type-safe: it returns false for any non-key Msg, so you
7
+ // don't have to guard `msg.type === 'key'` first.
8
+
9
+ // A reusable named binding: the chords that trigger an action plus optional
10
+ // help text. Shaped for a future help component, but useful now as a single
11
+ // place to define an action's keys.
12
+ // const up = key.binding({ keys: ['up', 'k'], help: { key: '↑/k', desc: 'up' } })
13
+ // if (key.matches(msg, up)) ...
14
+ function binding({ keys = [], help = null } = {}) {
15
+ return { keys: [].concat(keys), help }
16
+ }
17
+
18
+ // True if `msg` is a key matching any of the given chords or bindings. Bindings
19
+ // are expanded to their `keys`, so you can mix:
20
+ // key.matches(msg, 'enter', someBinding, 'ctrl+c')
21
+ function matches(msg, ...items) {
22
+ if (!msg || msg.type !== 'key' || typeof msg.is !== 'function') return false
23
+ const chords = items.flatMap((item) =>
24
+ item && typeof item === 'object' && Array.isArray(item.keys) ? item.keys : [item]
25
+ )
26
+ return msg.is(...chords)
27
+ }
28
+
29
+ module.exports = { matches, binding }
package/messages.js ADDED
@@ -0,0 +1,62 @@
1
+ // Messages (Msg) are the only thing that flows into a model's update().
2
+ //
3
+ // A Msg is just a tagged plain object — authors can define their own. These are
4
+ // the ones the runtime itself produces. We keep the shapes small and stable so
5
+ // they read the same as Bubble Tea's KeyMsg / WindowSizeMsg / QuitMsg.
6
+
7
+ // KeyMsg wraps a decoded key from bare-ansi-escapes' KeyDecoder. The raw fields
8
+ // (name, ctrl, meta, shift, sequence) are preserved; toString() renders the
9
+ // Bubble Tea-style chord ("ctrl+c", "up", "enter") so update() can match on a
10
+ // single string instead of juggling booleans.
11
+ class KeyMsg {
12
+ constructor(key) {
13
+ this.type = 'key'
14
+ this.name = key.name
15
+ this.sequence = key.sequence
16
+ this.ctrl = key.ctrl
17
+ this.meta = key.meta
18
+ this.shift = key.shift
19
+ }
20
+
21
+ toString() {
22
+ const parts = []
23
+ if (this.ctrl) parts.push('ctrl')
24
+ if (this.meta) parts.push('alt')
25
+ // Only surface shift for named keys; letters already arrive upper/lower.
26
+ if (this.shift && this.name && this.name.length > 1) parts.push('shift')
27
+ parts.push(this.name === 'return' ? 'enter' : this.name)
28
+ return parts.join('+')
29
+ }
30
+
31
+ // True if this key matches any of the given chords. A chord is matched
32
+ // against both the full string form ("ctrl+c", "enter") and the bare name
33
+ // ("c", "return"), so 'enter'/'return' and 'esc'/'escape' both work.
34
+ // if (msg.is('q', 'ctrl+c')) ...
35
+ is(...chords) {
36
+ const str = this.toString()
37
+ for (let chord of chords) {
38
+ if (chord === 'esc') chord = 'escape'
39
+ if (chord === str || chord === this.name) return true
40
+ }
41
+ return false
42
+ }
43
+ }
44
+
45
+ // Emitted on startup and whenever the terminal is resized.
46
+ function windowSize(width, height) {
47
+ return { type: 'resize', width, height }
48
+ }
49
+
50
+ // The runtime tears down and exits when it sees this. `quit` (see commands.js)
51
+ // is the Cmd that produces it.
52
+ function quitMsg() {
53
+ return { type: 'quit' }
54
+ }
55
+
56
+ // Wraps an error thrown by a Cmd so it can be handled in update() rather than
57
+ // crashing the loop.
58
+ function errorMsg(error) {
59
+ return { type: 'error', error }
60
+ }
61
+
62
+ module.exports = { KeyMsg, windowSize, quitMsg, errorMsg }
package/mouse.js ADDED
@@ -0,0 +1,102 @@
1
+ // Mouse support — SGR (1006) tracking and decoding.
2
+ //
3
+ // bare-ansi-escapes' KeyDecoder doesn't understand mouse reports, so the
4
+ // Program runs raw input through MouseParser first: it pulls SGR sequences
5
+ // (\x1b[<b;x;yM for press / motion, \x1b[<b;x;ym for release) out of the byte
6
+ // stream as MouseMsgs and forwards everything else to the key decoder.
7
+ //
8
+ // A MouseMsg looks like:
9
+ // { type: 'mouse', action, button, x, y, ctrl, alt, shift }
10
+ // action: 'press' | 'release' | 'motion' | 'wheel'
11
+ // button: 'left' | 'middle' | 'right' | 'none' | 'wheelup' | 'wheeldown'
12
+ // x, y: zero-indexed cell coordinates
13
+ const { constants } = require('bare-ansi-escapes')
14
+ const CSI = constants.CSI
15
+
16
+ const SGR = '?1006' // SGR extended coordinates (no 223-column cap, clean parse)
17
+ const MODES = {
18
+ basic: '?1000', // press / release
19
+ drag: '?1002', // + motion while a button is held
20
+ all: '?1003' // + motion with no button (hover)
21
+ }
22
+
23
+ function enable(mode = 'basic') {
24
+ const m = MODES[mode] || MODES.basic
25
+ return CSI + m + 'h' + CSI + SGR + 'h'
26
+ }
27
+
28
+ function disable(mode = 'basic') {
29
+ const m = MODES[mode] || MODES.basic
30
+ return CSI + SGR + 'l' + CSI + m + 'l'
31
+ }
32
+
33
+ const BUTTONS = ['left', 'middle', 'right', 'none']
34
+
35
+ // Decode the "b;x;y" body of an SGR mouse report plus its final char.
36
+ function decode(body, final) {
37
+ const parts = body.split(';')
38
+ if (parts.length !== 3) return null
39
+ const b = Number(parts[0])
40
+ const col = Number(parts[1])
41
+ const row = Number(parts[2])
42
+ if (!Number.isInteger(b) || !Number.isInteger(col) || !Number.isInteger(row)) {
43
+ return null
44
+ }
45
+
46
+ const mods = { ctrl: !!(b & 16), alt: !!(b & 8), shift: !!(b & 4) }
47
+
48
+ let action
49
+ let button
50
+ if (b & 64) {
51
+ action = 'wheel'
52
+ button = b & 1 ? 'wheeldown' : 'wheelup'
53
+ } else {
54
+ button = BUTTONS[b & 3]
55
+ action = b & 32 ? 'motion' : final === 'M' ? 'press' : 'release'
56
+ }
57
+
58
+ return { type: 'mouse', action, button, x: col - 1, y: row - 1, ...mods }
59
+ }
60
+
61
+ // Splits a byte stream into MouseMsgs and the remaining key bytes. Holds an
62
+ // incomplete trailing mouse sequence between feeds; uses latin1 throughout so
63
+ // non-mouse bytes (including 8-bit meta keys) round-trip to the decoder intact.
64
+ //
65
+ // Note: a mouse report split across two reads *before* its `<` arrives can't be
66
+ // distinguished from a key escape, so we only buffer once `\x1b[<` is seen.
67
+ // Terminals emit each report in a single write, so this is not a problem in
68
+ // practice.
69
+ class MouseParser {
70
+ constructor() {
71
+ this._partial = ''
72
+ }
73
+
74
+ feed(buf) {
75
+ const s = this._partial + buf.toString('latin1')
76
+ this._partial = ''
77
+
78
+ let keys = ''
79
+ const events = []
80
+ let i = 0
81
+ while (i < s.length) {
82
+ if (s[i] === '\x1b' && s[i + 1] === '[' && s[i + 2] === '<') {
83
+ let j = i + 3
84
+ while (j < s.length && s[j] !== 'M' && s[j] !== 'm') j++
85
+ if (j >= s.length) {
86
+ this._partial = s.slice(i) // incomplete report; wait for more
87
+ break
88
+ }
89
+ const ev = decode(s.slice(i + 3, j), s[j])
90
+ if (ev) events.push(ev)
91
+ i = j + 1
92
+ } else {
93
+ keys += s[i]
94
+ i++
95
+ }
96
+ }
97
+
98
+ return { keys: Buffer.from(keys, 'latin1'), events }
99
+ }
100
+ }
101
+
102
+ module.exports = { enable, disable, decode, MouseParser, MODES }
package/package.json CHANGED
@@ -1 +1,59 @@
1
- {"name":"bare-tui","version":"0.0.0"}
1
+ {
2
+ "name": "bare-tui",
3
+ "version": "0.0.2",
4
+ "description": "A little TUI framework for Bare, based on bubbletea",
5
+ "main": "index.js",
6
+ "exports": {
7
+ "./package": "./package.json",
8
+ ".": {
9
+ "types": "./index.d.ts",
10
+ "default": "./index.js"
11
+ }
12
+ },
13
+ "files": [
14
+ "package.json",
15
+ "index.js",
16
+ "index.d.ts",
17
+ "ansi.js",
18
+ "commands.js",
19
+ "key.js",
20
+ "messages.js",
21
+ "mouse.js",
22
+ "program.js",
23
+ "renderer.js",
24
+ "style.js",
25
+ "components",
26
+ "docs",
27
+ "README.md",
28
+ "LICENSE",
29
+ "NOTICE"
30
+ ],
31
+ "dependencies": {
32
+ "bare-ansi-escapes": "^2.2.3",
33
+ "bare-tty": "^5.1.0"
34
+ },
35
+ "devDependencies": {
36
+ "bare-fs": "^4.7.1",
37
+ "bare-path": "^3.0.0",
38
+ "bare-stream": "^2.13.1",
39
+ "brittle": "^3.19.0",
40
+ "lunte": "^1.2.0",
41
+ "prettier": "^3.6.2",
42
+ "prettier-config-holepunch": "^2.0.0"
43
+ },
44
+ "scripts": {
45
+ "format": "prettier --write . && lunte --fix",
46
+ "lint": "prettier --check . && lunte",
47
+ "test": "brittle-bare --coverage test/index.js"
48
+ },
49
+ "repository": {
50
+ "type": "git",
51
+ "url": "https://github.com/holepunchto/bare-tui.git"
52
+ },
53
+ "author": "Holepunch",
54
+ "license": "Apache-2.0",
55
+ "bugs": {
56
+ "url": "https://github.com/holepunchto/bare-tui/issues"
57
+ },
58
+ "homepage": "https://github.com/holepunchto/bare-tui"
59
+ }