@trendr/core 0.4.10 → 0.4.12

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.4.10",
3
+ "version": "0.4.12",
4
4
  "description": "direct-mode TUI renderer with JSX, signals, and per-cell diffing",
5
5
  "license": "MIT",
6
6
  "author": "Jonathan Pyers",
package/src/ansi.js CHANGED
@@ -19,6 +19,9 @@ export const sgrReset = `${ESC}0m`
19
19
  export const setTitle = (title) => `\x1b]2;${title}\x07`
20
20
  export const osc52Copy = (text) => `\x1b]52;c;${Buffer.from(text, 'utf8').toString('base64')}\x07`
21
21
 
22
+ export const beginSync = `${ESC}?2026h`
23
+ export const endSync = `${ESC}?2026l`
24
+
22
25
  export const enableMouse = `${ESC}?1003h${ESC}?1006h`
23
26
  export const disableMouse = `${ESC}?1003l${ESC}?1006l`
24
27
 
@@ -28,6 +31,9 @@ export const ITALIC = 4
28
31
  export const UNDERLINE = 8
29
32
  export const INVERSE = 16
30
33
  export const STRIKETHROUGH = 32
34
+ // never emitted as sgr: marks ui chrome cells (scrollbars, counters) that
35
+ // text selection should skip when extracting copy text
36
+ export const COPY_IGNORE = 64
31
37
 
32
38
  const ATTR_CODES = [
33
39
  [BOLD, '1'],
package/src/buffer.js CHANGED
@@ -159,5 +159,8 @@ export function blitRect(src, dst, x, y, w, h) {
159
159
  for (let col = x1; col < x2; col++) {
160
160
  dst.cells[base + col] = src.cells[base + col]
161
161
  }
162
+ // softWrap is per-row metadata set at text paint time; a blitted clean
163
+ // subtree must carry its wrap flags or selection re-joins lines wrongly
164
+ if (src.softWrap[row]) dst.softWrap[row] = 1
162
165
  }
163
166
  }
package/src/diff-view.js CHANGED
@@ -236,7 +236,7 @@ export function Diff({
236
236
  const isThumb = i >= thumbStart && i < thumbStart + thumbH
237
237
  bar.push(jsx('text', {
238
238
  key: i,
239
- style: { color: isThumb ? palette.lineNo : palette.lineNo, dim: !isThumb },
239
+ style: { color: palette.lineNo, dim: !isThumb, copyIgnore: true },
240
240
  children: isThumb ? '█' : '│',
241
241
  }))
242
242
  }
package/src/dropdown.js CHANGED
@@ -126,7 +126,7 @@ export function Dropdown({ items, cursor, scroll, visibleCount, width, onSubmit,
126
126
  children: [
127
127
  content,
128
128
  jsx('text', {
129
- style: { color: barIsThumb ? s.accent : muted, dim: !barIsThumb },
129
+ style: { color: barIsThumb ? s.accent : muted, dim: !barIsThumb, copyIgnore: true },
130
130
  children: ' ' + (barIsThumb ? '█' : '│'),
131
131
  }),
132
132
  ],
package/src/list.js CHANGED
@@ -190,7 +190,7 @@ export function List({ items = [], selected: selectedProp, onSelect, onCursorCha
190
190
  barChildren.push(
191
191
  jsx('text', {
192
192
  key: i,
193
- style: { color: isThumb ? (focused ? accent : muted) : muted, dim: !isThumb },
193
+ style: { color: isThumb ? (focused ? accent : muted) : muted, dim: !isThumb, copyIgnore: true },
194
194
  children: isThumb ? '\u2588' : '\u2502',
195
195
  })
196
196
  )
package/src/renderer.js CHANGED
@@ -98,6 +98,7 @@ function resolveAttrs(style) {
98
98
  if (style.underline) attrs |= ansi.UNDERLINE
99
99
  if (style.inverse) attrs |= ansi.INVERSE
100
100
  if (style.strikethrough) attrs |= ansi.STRIKETHROUGH
101
+ if (style.copyIgnore) attrs |= ansi.COPY_IGNORE
101
102
  return attrs
102
103
  }
103
104
 
@@ -1068,10 +1069,13 @@ export function mount(rootComponent, { stream, stdin, title, theme, onExit: onEx
1068
1069
 
1069
1070
  const { output, changed } = diff(prev, curr)
1070
1071
  if (changed > 0) {
1071
- out.write(ansi.hideCursor)
1072
+ // synchronized output (dec 2026): supporting terminals apply the whole
1073
+ // frame atomically instead of tearing mid-write; others ignore it
1074
+ out.write(ansi.beginSync + ansi.hideCursor)
1072
1075
  // diff() returns a view into a shared double buffer that gets reused two
1073
1076
  // frames later; write a copy so backpressured streams never see it mutate
1074
1077
  out.write(Buffer.from(output))
1078
+ out.write(ansi.endSync)
1075
1079
  }
1076
1080
 
1077
1081
  const now = performance.now()
@@ -1226,6 +1230,10 @@ export function mount(rootComponent, { stream, stdin, title, theme, onExit: onEx
1226
1230
  }
1227
1231
 
1228
1232
  ctx.repaint = repaint
1233
+ // gentle sibling of repaint: schedules a normal diffed frame with no
1234
+ // clear-screen; right for overlay state like selection that frame()
1235
+ // already applies each pass
1236
+ ctx.requestFrame = () => scheduler.forceFrame()
1229
1237
  ctx.getPaintBuffer = () => (inline ? lastInlineBuf : prev)
1230
1238
 
1231
1239
  return { unmount, repaint, setTheme, getBuffer: ctx.getPaintBuffer }
package/src/scroll-box.js CHANGED
@@ -93,7 +93,7 @@ export function ScrollBox({ children, focused = true, scrollOffset: offsetProp,
93
93
  barChildren.push(
94
94
  jsx('text', {
95
95
  key: i,
96
- style: { color: isThumb ? (focused ? accent : muted) : muted, dim: !isThumb },
96
+ style: { color: isThumb ? (focused ? accent : muted) : muted, dim: !isThumb, copyIgnore: true },
97
97
  children: isThumb ? thumbChar : trackChar,
98
98
  })
99
99
  )
@@ -89,7 +89,7 @@ export function ScrollableText({ content = '', focused = true, scrollOffset: off
89
89
  style: { flexDirection: 'row', height: 1 },
90
90
  children: [
91
91
  jsx('text', { style: { flexGrow: 1, ...textStyle }, children: line || ' ' }),
92
- jsx('text', { style: { color: barColor, dim: !isThumb }, children: ' ' + barChar }),
92
+ jsx('text', { style: { color: barColor, dim: !isThumb, copyIgnore: true }, children: ' ' + barChar }),
93
93
  ],
94
94
  })
95
95
  })
package/src/selection.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { useMouse } from './hooks.js'
2
2
  import { getContext, registerHook } from './renderer.js'
3
- import { osc52Copy } from './ansi.js'
3
+ import { osc52Copy, COPY_IGNORE } from './ansi.js'
4
4
 
5
5
  function normalize(a, b) {
6
6
  const forward = a.y < b.y || (a.y === b.y && a.x <= b.x)
@@ -22,8 +22,10 @@ export function extractSelectionText(buf, sel) {
22
22
  const to = y === sel.ey ? Math.min(sel.ex, buf.width - 1) : buf.width - 1
23
23
  let text = ''
24
24
  for (let x = 0; x <= to; x++) {
25
- const ch = buf.cells[y * buf.width + x].ch
26
- if (ch === '') continue
25
+ const cell = buf.cells[y * buf.width + x]
26
+ if (cell.ch === '') continue
27
+ // chrome cells (scrollbars, counters) never belong in copied text
28
+ const ch = cell.attrs & COPY_IGNORE ? ' ' : cell.ch
27
29
  text += y === sel.sy && x < sel.sx ? ' ' : ch
28
30
  }
29
31
  rows.push({ text: text.replace(/\s+$/, ''), soft: !!buf.softWrap[y] })
@@ -63,7 +65,7 @@ export function useSelection({ onCopy, copy = true } = {}) {
63
65
  state.dragging = false
64
66
  if (ctx.selection) {
65
67
  ctx.selection = null
66
- ctx.repaint()
68
+ ctx.requestFrame()
67
69
  }
68
70
  return
69
71
  }
@@ -71,7 +73,7 @@ export function useSelection({ onCopy, copy = true } = {}) {
71
73
  if (event.action === 'drag' && state.anchor) {
72
74
  state.dragging = true
73
75
  ctx.selection = normalize(state.anchor, { x: event.x, y: event.y })
74
- ctx.repaint()
76
+ ctx.requestFrame()
75
77
  return
76
78
  }
77
79
 
@@ -79,7 +81,7 @@ export function useSelection({ onCopy, copy = true } = {}) {
79
81
  if (state.dragging && ctx.selection) {
80
82
  const text = extractSelectionText(ctx.getPaintBuffer(), ctx.selection)
81
83
  ctx.selection = null
82
- ctx.repaint()
84
+ ctx.requestFrame()
83
85
  if (text) {
84
86
  if (copy) ctx.stream.write(osc52Copy(text))
85
87
  if (onCopy) onCopy(text)
package/src/text-area.js CHANGED
@@ -372,7 +372,7 @@ export function TextArea({ onSubmit, onCancel, onChange, onKeyDown, placeholder,
372
372
  const counter = counterActive
373
373
  ? jsx('box', {
374
374
  style: { flexDirection: 'row', height: 1 },
375
- children: jsx('text', { style: { color: muted, dim: true }, children: `${displayPos.row + 1}/${lineMap.length}` }),
375
+ children: jsx('text', { style: { color: muted, dim: true, copyIgnore: true }, children: `${displayPos.row + 1}/${lineMap.length}` }),
376
376
  })
377
377
  : null
378
378