cellery 0.0.4 → 1.0.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/README.md +68 -6
- package/index.js +9 -0
- package/lib/cellery.js +24 -0
- package/lib/cells.js +116 -0
- package/lib/{base.js → decorations.js} +47 -85
- package/package.json +5 -26
- package/example.js +0 -258
- package/lib/components/base.js +0 -165
- package/lib/components/index.js +0 -7
- package/lib/components/list.js +0 -107
- package/lib/index.js +0 -7
- package/lib/keys.js +0 -9
- package/lib/renderers/html-renderer.js +0 -133
- package/lib/renderers/index.js +0 -4
- package/lib/renderers/tui-renderer.js +0 -480
|
@@ -1,480 +0,0 @@
|
|
|
1
|
-
const process = require('process')
|
|
2
|
-
const { cursorPosition, eraseLine, eraseDisplay } = require('bare-ansi-escapes')
|
|
3
|
-
const { Alignment } = require('../base')
|
|
4
|
-
const goodbye = require('graceful-goodbye')
|
|
5
|
-
const keys = require('../keys')
|
|
6
|
-
|
|
7
|
-
const originalRawMode = process.stdin.isRaw
|
|
8
|
-
|
|
9
|
-
function start() {
|
|
10
|
-
process.stdin.setRawMode(true)
|
|
11
|
-
process.stdin.resume()
|
|
12
|
-
process.stdin.setEncoding('utf8')
|
|
13
|
-
|
|
14
|
-
process.stdout.write('\x1b[?1049h')
|
|
15
|
-
process.stdout.write('\x1b[?25l')
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
function exit() {
|
|
19
|
-
process.stdout.write(cursorPosition(0))
|
|
20
|
-
process.stdout.write(eraseLine)
|
|
21
|
-
process.stdin.setRawMode(originalRawMode)
|
|
22
|
-
process.stdout.write('\x1b[?25h')
|
|
23
|
-
process.stdout.write('\x1b[?1049l')
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function parseDimension(value, parentValue) {
|
|
27
|
-
if (typeof value === 'number') {
|
|
28
|
-
return value
|
|
29
|
-
} else if (typeof value === 'string') {
|
|
30
|
-
if (value.endsWith('%')) {
|
|
31
|
-
const percent = Number(value.replace('%', '')) / 100
|
|
32
|
-
return parentValue * percent
|
|
33
|
-
} else if (value.startsWith('calc(') && value.endsWith(')')) {
|
|
34
|
-
// Simple calc parser: calc(100% - N) or calc(100% + N)
|
|
35
|
-
const expr = value.slice(5, -1).trim()
|
|
36
|
-
const match = expr.match(/^(\d+)%\s*([+-])\s*(\d+)$/)
|
|
37
|
-
if (match) {
|
|
38
|
-
const percent = Number(match[1]) / 100
|
|
39
|
-
const operator = match[2]
|
|
40
|
-
const offset = Number(match[3])
|
|
41
|
-
const base = parentValue * percent
|
|
42
|
-
return operator === '+' ? base + offset : base - offset
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
return 0
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
function getDimensions() {
|
|
51
|
-
const width = parseDimension(this.width, this.parent?.width)
|
|
52
|
-
const height = parseDimension(this.height, this.parent?.height)
|
|
53
|
-
|
|
54
|
-
return {
|
|
55
|
-
width: Math.floor(width),
|
|
56
|
-
height: Math.floor(height)
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
class CelleryRendererTUI {
|
|
61
|
-
hotkeys = new Map()
|
|
62
|
-
|
|
63
|
-
components = {
|
|
64
|
-
Container: function () {
|
|
65
|
-
const { width, height } = getDimensions.call(this)
|
|
66
|
-
|
|
67
|
-
// Calculate margins (outside everything)
|
|
68
|
-
const margin = this.margin || { left: 0, top: 0, right: 0, bottom: 0 }
|
|
69
|
-
const marginLeft = Math.floor(margin.left)
|
|
70
|
-
const marginTop = Math.floor(margin.top)
|
|
71
|
-
const marginRight = Math.floor(margin.right)
|
|
72
|
-
const marginBottom = Math.floor(margin.bottom)
|
|
73
|
-
|
|
74
|
-
// Check if border exists
|
|
75
|
-
const hasBorder = this.decoration && this.decoration.border
|
|
76
|
-
const borderWidth = hasBorder ? 1 : 0
|
|
77
|
-
|
|
78
|
-
// Calculate padding (inside decoration/border)
|
|
79
|
-
const padding = this.padding || { left: 0, top: 0, right: 0, bottom: 0 }
|
|
80
|
-
const paddingLeft = Math.floor(padding.left)
|
|
81
|
-
const paddingTop = Math.floor(padding.top)
|
|
82
|
-
const paddingRight = Math.floor(padding.right)
|
|
83
|
-
const paddingBottom = Math.floor(padding.bottom)
|
|
84
|
-
|
|
85
|
-
// Get background color
|
|
86
|
-
const backgroundColor = this.decoration?.color || this.color || null
|
|
87
|
-
|
|
88
|
-
// Create grid
|
|
89
|
-
const grid = []
|
|
90
|
-
for (let y = 0; y < height; y++) {
|
|
91
|
-
const row = []
|
|
92
|
-
for (let x = 0; x < width; x++) {
|
|
93
|
-
let char = ' '
|
|
94
|
-
let fgColor = null
|
|
95
|
-
let bgColor = null
|
|
96
|
-
|
|
97
|
-
// Check if we're in the margin area (outside)
|
|
98
|
-
const inMarginX = x < marginLeft || x >= width - marginRight
|
|
99
|
-
const inMarginY = y < marginTop || y >= height - marginBottom
|
|
100
|
-
|
|
101
|
-
// Adjust coordinates relative to decoration area (after margin)
|
|
102
|
-
const decorationX = x - marginLeft
|
|
103
|
-
const decorationY = y - marginTop
|
|
104
|
-
const decorationWidth = width - marginLeft - marginRight
|
|
105
|
-
const decorationHeight = height - marginTop - marginBottom
|
|
106
|
-
|
|
107
|
-
// Check if we're on the border
|
|
108
|
-
const onBorder =
|
|
109
|
-
!inMarginX &&
|
|
110
|
-
!inMarginY &&
|
|
111
|
-
hasBorder &&
|
|
112
|
-
(decorationX === 0 ||
|
|
113
|
-
decorationX === decorationWidth - 1 ||
|
|
114
|
-
decorationY === 0 ||
|
|
115
|
-
decorationY === decorationHeight - 1)
|
|
116
|
-
|
|
117
|
-
// Apply background color if not in margin and not on border
|
|
118
|
-
if (!inMarginX && !inMarginY && !onBorder && backgroundColor) {
|
|
119
|
-
bgColor = backgroundColor
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
// Only render border/decoration if not in margin
|
|
123
|
-
if (!inMarginX && !inMarginY && hasBorder) {
|
|
124
|
-
// Check border positions (using -1 for last index)
|
|
125
|
-
if (decorationX === 0 && decorationY === 0) {
|
|
126
|
-
char = '┌'
|
|
127
|
-
fgColor = this.decoration.border.color
|
|
128
|
-
} else if (decorationX === 0 && decorationY === decorationHeight - 1) {
|
|
129
|
-
char = '└'
|
|
130
|
-
fgColor = this.decoration.border.color
|
|
131
|
-
} else if (decorationX === decorationWidth - 1 && decorationY === 0) {
|
|
132
|
-
char = '┐'
|
|
133
|
-
fgColor = this.decoration.border.color
|
|
134
|
-
} else if (
|
|
135
|
-
decorationX === decorationWidth - 1 &&
|
|
136
|
-
decorationY === decorationHeight - 1
|
|
137
|
-
) {
|
|
138
|
-
char = '┘'
|
|
139
|
-
fgColor = this.decoration.border.color
|
|
140
|
-
} else if (decorationX === 0 || decorationX === decorationWidth - 1) {
|
|
141
|
-
char = '│'
|
|
142
|
-
fgColor = this.decoration.border.color
|
|
143
|
-
} else if (decorationY === 0 || decorationY === decorationHeight - 1) {
|
|
144
|
-
char = '─'
|
|
145
|
-
fgColor = this.decoration.border.color
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
row.push({ char, fgColor, bgColor })
|
|
150
|
-
}
|
|
151
|
-
grid.push(row)
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
// Render children vertically stacked
|
|
155
|
-
// Children go inside: margin → border → padding
|
|
156
|
-
if (this.children && this.children.length > 0) {
|
|
157
|
-
const childBaseX = marginLeft + borderWidth + paddingLeft
|
|
158
|
-
let childCurrentY = marginTop + borderWidth + paddingTop
|
|
159
|
-
|
|
160
|
-
// Calculate available width and height for children
|
|
161
|
-
const availableWidth =
|
|
162
|
-
width - marginLeft - marginRight - borderWidth * 2 - paddingLeft - paddingRight
|
|
163
|
-
|
|
164
|
-
const availableHeight =
|
|
165
|
-
height - marginTop - marginBottom - borderWidth * 2 - paddingTop - paddingBottom
|
|
166
|
-
|
|
167
|
-
const childParent = {
|
|
168
|
-
width: availableWidth,
|
|
169
|
-
height: availableHeight
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
for (const child of this.children) {
|
|
173
|
-
const childGrid = this.renderer._renderComponent(child, {
|
|
174
|
-
parent: childParent
|
|
175
|
-
})
|
|
176
|
-
if (!childGrid) continue
|
|
177
|
-
|
|
178
|
-
// Calculate horizontal position based on alignment
|
|
179
|
-
let childX = childBaseX
|
|
180
|
-
const childWidth = childGrid[0]?.length || 0
|
|
181
|
-
|
|
182
|
-
if (this.alignment === Alignment.Center) {
|
|
183
|
-
childX = childBaseX + Math.floor((availableWidth - childWidth) / 2)
|
|
184
|
-
} else if (this.alignment === Alignment.Right) {
|
|
185
|
-
childX = childBaseX + (availableWidth - childWidth)
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
this.renderer._mergeIntoBuffer(grid, childGrid, childX, childCurrentY)
|
|
189
|
-
|
|
190
|
-
// Move down by the height of the child
|
|
191
|
-
childCurrentY += childGrid.length
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
return grid
|
|
196
|
-
},
|
|
197
|
-
|
|
198
|
-
Center: function () {
|
|
199
|
-
const { width, height } = getDimensions.call(this.parent)
|
|
200
|
-
|
|
201
|
-
// Create empty grid
|
|
202
|
-
const grid = []
|
|
203
|
-
for (let y = 0; y < height; y++) {
|
|
204
|
-
const row = []
|
|
205
|
-
for (let x = 0; x < width; x++) {
|
|
206
|
-
row.push({ char: ' ', fgColor: null, bgColor: null })
|
|
207
|
-
}
|
|
208
|
-
grid.push(row)
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
// Render child if exists
|
|
212
|
-
if (this.child) {
|
|
213
|
-
const childParent = {
|
|
214
|
-
width: width,
|
|
215
|
-
height: height
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
const childGrid = this.renderer._renderComponent(this.child, {
|
|
219
|
-
parent: childParent
|
|
220
|
-
})
|
|
221
|
-
|
|
222
|
-
// Calculate center position
|
|
223
|
-
const childWidth = childGrid[0]?.length || 0
|
|
224
|
-
const childHeight = childGrid.length
|
|
225
|
-
|
|
226
|
-
const centerX = Math.floor((width - childWidth) / 2)
|
|
227
|
-
const centerY = Math.floor((height - childHeight) / 2)
|
|
228
|
-
|
|
229
|
-
this.renderer._mergeIntoBuffer(grid, childGrid, centerX, centerY)
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
return grid
|
|
233
|
-
},
|
|
234
|
-
|
|
235
|
-
Text: function () {
|
|
236
|
-
const text = String(this.value)
|
|
237
|
-
const grid = [[]]
|
|
238
|
-
|
|
239
|
-
for (let i = 0; i < text.length; i++) {
|
|
240
|
-
grid[0].push({ char: text[i], fgColor: this.color, bgColor: null })
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
return grid
|
|
244
|
-
},
|
|
245
|
-
|
|
246
|
-
Pressable: function () {
|
|
247
|
-
if (this.hotkey) {
|
|
248
|
-
this.renderer.registerHotkey(this.hotkey, () => {
|
|
249
|
-
this.onPress()
|
|
250
|
-
})
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
if (!this.child) return
|
|
254
|
-
|
|
255
|
-
return this.renderer._renderComponent(this.child, {
|
|
256
|
-
parent: this.parent
|
|
257
|
-
})
|
|
258
|
-
},
|
|
259
|
-
|
|
260
|
-
Scrollable: function () {
|
|
261
|
-
const { width, height } = getDimensions.call(this)
|
|
262
|
-
|
|
263
|
-
// Create viewport grid
|
|
264
|
-
const grid = []
|
|
265
|
-
for (let y = 0; y < height; y++) {
|
|
266
|
-
const row = []
|
|
267
|
-
for (let x = 0; x < width; x++) {
|
|
268
|
-
row.push({ char: ' ', fgColor: null, bgColor: null })
|
|
269
|
-
}
|
|
270
|
-
grid.push(row)
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
if (!this.child) {
|
|
274
|
-
// Store empty viewport info
|
|
275
|
-
this._renderedViewport = { itemCount: 0, height, itemHeight: 0 }
|
|
276
|
-
return grid
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
// The child should be a Container with children
|
|
280
|
-
// We need to know how many children it has to calculate viewport
|
|
281
|
-
const childrenCount = this.child.children?.length || 0
|
|
282
|
-
|
|
283
|
-
if (childrenCount === 0) {
|
|
284
|
-
this._renderedViewport = { itemCount: 0, height, itemHeight: 0 }
|
|
285
|
-
return grid
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
// Measure first child to determine item height
|
|
289
|
-
let itemHeight = 1
|
|
290
|
-
if (this.child.children && this.child.children.length > 0) {
|
|
291
|
-
const firstChild = this.child.children[0]
|
|
292
|
-
const firstChildGrid = this.renderer._renderComponent(firstChild, {
|
|
293
|
-
parent: { width, height }
|
|
294
|
-
})
|
|
295
|
-
if (firstChildGrid) {
|
|
296
|
-
itemHeight = firstChildGrid.length
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
const viewportItemCount = Math.floor(height / itemHeight)
|
|
301
|
-
|
|
302
|
-
// Store viewport info for component to use
|
|
303
|
-
this._renderedViewport = {
|
|
304
|
-
itemCount: viewportItemCount,
|
|
305
|
-
height,
|
|
306
|
-
itemHeight
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
// Get visible range based on scroll offset
|
|
310
|
-
const { start, end } = this.getVisibleRange(viewportItemCount, childrenCount)
|
|
311
|
-
|
|
312
|
-
// Store metadata for scroll indicators (optional)
|
|
313
|
-
this._scrollInfo = {
|
|
314
|
-
canScrollUp: start > 0,
|
|
315
|
-
canScrollDown: end < childrenCount,
|
|
316
|
-
visibleCount: end - start,
|
|
317
|
-
totalCount: childrenCount,
|
|
318
|
-
scrollOffset: this.scrollOffset,
|
|
319
|
-
viewportItemCount
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
// Create a modified child with only visible children
|
|
323
|
-
const visibleChildren = this.child.children.slice(start, end)
|
|
324
|
-
|
|
325
|
-
// Clone the child container with only visible children
|
|
326
|
-
const visibleChild = Object.create(Object.getPrototypeOf(this.child))
|
|
327
|
-
Object.assign(visibleChild, this.child)
|
|
328
|
-
visibleChild.children = visibleChildren
|
|
329
|
-
|
|
330
|
-
// Render the child with visible items
|
|
331
|
-
const childGrid = this.renderer._renderComponent(visibleChild, {
|
|
332
|
-
parent: { width, height }
|
|
333
|
-
})
|
|
334
|
-
|
|
335
|
-
if (childGrid) {
|
|
336
|
-
// Clip to viewport height
|
|
337
|
-
const clippedGrid = childGrid.slice(0, height)
|
|
338
|
-
this.renderer._mergeIntoBuffer(grid, clippedGrid, 0, 0)
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
return grid
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
_fgColorToAnsi(color) {
|
|
346
|
-
if (!color) return ''
|
|
347
|
-
const { red, green, blue } = color
|
|
348
|
-
return `\x1b[38;2;${red};${green};${blue}m`
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
_bgColorToAnsi(color) {
|
|
352
|
-
if (!color) return ''
|
|
353
|
-
const { red, green, blue } = color
|
|
354
|
-
return `\x1b[48;2;${red};${green};${blue}m`
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
_resetAnsi() {
|
|
358
|
-
return '\x1b[0m'
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
_mergeIntoBuffer(buffer, componentGrid, offsetX = 0, offsetY = 0) {
|
|
362
|
-
for (let dy = 0; dy < componentGrid.length; dy++) {
|
|
363
|
-
const targetY = offsetY + dy
|
|
364
|
-
if (targetY < 0 || targetY >= buffer.length) continue
|
|
365
|
-
|
|
366
|
-
for (let dx = 0; dx < componentGrid[dy].length; dx++) {
|
|
367
|
-
const targetX = offsetX + dx
|
|
368
|
-
if (targetX < 0 || targetX >= buffer[targetY].length) continue
|
|
369
|
-
|
|
370
|
-
const sourcePixel = componentGrid[dy][dx]
|
|
371
|
-
const targetPixel = buffer[targetY][targetX]
|
|
372
|
-
|
|
373
|
-
// Merge the pixels - preserve background if source doesn't have one
|
|
374
|
-
buffer[targetY][targetX] = {
|
|
375
|
-
char: sourcePixel.char,
|
|
376
|
-
fgColor: sourcePixel.fgColor,
|
|
377
|
-
bgColor: sourcePixel.bgColor !== null ? sourcePixel.bgColor : targetPixel.bgColor
|
|
378
|
-
}
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
_renderComponent(component, opts = {}) {
|
|
384
|
-
if (typeof component.render === 'function') {
|
|
385
|
-
component = component.render()
|
|
386
|
-
}
|
|
387
|
-
component.renderer = this
|
|
388
|
-
component.parent = opts.parent
|
|
389
|
-
const rendererFn = this.components[component.constructor.name]
|
|
390
|
-
return rendererFn.call(component)
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
registerHotkey(hotkey, fn) {
|
|
394
|
-
const key = hotkey.key
|
|
395
|
-
this.hotkeys.set(key, fn)
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
clearHotkeys() {
|
|
399
|
-
this.hotkeys.clear()
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
setup() {
|
|
403
|
-
if (this.isSetup) return
|
|
404
|
-
this.isSetup = true
|
|
405
|
-
|
|
406
|
-
start()
|
|
407
|
-
|
|
408
|
-
if (process.stdin.isTTY) {
|
|
409
|
-
process.stdin.on('data', (data) => {
|
|
410
|
-
const key = data.toString()
|
|
411
|
-
|
|
412
|
-
if (key === keys.CTRL_C || key === keys.ESC || key === 'q') {
|
|
413
|
-
// Ctrl+C
|
|
414
|
-
process.exit(0)
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
// Compare against the key string directly (not hotkey object)
|
|
418
|
-
for (const [hotkeyString, fn] of this.hotkeys.entries()) {
|
|
419
|
-
if (key === hotkeyString) {
|
|
420
|
-
fn()
|
|
421
|
-
return
|
|
422
|
-
}
|
|
423
|
-
}
|
|
424
|
-
})
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
process.on('SIGINT', exit)
|
|
428
|
-
process.on('SIGTERM', exit)
|
|
429
|
-
process.on('exit', exit)
|
|
430
|
-
|
|
431
|
-
process.stdout.on('resize', () => {
|
|
432
|
-
this.render()
|
|
433
|
-
})
|
|
434
|
-
|
|
435
|
-
goodbye(() => {
|
|
436
|
-
exit()
|
|
437
|
-
})
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
draw() {
|
|
441
|
-
this.clearHotkeys()
|
|
442
|
-
|
|
443
|
-
const buffer = this._renderComponent(this.root, {
|
|
444
|
-
parent: this.size
|
|
445
|
-
})
|
|
446
|
-
|
|
447
|
-
process.stdout.write(eraseDisplay)
|
|
448
|
-
process.stdout.write(cursorPosition(0, 0))
|
|
449
|
-
|
|
450
|
-
for (const row of buffer) {
|
|
451
|
-
for (const pixel of row) {
|
|
452
|
-
if (pixel.bgColor) {
|
|
453
|
-
process.stdout.write(this._bgColorToAnsi(pixel.bgColor))
|
|
454
|
-
}
|
|
455
|
-
if (pixel.fgColor) {
|
|
456
|
-
process.stdout.write(this._fgColorToAnsi(pixel.fgColor))
|
|
457
|
-
}
|
|
458
|
-
process.stdout.write(pixel.char)
|
|
459
|
-
if (pixel.fgColor || pixel.bgColor) {
|
|
460
|
-
process.stdout.write(this._resetAnsi())
|
|
461
|
-
}
|
|
462
|
-
}
|
|
463
|
-
process.stdout.write('\n')
|
|
464
|
-
}
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
render(component) {
|
|
468
|
-
if (component) {
|
|
469
|
-
this.root = component
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
this.size = { width: process.stdout.columns, height: process.stdout.rows }
|
|
473
|
-
|
|
474
|
-
this.setup()
|
|
475
|
-
|
|
476
|
-
this.draw()
|
|
477
|
-
}
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
module.exports = { CelleryRendererTUI }
|