@trendr/core 0.1.0 → 0.2.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trendr/core",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "direct-mode TUI renderer with JSX, signals, and per-cell diffing",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -27,9 +27,27 @@
27
27
  "reader": "node esbuild.config.js && node dist/reader.js",
28
28
  "focus-demo": "node esbuild.config.js && node dist/focus-demo.js",
29
29
  "modal-form": "node esbuild.config.js && node dist/modal-form.js",
30
- "highlight": "node esbuild.config.js && node dist/highlight.js src"
30
+ "highlight": "node esbuild.config.js && node dist/highlight.js src",
31
+ "multi-row-list": "node esbuild.config.js && node dist/multi-row-list.js",
32
+ "plasma": "node esbuild.config.js && node dist/plasma.js",
33
+ "rain": "node esbuild.config.js && node dist/rain.js",
34
+ "animation": "node esbuild.config.js && node dist/animation.js",
35
+ "texture": "node esbuild.config.js && node dist/texture.js",
36
+ "absolute": "node esbuild.config.js && node dist/absolute.js",
37
+ "scroll-box": "node esbuild.config.js && node dist/scroll-box.js",
38
+ "split-pane": "node esbuild.config.js && node dist/split-pane.js",
39
+ "custom-table": "node esbuild.config.js && node dist/custom-table.js",
40
+ "async": "node esbuild.config.js && node dist/async.js",
41
+ "timeout": "node esbuild.config.js && node dist/timeout.js",
42
+ "task": "node esbuild.config.js && node dist/task.js",
43
+ "progress": "node esbuild.config.js && node dist/progress.js",
44
+ "shimmer": "node esbuild.config.js && node dist/shimmer.js",
45
+ "spinner": "node esbuild.config.js && node dist/spinner.js",
46
+ "demo": "node esbuild.config.js && node demo/server.js",
47
+ "demo:build": "node esbuild.config.js"
31
48
  },
32
49
  "devDependencies": {
33
- "esbuild": "^0.25.0"
50
+ "esbuild": "^0.25.0",
51
+ "ssh2": "^1.16.0"
34
52
  }
35
53
  }
@@ -0,0 +1,204 @@
1
+ import { createSignalRaw, batch } from './signal.js'
2
+ import { registerHook } from './renderer.js'
3
+ import { onCleanup } from './signal.js'
4
+
5
+ const active = new Set()
6
+ let loopTimer = null
7
+
8
+ function startLoop() {
9
+ if (loopTimer) return
10
+ loopTimer = setInterval(tickAll, 1000 / 60)
11
+ }
12
+
13
+ function stopLoop() {
14
+ if (loopTimer) {
15
+ clearInterval(loopTimer)
16
+ loopTimer = null
17
+ }
18
+ }
19
+
20
+ function tickAll() {
21
+ if (active.size === 0) {
22
+ stopLoop()
23
+ return
24
+ }
25
+
26
+ const dt = 1 / 60
27
+
28
+ batch(() => {
29
+ for (const anim of active) {
30
+ const result = anim.interpolator(anim.current, anim.target, anim.velocity, dt)
31
+ anim.current = result.value
32
+ anim.velocity = result.velocity
33
+ anim.setter(result.value)
34
+ if (anim.onTick) anim.onTick(result.value)
35
+
36
+ if (result.done) {
37
+ anim.current = anim.target
38
+ anim.setter(anim.target)
39
+ if (anim.onTick) anim.onTick(anim.target)
40
+ active.delete(anim)
41
+ }
42
+ }
43
+ })
44
+
45
+ if (active.size === 0) stopLoop()
46
+ }
47
+
48
+ export function animated(initial, interpolator) {
49
+ const [get, set] = createSignalRaw(initial)
50
+
51
+ const anim = {
52
+ current: initial,
53
+ target: initial,
54
+ velocity: 0,
55
+ interpolator,
56
+ setter: set,
57
+ onTick: null,
58
+ }
59
+
60
+ function setTarget(target) {
61
+ if (target === anim.target && !active.has(anim)) return
62
+ anim.target = target
63
+ active.add(anim)
64
+ startLoop()
65
+ }
66
+
67
+ function getValue() {
68
+ return get()
69
+ }
70
+
71
+ getValue.set = setTarget
72
+ getValue.get = get
73
+ getValue.snap = (v) => {
74
+ anim.target = v
75
+ anim.current = v
76
+ anim.velocity = 0
77
+ active.delete(anim)
78
+ set(v)
79
+ }
80
+ getValue.setInterpolator = (interp) => {
81
+ anim.interpolator = interp
82
+ anim.velocity = 0
83
+ }
84
+ getValue.onTick = (fn) => { anim.onTick = fn }
85
+ getValue._anim = anim
86
+
87
+ return getValue
88
+ }
89
+
90
+ export function useAnimated(initial, interpolator) {
91
+ return registerHook(() => {
92
+ const a = animated(initial, interpolator)
93
+ onCleanup(() => {
94
+ active.delete(a._anim)
95
+ })
96
+ return a
97
+ })
98
+ }
99
+
100
+ // -- interpolators --
101
+
102
+ export function spring({ frequency = 2, damping = 0.3 } = {}) {
103
+ const omega = frequency * 2 * Math.PI
104
+ const zeta = damping
105
+
106
+ return function stepSpring(current, target, velocity, dt) {
107
+ const x0 = current - target
108
+ const v0 = velocity
109
+
110
+ let newX, newV
111
+
112
+ if (zeta > 1) {
113
+ // overdamped
114
+ const za = -omega * (zeta + Math.sqrt(zeta * zeta - 1))
115
+ const zb = -omega * (zeta - Math.sqrt(zeta * zeta - 1))
116
+ const expA = Math.exp(za * dt)
117
+ const expB = Math.exp(zb * dt)
118
+ const c2 = (v0 - x0 * za) / (zb - za)
119
+ const c1 = x0 - c2
120
+ newX = c1 * expA + c2 * expB
121
+ newV = c1 * za * expA + c2 * zb * expB
122
+ } else if (zeta === 1) {
123
+ // critically damped
124
+ const expW = Math.exp(-omega * dt)
125
+ newX = (x0 + (v0 + omega * x0) * dt) * expW
126
+ newV = (v0 - (v0 + omega * x0) * omega * dt) * expW
127
+ } else {
128
+ // underdamped
129
+ const alpha = omega * Math.sqrt(1 - zeta * zeta)
130
+ const expW = Math.exp(-zeta * omega * dt)
131
+ const cosA = Math.cos(alpha * dt)
132
+ const sinA = Math.sin(alpha * dt)
133
+ newX = expW * (x0 * cosA + ((v0 + zeta * omega * x0) / alpha) * sinA)
134
+ newV = expW * (v0 * cosA - ((v0 * zeta * omega + omega * omega * x0) / alpha) * sinA)
135
+ }
136
+
137
+ const done = Math.abs(newX) < 0.01 && Math.abs(newV) < 0.01
138
+
139
+ return { value: target + newX, velocity: newV, done }
140
+ }
141
+ }
142
+
143
+ export function ease(durationMs, easingFn = easeOutCubic) {
144
+ let elapsed = 0
145
+ let startValue = null
146
+
147
+ return function stepEase(current, target, velocity, dt) {
148
+ if (startValue === null) startValue = current
149
+
150
+ elapsed += dt * 1000
151
+ const t = Math.min(1, elapsed / durationMs)
152
+ const eased = easingFn(t)
153
+ const value = startValue + (target - startValue) * eased
154
+
155
+ if (t >= 1) {
156
+ startValue = null
157
+ elapsed = 0
158
+ return { value: target, velocity: 0, done: true }
159
+ }
160
+
161
+ return { value, velocity: 0, done: false }
162
+ }
163
+ }
164
+
165
+ export function decay({ deceleration = 0.998 } = {}) {
166
+ return function stepDecay(current, target, velocity, dt) {
167
+ const newVelocity = velocity * Math.pow(deceleration, dt * 1000)
168
+ const newValue = current + newVelocity * dt
169
+
170
+ if (Math.abs(newVelocity) < 0.1) {
171
+ return { value: newValue, velocity: 0, done: true }
172
+ }
173
+
174
+ return { value: newValue, velocity: newVelocity, done: false }
175
+ }
176
+ }
177
+
178
+ // -- easing functions --
179
+
180
+ export function linear(t) { return t }
181
+ export function easeInQuad(t) { return t * t }
182
+ export function easeOutQuad(t) { return t * (2 - t) }
183
+ export function easeInOutQuad(t) { return t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t }
184
+ export function easeInCubic(t) { return t * t * t }
185
+ export function easeOutCubic(t) { return (--t) * t * t + 1 }
186
+ export function easeInOutCubic(t) { return t < 0.5 ? 4 * t * t * t : (t - 1) * (2 * t - 2) * (2 * t - 2) + 1 }
187
+ export function easeOutElastic({ amplitude = 1, period = 0.3 } = {}) {
188
+ const a = Math.max(1, amplitude)
189
+ const s = period / (2 * Math.PI) * Math.asin(1 / a)
190
+ return function (t) {
191
+ if (t === 0 || t === 1) return t
192
+ return a * Math.pow(2, -10 * t) * Math.sin((t - s) * (2 * Math.PI) / period) + 1
193
+ }
194
+ }
195
+
196
+ export function easeOutBounce({ bounciness = 7.5625, threshold = 2.75 } = {}) {
197
+ return function (t) {
198
+ const inv = 1 / threshold
199
+ if (t < inv) return bounciness * t * t
200
+ if (t < 2 * inv) return bounciness * (t -= 1.5 / threshold) * t + 0.75
201
+ if (t < 2.5 / threshold) return bounciness * (t -= 2.25 / threshold) * t + 0.9375
202
+ return bounciness * (t -= 2.625 / threshold) * t + 0.984375
203
+ }
204
+ }
package/src/ansi.js CHANGED
@@ -16,6 +16,9 @@ export const sgrReset = `${ESC}0m`
16
16
 
17
17
  export const setTitle = (title) => `\x1b]2;${title}\x07`
18
18
 
19
+ export const enableMouse = `${ESC}?1002h${ESC}?1006h`
20
+ export const disableMouse = `${ESC}?1002l${ESC}?1006l`
21
+
19
22
  export const BOLD = 1
20
23
  export const DIM = 2
21
24
  export const ITALIC = 4
package/src/buffer.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { parseSgr } from './ansi.js'
2
+ import { charWidth } from './wrap.js'
2
3
 
3
4
  const EMPTY = { ch: ' ', fg: null, bg: null, attrs: 0 }
4
5
 
@@ -31,17 +32,29 @@ export function writeText(buf, x, y, text, fg, bg, attrs, maxWidth) {
31
32
  const max = maxWidth ?? (buf.width - x)
32
33
 
33
34
  if (text.indexOf('\x1b') === -1) {
34
- const n = Math.min(max, text.length, buf.width - x)
35
- for (let i = 0; i < n; i++) {
36
- const cx = x + i
37
- if (cx < 0 || cx >= buf.width) continue
38
- const prev = buf.cells[y * buf.width + cx]
39
- buf.cells[y * buf.width + cx] = {
40
- ch: text[i],
41
- fg: fg ?? prev.fg,
42
- bg: bg ?? prev.bg,
43
- attrs: attrs || prev.attrs,
35
+ let col = 0
36
+ let i = 0
37
+ while (i < text.length && col < max) {
38
+ const code = text.codePointAt(i)
39
+ const len = code > 0xffff ? 2 : 1
40
+ const w = charWidth(code)
41
+ const cx = x + col
42
+ if (cx >= 0 && cx < buf.width) {
43
+ const ch = len === 1 ? text[i] : text.slice(i, i + len)
44
+ const prev = buf.cells[y * buf.width + cx]
45
+ const transparent = ch === ' ' && !bg && prev.ch !== ' '
46
+ buf.cells[y * buf.width + cx] = {
47
+ ch: transparent ? prev.ch : ch,
48
+ fg: transparent ? prev.fg : (fg ?? prev.fg),
49
+ bg: bg ?? prev.bg,
50
+ attrs: transparent ? prev.attrs : (attrs || prev.attrs),
51
+ }
52
+ if (w === 2 && cx + 1 < buf.width) {
53
+ buf.cells[y * buf.width + cx + 1] = { ch: '', fg: fg ?? null, bg: bg ?? null, attrs: attrs ?? 0 }
54
+ }
44
55
  }
56
+ col += w
57
+ i += len
45
58
  }
46
59
  return
47
60
  }
@@ -60,18 +73,26 @@ export function writeText(buf, x, y, text, fg, bg, attrs, maxWidth) {
60
73
  }
61
74
  }
62
75
 
76
+ const code = text.codePointAt(i)
77
+ const len = code > 0xffff ? 2 : 1
78
+ const w = charWidth(code)
63
79
  const cx = x + col
64
80
  if (cx >= 0 && cx < buf.width) {
81
+ const ch = len === 1 ? text[i] : text.slice(i, i + len)
65
82
  const prev = buf.cells[y * buf.width + cx]
83
+ const transparent = ch === ' ' && !bg && prev.ch !== ' '
66
84
  buf.cells[y * buf.width + cx] = {
67
- ch: text[i],
68
- fg: ansi.fg ?? fg ?? prev.fg,
85
+ ch: transparent ? prev.ch : ch,
86
+ fg: transparent ? prev.fg : (ansi.fg ?? fg ?? prev.fg),
69
87
  bg: ansi.bg ?? bg ?? prev.bg,
70
- attrs: ansi.attrs || attrs || prev.attrs,
88
+ attrs: transparent ? prev.attrs : (ansi.attrs || attrs || prev.attrs),
89
+ }
90
+ if (w === 2 && cx + 1 < buf.width) {
91
+ buf.cells[y * buf.width + cx + 1] = { ch: '', fg: ansi.fg ?? fg ?? null, bg: ansi.bg ?? bg ?? null, attrs: ansi.attrs || attrs || 0 }
71
92
  }
72
93
  }
73
- col++
74
- i++
94
+ col += w
95
+ i += len
75
96
  }
76
97
  }
77
98
 
@@ -99,3 +120,16 @@ export function copyBuffer(src, dst) {
99
120
  const len = Math.min(src.cells.length, dst.cells.length)
100
121
  for (let i = 0; i < len; i++) dst.cells[i] = src.cells[i]
101
122
  }
123
+
124
+ export function blitRect(src, dst, x, y, w, h) {
125
+ const x1 = Math.max(x, 0)
126
+ const y1 = Math.max(y, 0)
127
+ const x2 = Math.min(x + w, src.width, dst.width)
128
+ const y2 = Math.min(y + h, src.height, dst.height)
129
+ for (let row = y1; row < y2; row++) {
130
+ const base = row * dst.width
131
+ for (let col = x1; col < x2; col++) {
132
+ dst.cells[base + col] = src.cells[base + col]
133
+ }
134
+ }
135
+ }
package/src/button.js CHANGED
@@ -1,8 +1,9 @@
1
1
  import { jsx } from '../jsx-runtime.js'
2
- import { useInput, useTheme } from './hooks.js'
2
+ import { useInput, useMouse, useLayout, useTheme } from './hooks.js'
3
3
 
4
4
  export function Button({ label, onPress, focused = false, variant }) {
5
5
  const { accent = 'cyan' } = useTheme()
6
+ const layout = useLayout()
6
7
 
7
8
  useInput((event) => {
8
9
  if (!focused) return
@@ -12,6 +13,14 @@ export function Button({ label, onPress, focused = false, variant }) {
12
13
  }
13
14
  })
14
15
 
16
+ useMouse((event) => {
17
+ if (event.action !== 'press' || event.button !== 'left') return
18
+ const { x, y } = event
19
+ if (x < layout.x || x >= layout.x + layout.width || y < layout.y || y >= layout.y + layout.height) return
20
+ onPress?.()
21
+ event.stopPropagation()
22
+ })
23
+
15
24
  const dim = variant === 'dim'
16
25
 
17
26
  return jsx('text', {
package/src/checkbox.js CHANGED
@@ -1,8 +1,9 @@
1
1
  import { jsx, jsxs } from '../jsx-runtime.js'
2
- import { useInput, useTheme } from './hooks.js'
2
+ import { useInput, useMouse, useLayout, useTheme } from './hooks.js'
3
3
 
4
- export function Checkbox({ checked = false, label, onChange, focused = false }) {
4
+ export function Checkbox({ checked = false, label, onChange, focused = false, checkedIcon = '[x]', uncheckedIcon = '[ ]' }) {
5
5
  const { accent = 'cyan' } = useTheme()
6
+ const layout = useLayout()
6
7
 
7
8
  useInput((event) => {
8
9
  if (!focused) return
@@ -12,7 +13,15 @@ export function Checkbox({ checked = false, label, onChange, focused = false })
12
13
  }
13
14
  })
14
15
 
15
- const icon = checked ? '[x]' : '[ ]'
16
+ useMouse((event) => {
17
+ if (event.action !== 'press' || event.button !== 'left') return
18
+ const { x, y } = event
19
+ if (x < layout.x || x >= layout.x + layout.width || y < layout.y || y >= layout.y + layout.height) return
20
+ onChange?.(!checked)
21
+ event.stopPropagation()
22
+ })
23
+
24
+ const icon = checked ? checkedIcon : uncheckedIcon
16
25
  const bg = focused ? accent : null
17
26
  const color = focused ? 'black' : null
18
27
 
package/src/diff.js CHANGED
@@ -1,4 +1,104 @@
1
- import { moveTo, sgr, sgrReset } from './ansi.js'
1
+ import { BOLD, DIM, ITALIC, UNDERLINE, INVERSE, STRIKETHROUGH } from './ansi.js'
2
+
3
+ const NAMED_COLORS = {
4
+ black: 0, red: 1, green: 2, yellow: 3,
5
+ blue: 4, magenta: 5, cyan: 6, white: 7,
6
+ gray: 8, grey: 8,
7
+ brightRed: 9, brightGreen: 10, brightYellow: 11,
8
+ brightBlue: 12, brightMagenta: 13, brightCyan: 14, brightWhite: 15,
9
+ }
10
+
11
+ const ATTR_MASKS = [BOLD, DIM, ITALIC, UNDERLINE, INVERSE, STRIKETHROUGH]
12
+ const ATTR_SGRCODES = [49, 50, 51, 52, 55, 57] // '1','2','3','4','7','9' as ascii
13
+
14
+ const bufA = Buffer.allocUnsafe(2 * 1024 * 1024)
15
+ const bufB = Buffer.allocUnsafe(2 * 1024 * 1024)
16
+ let buf = bufA
17
+ let pos = 0
18
+
19
+ function ensure(n) {
20
+ if (pos + n > buf.length) throw new Error('diff output buffer overflow')
21
+ }
22
+
23
+ function writeNum(n) {
24
+ if (n >= 1000) { buf[pos++] = 48 + (n / 1000 | 0); n %= 1000; buf[pos++] = 48 + (n / 100 | 0); n %= 100; buf[pos++] = 48 + (n / 10 | 0); buf[pos++] = 48 + (n % 10) }
25
+ else if (n >= 100) { buf[pos++] = 48 + (n / 100 | 0); n %= 100; buf[pos++] = 48 + (n / 10 | 0); buf[pos++] = 48 + (n % 10) }
26
+ else if (n >= 10) { buf[pos++] = 48 + (n / 10 | 0); buf[pos++] = 48 + (n % 10) }
27
+ else buf[pos++] = 48 + n
28
+ }
29
+
30
+ function writeMoveTo(row, col) {
31
+ ensure(12)
32
+ buf[pos++] = 0x1b
33
+ buf[pos++] = 0x5b
34
+ writeNum(row)
35
+ buf[pos++] = 0x3b
36
+ writeNum(col)
37
+ buf[pos++] = 0x48
38
+ }
39
+
40
+ function writeSgr(fg, bg, attrs) {
41
+ ensure(52)
42
+ buf[pos++] = 0x1b
43
+ buf[pos++] = 0x5b
44
+ buf[pos++] = 48 // '0' - reset
45
+
46
+ for (let i = 0; i < 6; i++) {
47
+ if (attrs & ATTR_MASKS[i]) {
48
+ buf[pos++] = 0x3b
49
+ buf[pos++] = ATTR_SGRCODES[i]
50
+ }
51
+ }
52
+
53
+ if (fg != null) writeColor(fg, 38)
54
+ if (bg != null) writeColor(bg, 48)
55
+
56
+ buf[pos++] = 0x6d
57
+ }
58
+
59
+ function writeColor(color, offset) {
60
+ if (typeof color === 'number' || color in NAMED_COLORS) {
61
+ const idx = typeof color === 'number' ? color : NAMED_COLORS[color]
62
+ buf[pos++] = 0x3b
63
+ writeNum(offset)
64
+ buf[pos++] = 0x3b
65
+ buf[pos++] = 53 // '5'
66
+ buf[pos++] = 0x3b
67
+ writeNum(idx)
68
+ } else if (color.charCodeAt(0) === 35 && color.length === 7) {
69
+ const r = parseInt(color.slice(1, 3), 16)
70
+ const g = parseInt(color.slice(3, 5), 16)
71
+ const b = parseInt(color.slice(5, 7), 16)
72
+ buf[pos++] = 0x3b
73
+ writeNum(offset)
74
+ buf[pos++] = 0x3b
75
+ buf[pos++] = 50 // '2'
76
+ buf[pos++] = 0x3b
77
+ writeNum(r)
78
+ buf[pos++] = 0x3b
79
+ writeNum(g)
80
+ buf[pos++] = 0x3b
81
+ writeNum(b)
82
+ }
83
+ }
84
+
85
+ function writeReset() {
86
+ ensure(4)
87
+ buf[pos++] = 0x1b
88
+ buf[pos++] = 0x5b
89
+ buf[pos++] = 48
90
+ buf[pos++] = 0x6d
91
+ }
92
+
93
+ function writeChar(ch) {
94
+ ensure(4)
95
+ const code = ch.charCodeAt(0)
96
+ if (code < 0x80) {
97
+ buf[pos++] = code
98
+ } else {
99
+ pos += buf.write(ch, pos)
100
+ }
101
+ }
2
102
 
3
103
  function cellEq(a, b) {
4
104
  return a === b || (a.ch === b.ch && a.fg === b.fg && a.bg === b.bg && a.attrs === b.attrs)
@@ -7,7 +107,8 @@ function cellEq(a, b) {
7
107
  export function diff(prev, curr) {
8
108
  const w = curr.width
9
109
  const h = curr.height
10
- const parts = []
110
+ pos = 0
111
+ let changed = 0
11
112
 
12
113
  let lastFg = undefined
13
114
  let lastBg = undefined
@@ -17,35 +118,37 @@ export function diff(prev, curr) {
17
118
  let x = 0
18
119
  while (x < w) {
19
120
  const idx = y * w + x
20
- if (cellEq(prev.cells[idx], curr.cells[idx])) {
121
+ if (curr.cells[idx].ch === '' || cellEq(prev.cells[idx], curr.cells[idx])) {
21
122
  x++
22
123
  continue
23
124
  }
24
125
 
25
- parts.push(moveTo(y + 1, x + 1))
126
+ writeMoveTo(y + 1, x + 1)
26
127
 
27
128
  while (x < w) {
28
129
  const i = y * w + x
29
- if (cellEq(prev.cells[i], curr.cells[i])) break
30
-
31
130
  const c = curr.cells[i]
131
+ if (c.ch === '') { x++; continue }
132
+ if (cellEq(prev.cells[i], c)) break
133
+
134
+ changed++
32
135
 
33
136
  if (c.fg !== lastFg || c.bg !== lastBg || c.attrs !== lastAttrs) {
34
- parts.push(sgr(c.fg, c.bg, c.attrs))
137
+ writeSgr(c.fg, c.bg, c.attrs)
35
138
  lastFg = c.fg
36
139
  lastBg = c.bg
37
140
  lastAttrs = c.attrs
38
141
  }
39
142
 
40
- parts.push(c.ch)
143
+ writeChar(c.ch)
41
144
  x++
42
145
  }
43
146
  }
44
147
  }
45
148
 
46
- if (parts.length > 0) {
47
- parts.push(sgrReset)
48
- }
149
+ if (pos > 0) writeReset()
49
150
 
50
- return parts.join('')
151
+ const output = pos > 0 ? buf.subarray(0, pos) : ''
152
+ buf = buf === bufA ? bufB : bufA
153
+ return { output, changed }
51
154
  }