@hraness/dawg 0.6.1 → 0.8.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/CHANGELOG.md +118 -0
- package/DAWG.md +454 -140
- package/README.md +30 -26
- package/core/autotune.ts +1119 -0
- package/core/chords.ts +271 -23
- package/core/clips.ts +499 -0
- package/core/diff.ts +184 -104
- package/core/expression.ts +15 -0
- package/core/fx.ts +99 -29
- package/core/ids.ts +52 -0
- package/core/instruments.ts +19 -0
- package/core/keys.ts +3 -3
- package/core/loop.ts +8 -0
- package/core/lyrics.ts +297 -0
- package/core/master.ts +3 -3
- package/core/range.ts +581 -0
- package/core/resonators.ts +16 -2
- package/core/routing.ts +165 -0
- package/core/score.ts +955 -14
- package/core/sdk/eval-child.ts +7 -2
- package/core/sdk/eval.ts +35 -6
- package/core/sdk/print.ts +287 -5
- package/core/sdk/sync-lyrics.ts +49 -0
- package/core/sdk/v1.ts +2132 -46
- package/core/sections.ts +480 -35
- package/core/sing.ts +815 -0
- package/core/style-provenance.ts +80 -0
- package/core/styles/africa-mena-southasia.ts +2893 -0
- package/core/styles/americas.ts +3810 -0
- package/core/styles/art.ts +4993 -0
- package/core/styles/base.ts +123 -0
- package/core/styles/cycles.ts +106 -0
- package/core/styles/electronic.ts +2723 -0
- package/core/styles/europe-asia-pacific.ts +2838 -0
- package/core/styles/excerpt.ts +29 -0
- package/core/styles/gamelan.ts +283 -0
- package/core/styles/generate.ts +2199 -0
- package/core/styles/index.ts +515 -0
- package/core/styles/parts.ts +106 -0
- package/core/styles/pop.ts +3189 -0
- package/core/styles/rock.ts +2993 -0
- package/core/styles/roots.ts +4175 -0
- package/core/styles/schema.ts +429 -0
- package/core/styles/taxonomy.ts +940 -0
- package/core/styles/validate.ts +528 -0
- package/core/tempo.ts +32 -2
- package/core/tuning.ts +19 -3
- package/core/vocoder.ts +524 -0
- package/guides/agent.md +29 -0
- package/guides/arrange.md +31 -0
- package/guides/audio.md +31 -0
- package/guides/audition.md +20 -14
- package/guides/automation.md +12 -7
- package/guides/chords.md +15 -15
- package/guides/effects.md +17 -16
- package/guides/faders.md +20 -16
- package/guides/files.md +13 -9
- package/guides/getting-started.md +15 -11
- package/guides/keys.md +19 -15
- package/guides/media.md +17 -12
- package/guides/mix.md +15 -7
- package/guides/music.md +23 -8
- package/guides/notes.md +15 -9
- package/guides/panes.md +32 -0
- package/guides/performance.md +15 -13
- package/guides/play.md +21 -13
- package/guides/project.md +26 -8
- package/guides/providers.md +20 -14
- package/guides/resample.md +15 -9
- package/guides/rhythm.md +18 -13
- package/guides/sessions.md +17 -7
- package/guides/show-me.md +31 -0
- package/guides/sound.md +25 -9
- package/guides/sounds.md +15 -13
- package/guides/styles.md +31 -0
- package/guides/tape.md +32 -0
- package/guides/tempo.md +15 -10
- package/guides/tracks.md +15 -10
- package/guides/tuning.md +32 -0
- package/guides/voice.md +31 -0
- package/guides/web-search.md +18 -8
- package/native/prebuilt/darwin-arm64/libdawg_sink.dylib +0 -0
- package/native/prebuilt/darwin-x64/libdawg_sink.dylib +0 -0
- package/native/prebuilt/linux-arm64/libdawg_sink.so +0 -0
- package/native/prebuilt/linux-x64/libdawg_sink.so +0 -0
- package/native/prebuilt/manifest.json +21 -0
- package/package.json +6 -2
- package/src/agent/agent.ts +130 -18
- package/src/agent/calibration-tools.ts +53 -0
- package/src/agent/clip-tools.ts +453 -0
- package/src/agent/command-agent.ts +378 -0
- package/src/agent/drum-tools.ts +2 -2
- package/src/agent/expression-tools.ts +1 -1
- package/src/agent/gateway.ts +246 -60
- package/src/agent/models.ts +53 -12
- package/src/agent/ops.ts +27 -1
- package/src/agent/pack-tools.ts +1 -1
- package/src/agent/planner.ts +25 -0
- package/src/agent/portable-schema.ts +80 -0
- package/src/agent/preview-tool.ts +4 -1
- package/src/agent/provider.ts +22 -8
- package/src/agent/range-tools.ts +216 -0
- package/src/agent/rhythm-tools.ts +1 -1
- package/src/agent/section-tools.ts +1 -1
- package/src/agent/show-me.ts +497 -0
- package/src/agent/steer.ts +15 -0
- package/src/agent/style-tools.ts +217 -0
- package/src/agent/tool-error.ts +12 -0
- package/src/agent/tools.ts +110 -23
- package/src/agent/usage.ts +2 -2
- package/src/agent/voice-tools.ts +925 -0
- package/src/agent/xcb-agent.ts +13 -10
- package/src/argv.ts +38 -0
- package/src/audio/analysis.ts +253 -0
- package/src/audio/arrange.ts +49 -5
- package/src/audio/audio-command.ts +218 -0
- package/src/audio/autotune-engine.ts +101 -0
- package/src/audio/autotune.ts +640 -0
- package/src/audio/clips.ts +240 -0
- package/src/audio/devices.ts +264 -0
- package/src/audio/doctor.ts +157 -0
- package/src/audio/dsp/bandbank.ts +138 -0
- package/src/audio/dsp/envelope.ts +10 -0
- package/src/audio/dsp/follow.ts +120 -0
- package/src/audio/dsp/formant.ts +427 -0
- package/src/audio/dsp/glottal.ts +243 -0
- package/src/audio/dsp/interp.ts +7 -2
- package/src/audio/dsp/lpc.ts +50 -0
- package/src/audio/dsp/periodicity.ts +59 -0
- package/src/audio/dsp/pitch.ts +995 -0
- package/src/audio/dsp/psola.ts +199 -0
- package/src/audio/effects/chain.ts +3 -1
- package/src/audio/effects/common.ts +43 -0
- package/src/audio/effects/convolution.ts +7 -4
- package/src/audio/effects/filter.ts +48 -69
- package/src/audio/effects/formant.ts +263 -0
- package/src/audio/engine.ts +360 -40
- package/src/audio/fit.ts +35 -3
- package/src/audio/instrument-check.ts +59 -43
- package/src/audio/instruments.ts +4 -0
- package/src/audio/keys/calibration.ts +56 -0
- package/src/audio/keys/electric.ts +8 -1
- package/src/audio/keys/engine.ts +13 -1
- package/src/audio/keys/piano.ts +22 -2
- package/src/audio/kits.ts +135 -6
- package/src/audio/live.ts +114 -20
- package/src/audio/native.ts +615 -0
- package/src/audio/preview.ts +30 -2
- package/src/audio/render-worker.ts +2 -0
- package/src/audio/renderer.ts +2 -0
- package/src/audio/resample.ts +2 -1
- package/src/audio/sampler.ts +85 -4
- package/src/audio/samples.ts +20 -2
- package/src/audio/sing/analysis.ts +193 -0
- package/src/audio/sing/engine.ts +949 -0
- package/src/audio/strings/bow.ts +48 -5
- package/src/audio/strings/engine.ts +8 -2
- package/src/audio/synth/oscillators.ts +31 -21
- package/src/audio/synth/voice.ts +34 -1
- package/src/audio/vocoder/bank.ts +314 -0
- package/src/audio/vocoder/carrier.ts +165 -0
- package/src/audio/vocoder/control.ts +68 -0
- package/src/audio/vocoder/detect.ts +50 -0
- package/src/audio/vocoder/index.ts +304 -0
- package/src/audio/vocoder/talkbox.ts +143 -0
- package/src/audio/wav.ts +500 -77
- package/src/audio/winds/engine.ts +5 -1
- package/src/audio/winds/trim.ts +28 -4
- package/src/audio/winds/trims1.ts +297 -0
- package/src/audio/winds/voice.ts +15 -2
- package/src/auth/cli.ts +38 -36
- package/src/auth/credentials.ts +30 -1
- package/src/auth/login.ts +15 -9
- package/src/auth/tui.ts +19 -10
- package/src/commands/arrange.ts +82 -32
- package/src/commands/autotune.ts +421 -0
- package/src/commands/calibration.ts +74 -0
- package/src/commands/clips.ts +887 -0
- package/src/commands/drums.ts +3 -2
- package/src/commands/edit.ts +11 -4
- package/src/commands/expression.ts +1 -1
- package/src/commands/formant.ts +221 -0
- package/src/commands/fx.ts +101 -32
- package/src/commands/grammar.ts +560 -0
- package/src/commands/help.ts +774 -366
- package/src/commands/history.ts +139 -10
- package/src/commands/keys.ts +25 -8
- package/src/commands/modal.ts +1 -1
- package/src/commands/music.ts +1 -1
- package/src/commands/nearest.ts +53 -0
- package/src/commands/pack.ts +9 -2
- package/src/commands/param-range.ts +56 -0
- package/src/commands/parses.ts +135 -0
- package/src/commands/progression.ts +170 -0
- package/src/commands/range.ts +763 -0
- package/src/commands/resample.ts +13 -10
- package/src/commands/rhythm.ts +3 -0
- package/src/commands/rig.ts +3 -24
- package/src/commands/sample.ts +11 -1
- package/src/commands/sing.ts +474 -0
- package/src/commands/strum.ts +13 -1
- package/src/commands/style.ts +415 -0
- package/src/commands/time.ts +6 -3
- package/src/commands/tuning.ts +3 -3
- package/src/commands/vocal-pitch.ts +617 -0
- package/src/commands/vocal.ts +147 -0
- package/src/commands/vocoder.ts +627 -0
- package/src/commands/wind.ts +2 -2
- package/src/fs/durable.ts +50 -0
- package/src/identity/actor.ts +127 -0
- package/src/lang/glossary.ts +511 -0
- package/src/launch-args.ts +267 -0
- package/src/main.ts +2508 -308
- package/src/media/cli.ts +20 -3
- package/src/media/import.ts +3 -1
- package/src/project/check.ts +21 -2
- package/src/project/clip-pins.ts +72 -0
- package/src/project/init.ts +23 -8
- package/src/project/sync.ts +418 -86
- package/src/render.ts +20 -1
- package/src/session/client.ts +107 -3
- package/src/session/clipboard.ts +79 -0
- package/src/session/daemon.ts +199 -5
- package/src/session/live-host.ts +232 -0
- package/src/session/meta.ts +14 -0
- package/src/session/origin.ts +174 -0
- package/src/session/port.ts +109 -14
- package/src/session/presence.ts +34 -4
- package/src/session/protocol.ts +365 -7
- package/src/session/rebase.ts +63 -12
- package/src/session/receipt.ts +265 -0
- package/src/session/shared-live.ts +131 -0
- package/src/session/store.ts +126 -45
- package/src/tui/arrange-menu.ts +225 -23
- package/src/tui/audition.ts +1 -1
- package/src/tui/euclid.ts +113 -27
- package/src/tui/fader.ts +282 -42
- package/src/tui/granular-menu.ts +2 -4
- package/src/tui/knob-fields.ts +107 -0
- package/src/tui/knob-map.ts +198 -0
- package/src/tui/menu-clips.ts +297 -0
- package/src/tui/menu-time.ts +20 -13
- package/src/tui/menu-voice.ts +405 -0
- package/src/tui/menu.ts +987 -179
- package/src/tui/modal-menu.ts +6 -6
- package/src/tui/performance-menu.ts +5 -2
- package/src/tui/play-chords.ts +4 -2
- package/src/tui/play-mode.ts +15 -1
- package/src/tui/play-session.ts +243 -28
- package/src/tui/sing-menu.ts +278 -0
- package/src/tui/style-menu.ts +104 -0
- package/src/tui/tape-mode.ts +580 -0
- package/src/tui/tape-view.ts +263 -0
- package/src/tui/vocoder-menu.ts +244 -0
- package/src/tui/wind-menu.ts +3 -3
- package/src/version.ts +8 -0
- package/src/web/fetch.ts +115 -29
- package/tui/activity.ts +180 -9
- package/tui/app.ts +491 -84
- package/tui/clip-row.ts +132 -0
- package/tui/delight.ts +193 -0
- package/tui/drawer.ts +233 -27
- package/tui/frame-gate.ts +76 -0
- package/tui/grammar.ts +143 -65
- package/tui/guide.ts +50 -5
- package/tui/highway.ts +309 -30
- package/tui/hints.ts +197 -0
- package/tui/hits.ts +7 -1
- package/tui/input.ts +60 -9
- package/tui/keys.ts +1 -1
- package/tui/knobs.ts +268 -0
- package/tui/play-strip.ts +80 -14
- package/tui/prompt.ts +1 -1
- package/tui/screen.ts +151 -18
- package/tui/tape.ts +439 -0
- package/tui/text.ts +35 -2
- package/tui/theme.ts +46 -1
package/tui/screen.ts
CHANGED
|
@@ -8,7 +8,12 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { graphemes, graphemeWidth } from "./text.ts";
|
|
11
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
foregroundSgr,
|
|
13
|
+
styleSgr,
|
|
14
|
+
type Style,
|
|
15
|
+
type TerminalCapabilities,
|
|
16
|
+
} from "./theme.ts";
|
|
12
17
|
|
|
13
18
|
export interface Cell {
|
|
14
19
|
/** Grapheme text; "" marks the trailing half of a wide character. */
|
|
@@ -17,6 +22,8 @@ export interface Cell {
|
|
|
17
22
|
}
|
|
18
23
|
|
|
19
24
|
const EMPTY: Style = {};
|
|
25
|
+
/** Unchanged cells that split a row into two writes (a CUP is ~8 bytes). */
|
|
26
|
+
const SPAN_GAP = 6;
|
|
20
27
|
|
|
21
28
|
export class CellBuffer {
|
|
22
29
|
readonly width: number;
|
|
@@ -26,10 +33,10 @@ export class CellBuffer {
|
|
|
26
33
|
constructor(width: number, height: number, fill: Style = EMPTY) {
|
|
27
34
|
this.width = Math.max(0, Math.floor(width));
|
|
28
35
|
this.height = Math.max(0, Math.floor(height));
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
36
|
+
const count = this.width * this.height;
|
|
37
|
+
this.cells = new Array<Cell>(count);
|
|
38
|
+
for (let index = 0; index < count; index += 1)
|
|
39
|
+
this.cells[index] = { ch: " ", style: fill };
|
|
33
40
|
}
|
|
34
41
|
|
|
35
42
|
get(x: number, y: number): Cell | undefined {
|
|
@@ -107,6 +114,77 @@ export class CellBuffer {
|
|
|
107
114
|
return rows;
|
|
108
115
|
}
|
|
109
116
|
|
|
117
|
+
/**
|
|
118
|
+
* The columns `[from, to)` of row `y` that differ from `other`, widened so
|
|
119
|
+
* neither edge splits a wide character, or undefined when the row matches.
|
|
120
|
+
*/
|
|
121
|
+
rowSpan(
|
|
122
|
+
other: CellBuffer,
|
|
123
|
+
y: number,
|
|
124
|
+
): Readonly<{ from: number; to: number }> | undefined {
|
|
125
|
+
if (other.width !== this.width) return { from: 0, to: this.width };
|
|
126
|
+
const offset = y * this.width;
|
|
127
|
+
const differs = (x: number) => {
|
|
128
|
+
const left = this.cells[offset + x]!;
|
|
129
|
+
const right = other.cells[offset + x]!;
|
|
130
|
+
if (left.ch !== right.ch) return true;
|
|
131
|
+
// Two blanks look the same whatever their foreground.
|
|
132
|
+
if (isBlank(left) && isBlank(right)) return false;
|
|
133
|
+
return !sameStyle(left.style, right.style);
|
|
134
|
+
};
|
|
135
|
+
let from = 0;
|
|
136
|
+
while (from < this.width && !differs(from)) from += 1;
|
|
137
|
+
if (from === this.width) return undefined;
|
|
138
|
+
let to = this.width;
|
|
139
|
+
while (to > from && !differs(to - 1)) to -= 1;
|
|
140
|
+
// A continuation cell ("") belongs to the wide character on its left.
|
|
141
|
+
while (from > 0 && this.cells[offset + from]!.ch === "") from -= 1;
|
|
142
|
+
while (to < this.width && this.cells[offset + to]!.ch === "") to += 1;
|
|
143
|
+
return { from, to };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* The changed runs of row `y`, split wherever at least `gap` unchanged
|
|
148
|
+
* cells separate them (a cursor move is cheaper than repainting those),
|
|
149
|
+
* each widened so no edge splits a wide character.
|
|
150
|
+
*/
|
|
151
|
+
rowSpans(
|
|
152
|
+
other: CellBuffer,
|
|
153
|
+
y: number,
|
|
154
|
+
gap = SPAN_GAP,
|
|
155
|
+
): Array<{ from: number; to: number }> {
|
|
156
|
+
const whole = this.rowSpan(other, y);
|
|
157
|
+
if (!whole) return [];
|
|
158
|
+
if (other.width !== this.width) return [whole];
|
|
159
|
+
const offset = y * this.width;
|
|
160
|
+
const spans: Array<{ from: number; to: number }> = [];
|
|
161
|
+
let start = -1;
|
|
162
|
+
let lastDiff = -1;
|
|
163
|
+
for (let x = whole.from; x < whole.to; x += 1) {
|
|
164
|
+
const left = this.cells[offset + x]!;
|
|
165
|
+
const right = other.cells[offset + x]!;
|
|
166
|
+
const changed =
|
|
167
|
+
left.ch !== right.ch ||
|
|
168
|
+
(!(isBlank(left) && isBlank(right)) &&
|
|
169
|
+
!sameStyle(left.style, right.style));
|
|
170
|
+
if (!changed) continue;
|
|
171
|
+
if (start < 0) start = x;
|
|
172
|
+
else if (x - lastDiff - 1 >= gap) {
|
|
173
|
+
spans.push({ from: start, to: lastDiff + 1 });
|
|
174
|
+
start = x;
|
|
175
|
+
}
|
|
176
|
+
lastDiff = x;
|
|
177
|
+
}
|
|
178
|
+
if (start >= 0) spans.push({ from: start, to: lastDiff + 1 });
|
|
179
|
+
for (const span of spans) {
|
|
180
|
+
while (span.from > 0 && this.cells[offset + span.from]!.ch === "")
|
|
181
|
+
span.from -= 1;
|
|
182
|
+
while (span.to < this.width && this.cells[offset + span.to]!.ch === "")
|
|
183
|
+
span.to += 1;
|
|
184
|
+
}
|
|
185
|
+
return spans;
|
|
186
|
+
}
|
|
187
|
+
|
|
110
188
|
rowEquals(other: CellBuffer, y: number): boolean {
|
|
111
189
|
if (other.width !== this.width) return false;
|
|
112
190
|
const offset = y * this.width;
|
|
@@ -122,6 +200,9 @@ export class CellBuffer {
|
|
|
122
200
|
private clearWide(x: number, y: number): void {
|
|
123
201
|
const offset = y * this.width;
|
|
124
202
|
const cell = this.cells[offset + x]!;
|
|
203
|
+
// Fast path: one UTF-16 unit below U+1100 is never wide (nor a wide
|
|
204
|
+
// character's empty trail), and it is nearly every cell of a frame.
|
|
205
|
+
if (cell.ch.length === 1 && cell.ch.charCodeAt(0) < 0x1100) return;
|
|
125
206
|
if (cell.ch === "" && x > 0) {
|
|
126
207
|
const lead = this.cells[offset + x - 1]!;
|
|
127
208
|
if (graphemeWidth(lead.ch) === 2) lead.ch = " ";
|
|
@@ -155,22 +236,65 @@ function sameColor(left: Style["fg"], right: Style["fg"]): boolean {
|
|
|
155
236
|
return left.r === right.r && left.g === right.g && left.b === right.b;
|
|
156
237
|
}
|
|
157
238
|
|
|
158
|
-
/**
|
|
239
|
+
/** True when only the foreground could differ and none of it shows. */
|
|
240
|
+
function invisibleStyle(style: Style | undefined): boolean {
|
|
241
|
+
return !style || (!style.bg && !style.reverse && !style.underline);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/** A space whose style draws nothing: its foreground never shows. */
|
|
245
|
+
export function isBlank(cell: Cell): boolean {
|
|
246
|
+
return cell.ch === " " && invisibleStyle(cell.style);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Serialise one row with minimal SGR changes: a blank keeps the current
|
|
251
|
+
* colour instead of resetting, and a colour-only change writes just the
|
|
252
|
+
* foreground code. Playback rows are mostly dim rules between spaces, so
|
|
253
|
+
* this halves the bytes a frame costs.
|
|
254
|
+
*/
|
|
159
255
|
export function encodeRow(
|
|
160
256
|
buffer: CellBuffer,
|
|
161
257
|
y: number,
|
|
162
258
|
capabilities: TerminalCapabilities,
|
|
259
|
+
from = 0,
|
|
260
|
+
to = buffer.width,
|
|
261
|
+
): string {
|
|
262
|
+
return encodeSpans(buffer, y, capabilities, [{ from, to }], false);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Serialise several runs of one row; each run after the first (or every
|
|
267
|
+
* run, with `address`) starts with a cursor move. The colour state carries
|
|
268
|
+
* across runs, since a cursor move leaves it alone.
|
|
269
|
+
*/
|
|
270
|
+
export function encodeSpans(
|
|
271
|
+
buffer: CellBuffer,
|
|
272
|
+
y: number,
|
|
273
|
+
capabilities: TerminalCapabilities,
|
|
274
|
+
spans: ReadonlyArray<{ from: number; to: number }>,
|
|
275
|
+
address = true,
|
|
163
276
|
): string {
|
|
164
277
|
let out = "";
|
|
165
278
|
let current: Style | undefined | null = null;
|
|
166
|
-
for (
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
current
|
|
279
|
+
for (const [index, span] of spans.entries()) {
|
|
280
|
+
if (address || index > 0) out += `\u001b[${y + 1};${span.from + 1}H`;
|
|
281
|
+
for (let x = span.from; x < span.to; x += 1) {
|
|
282
|
+
const cell = buffer.get(x, y)!;
|
|
283
|
+
if (cell.ch === "") continue;
|
|
284
|
+
if (current !== null && isBlank(cell) && invisibleStyle(current)) {
|
|
285
|
+
out += " ";
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
288
|
+
if (current === null || !sameStyle(current, cell.style)) {
|
|
289
|
+
const next = cell.style ?? EMPTY;
|
|
290
|
+
out +=
|
|
291
|
+
(current !== null &&
|
|
292
|
+
foregroundSgr(current ?? EMPTY, next, capabilities)) ||
|
|
293
|
+
styleSgr(next, capabilities);
|
|
294
|
+
current = cell.style;
|
|
295
|
+
}
|
|
296
|
+
out += cell.ch;
|
|
172
297
|
}
|
|
173
|
-
out += cell.ch;
|
|
174
298
|
}
|
|
175
299
|
if (capabilities.attributes !== false) out += "\u001b[0m";
|
|
176
300
|
return out;
|
|
@@ -193,9 +317,10 @@ export interface CursorPosition {
|
|
|
193
317
|
}
|
|
194
318
|
|
|
195
319
|
/**
|
|
196
|
-
* Differential writer: emits cursor-addressed
|
|
197
|
-
*
|
|
198
|
-
*
|
|
320
|
+
* Differential writer: emits one cursor-addressed rewrite per changed row,
|
|
321
|
+
* covering only that row's changed run of cells. Each frame is one string
|
|
322
|
+
* wrapped in synchronized-update markers, which capable terminals use to
|
|
323
|
+
* avoid tearing and others ignore.
|
|
199
324
|
*/
|
|
200
325
|
export class ScreenWriter {
|
|
201
326
|
private previous: CellBuffer | undefined;
|
|
@@ -227,8 +352,16 @@ export class ScreenWriter {
|
|
|
227
352
|
let rows = 0;
|
|
228
353
|
if (full) body += "\u001b[0m\u001b[2J";
|
|
229
354
|
for (let y = 0; y < buffer.height; y += 1) {
|
|
230
|
-
if (
|
|
231
|
-
|
|
355
|
+
if (full) {
|
|
356
|
+
body += `\u001b[${y + 1};1H${encodeRow(buffer, y, this.capabilities)}`;
|
|
357
|
+
rows += 1;
|
|
358
|
+
continue;
|
|
359
|
+
}
|
|
360
|
+
// Only the changed run of cells: a moving playhead or a ticking meter
|
|
361
|
+
// rewrites a few columns, not the whole row.
|
|
362
|
+
const spans = buffer.rowSpans(this.previous!, y);
|
|
363
|
+
if (spans.length === 0) continue;
|
|
364
|
+
body += encodeSpans(buffer, y, this.capabilities, spans);
|
|
232
365
|
rows += 1;
|
|
233
366
|
}
|
|
234
367
|
const cursorKey = cursor ? `${cursor.x},${cursor.y}` : "hidden";
|
package/tui/tape.ts
ADDED
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TAPE (op1-ux §6, §8.3): every track across the bars, as a pure painter.
|
|
3
|
+
*
|
|
4
|
+
* The caller (src/tui/tape-view.ts) turns a score into a `TapeView`: one
|
|
5
|
+
* density level per cell and track, the sections, the loop range, the
|
|
6
|
+
* playhead and the knob slots. This module only draws it, so the layout is
|
|
7
|
+
* testable without a session. Colour is never the only cue: the playhead is
|
|
8
|
+
* `▼`/`│`, the loop `[══]`, the focused row `›`, a muted row dim `·`, a
|
|
9
|
+
* form repeat `░` (ASCII `~`), and every knob carries its shape glyph
|
|
10
|
+
* (NO_COLOR and the mono theme).
|
|
11
|
+
*/
|
|
12
|
+
import type { HitMap } from "./hits.ts";
|
|
13
|
+
import { paintKnobStrip, type KnobIndex, type KnobSlots } from "./knobs.ts";
|
|
14
|
+
import type { CellBuffer } from "./screen.ts";
|
|
15
|
+
import { displayWidth, truncate } from "./text.ts";
|
|
16
|
+
import { accentStyle, type Style, type Theme } from "./theme.ts";
|
|
17
|
+
|
|
18
|
+
/** One track row: its density levels (0 empty … 8 dense), one per cell. */
|
|
19
|
+
export type TapeRow = Readonly<{
|
|
20
|
+
id: string;
|
|
21
|
+
name: string;
|
|
22
|
+
muted: boolean;
|
|
23
|
+
levels: readonly number[];
|
|
24
|
+
/** Other panes on this track (§12.7): `B`, `C●`; empty when none. */
|
|
25
|
+
marks: string;
|
|
26
|
+
}>;
|
|
27
|
+
|
|
28
|
+
export type TapeSection = Readonly<{
|
|
29
|
+
name: string;
|
|
30
|
+
startBar: number;
|
|
31
|
+
bars: number;
|
|
32
|
+
}>;
|
|
33
|
+
|
|
34
|
+
/** Cells per bar: half beats, beats or whole bars (`-` / `=` zoom). */
|
|
35
|
+
export type TapeZoom = "half" | "beat" | "bar";
|
|
36
|
+
export const TAPE_ZOOMS: readonly TapeZoom[] = ["bar", "beat", "half"];
|
|
37
|
+
|
|
38
|
+
export type TapeView = Readonly<{
|
|
39
|
+
bars: number;
|
|
40
|
+
/** The bar (0-based) each cell belongs to, in order. */
|
|
41
|
+
cellBars: readonly number[];
|
|
42
|
+
/** Cell under the playhead. */
|
|
43
|
+
playheadCell: number;
|
|
44
|
+
/** The loop range (`score.loop`, or the looped section's bars). */
|
|
45
|
+
loop?: Readonly<{ startBar: number; bars: number }> | undefined;
|
|
46
|
+
sections: readonly TapeSection[];
|
|
47
|
+
/**
|
|
48
|
+
* An unrolled form (op1-ux §4): per cell, whether it is a later pass of a
|
|
49
|
+
* section (ghosted `░`, ASCII `~`), and which form pass it belongs to
|
|
50
|
+
* (-1 for bars the form never plays). Absent without a form.
|
|
51
|
+
*/
|
|
52
|
+
ghosts?: readonly boolean[] | undefined;
|
|
53
|
+
passes?: readonly number[] | undefined;
|
|
54
|
+
/** Each form pass's label (`chorus ×2`), indexed by `passes`. */
|
|
55
|
+
passNames?: readonly string[] | undefined;
|
|
56
|
+
/** The looped section's name, drawn reversed. */
|
|
57
|
+
loopSection?: string | undefined;
|
|
58
|
+
/** Tempo changes (`♩96` marks); empty when the tempo is constant. */
|
|
59
|
+
tempo: readonly Readonly<{ cell: number; bpm: number }>[];
|
|
60
|
+
rows: readonly TapeRow[];
|
|
61
|
+
focused: number;
|
|
62
|
+
/** `range: bass · bars 5–6 (loop)`. */
|
|
63
|
+
range: string;
|
|
64
|
+
/** `clipboard: drums · 2 bars`, when the pane holds one. */
|
|
65
|
+
clipboard?: string | undefined;
|
|
66
|
+
knobs: KnobSlots;
|
|
67
|
+
selected: KnobIndex;
|
|
68
|
+
/** The gesture row, from the screen's key table. */
|
|
69
|
+
hint: string;
|
|
70
|
+
/** The reels (`◐◓◑◒`, §9.1), turning on beats while playing. */
|
|
71
|
+
reel?: string | undefined;
|
|
72
|
+
/**
|
|
73
|
+
* The loop just wrapped (a recording pass closed, §9.3): the brackets
|
|
74
|
+
* draw reversed and bold and the fill turns `━`, so it reads in mono.
|
|
75
|
+
*/
|
|
76
|
+
loopFlash?: boolean | undefined;
|
|
77
|
+
}>;
|
|
78
|
+
|
|
79
|
+
/** Glyph for a density level, 1 … 8. */
|
|
80
|
+
const LEVELS = " ▁▂▃▄▅▆▇█";
|
|
81
|
+
const ASCII_LEVELS = " .:-=+*#@";
|
|
82
|
+
/** A later pass of a section on an unrolled form. */
|
|
83
|
+
const GHOST = "░";
|
|
84
|
+
const ASCII_GHOST = "~";
|
|
85
|
+
|
|
86
|
+
/** Columns of the row gutter: ` 2›bass `. */
|
|
87
|
+
export const TAPE_GUTTER = 9;
|
|
88
|
+
|
|
89
|
+
/** The rows the tape keeps below the tracks: range, knobs, hints. */
|
|
90
|
+
const FOOTER_ROWS = 3;
|
|
91
|
+
|
|
92
|
+
/** First visible cell: pages so the playhead stays on screen. */
|
|
93
|
+
export function tapeFirstCell(view: TapeView, cells: number): number {
|
|
94
|
+
if (view.cellBars.length <= cells) return 0;
|
|
95
|
+
const page = Math.floor(view.playheadCell / cells);
|
|
96
|
+
return Math.min(page * cells, view.cellBars.length - cells);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** First visible track row: keeps the focused row on screen. */
|
|
100
|
+
export function tapeFirstRow(view: TapeView, rows: number): number {
|
|
101
|
+
if (view.rows.length <= rows) return 0;
|
|
102
|
+
return Math.max(
|
|
103
|
+
0,
|
|
104
|
+
Math.min(view.focused - rows + 1, view.rows.length - rows),
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export type TapePaintOptions = Readonly<{
|
|
109
|
+
theme: Theme;
|
|
110
|
+
unicode: boolean;
|
|
111
|
+
hits?: HitMap | undefined;
|
|
112
|
+
}>;
|
|
113
|
+
|
|
114
|
+
/** What the painter laid out, for mouse hits and tests. */
|
|
115
|
+
export type TapeLayout = Readonly<{
|
|
116
|
+
rulerY: number;
|
|
117
|
+
firstCell: number;
|
|
118
|
+
cells: number;
|
|
119
|
+
firstRow: number;
|
|
120
|
+
rowsY: number;
|
|
121
|
+
rowCount: number;
|
|
122
|
+
}>;
|
|
123
|
+
|
|
124
|
+
export function paintTape(
|
|
125
|
+
buffer: CellBuffer,
|
|
126
|
+
rect: Readonly<{ x: number; y: number; width: number; height: number }>,
|
|
127
|
+
view: TapeView,
|
|
128
|
+
options: TapePaintOptions,
|
|
129
|
+
): TapeLayout {
|
|
130
|
+
const { theme, unicode } = options;
|
|
131
|
+
const roles = theme.roles;
|
|
132
|
+
// ` B C●`: room for up to three pane letters right of the cells.
|
|
133
|
+
const marksWidth = view.rows.some((row) => row.marks) ? 7 : 0;
|
|
134
|
+
const cells = Math.max(
|
|
135
|
+
0,
|
|
136
|
+
Math.min(view.cellBars.length, rect.width - TAPE_GUTTER - marksWidth),
|
|
137
|
+
);
|
|
138
|
+
const firstCell = tapeFirstCell(view, cells);
|
|
139
|
+
const left = rect.x + TAPE_GUTTER;
|
|
140
|
+
const bottom = rect.y + rect.height;
|
|
141
|
+
const cellBar = (cell: number): number => view.cellBars[cell] ?? view.bars;
|
|
142
|
+
const ghost = (cell: number): boolean => view.ghosts?.[cell] ?? false;
|
|
143
|
+
const passOf = (cell: number): number | undefined => view.passes?.[cell];
|
|
144
|
+
const barStart = (cell: number): boolean =>
|
|
145
|
+
cell === 0 ||
|
|
146
|
+
cellBar(cell) !== cellBar(cell - 1) ||
|
|
147
|
+
passOf(cell) !== passOf(cell - 1);
|
|
148
|
+
const loopStart = view.loop?.startBar;
|
|
149
|
+
const loopEnd = view.loop ? view.loop.startBar + view.loop.bars : undefined;
|
|
150
|
+
// The loop brackets its source bars, never a ghost pass of them.
|
|
151
|
+
const inLoop = (cell: number): boolean =>
|
|
152
|
+
loopStart !== undefined &&
|
|
153
|
+
!ghost(cell) &&
|
|
154
|
+
cellBar(cell) >= loopStart &&
|
|
155
|
+
cellBar(cell) < loopEnd!;
|
|
156
|
+
|
|
157
|
+
// Ruler: bar numbers every 4 bars and at the loop edges; `[══]` in green.
|
|
158
|
+
let y = rect.y;
|
|
159
|
+
const ruler = Array.from({ length: cells }, () => " ");
|
|
160
|
+
const rulerStyle: (Style | undefined)[] = Array.from(
|
|
161
|
+
{ length: cells },
|
|
162
|
+
() => roles.muted,
|
|
163
|
+
);
|
|
164
|
+
const flash = view.loopFlash === true;
|
|
165
|
+
const bracketStyle: Style = flash
|
|
166
|
+
? { ...roles.knob2, bold: true, reverse: true }
|
|
167
|
+
: roles.knob2;
|
|
168
|
+
for (let index = 0; index < cells; index += 1) {
|
|
169
|
+
const cell = firstCell + index;
|
|
170
|
+
if (inLoop(cell)) {
|
|
171
|
+
ruler[index] = flash ? (unicode ? "━" : "#") : unicode ? "═" : "=";
|
|
172
|
+
rulerStyle[index] = flash ? { ...roles.knob2, bold: true } : roles.knob2;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
for (let index = 0; index < cells; index += 1) {
|
|
176
|
+
const cell = firstCell + index;
|
|
177
|
+
if (!barStart(cell)) continue;
|
|
178
|
+
const bar = cellBar(cell);
|
|
179
|
+
const edge = !ghost(cell) && (bar === loopStart || bar + 1 === loopEnd);
|
|
180
|
+
if (bar % 4 !== 0 && !edge && bar !== view.bars - 1) continue;
|
|
181
|
+
const label = String(bar + 1);
|
|
182
|
+
const at = index + (edge && bar === loopStart ? 1 : 0);
|
|
183
|
+
// A number never overwrites a neighbour's digits.
|
|
184
|
+
if (ruler.slice(at, at + label.length).some((ch) => /\d/.test(ch)))
|
|
185
|
+
continue;
|
|
186
|
+
for (let k = 0; k < label.length && at + k < cells; k += 1) {
|
|
187
|
+
ruler[at + k] = label[k]!;
|
|
188
|
+
if (!inLoop(cell)) rulerStyle[at + k] = roles.muted;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
if (view.loop) {
|
|
192
|
+
for (let index = 0; index < cells; index += 1) {
|
|
193
|
+
const cell = firstCell + index;
|
|
194
|
+
if (barStart(cell) && cellBar(cell) === loopStart && !ghost(cell)) {
|
|
195
|
+
ruler[index] = "[";
|
|
196
|
+
rulerStyle[index] = bracketStyle;
|
|
197
|
+
}
|
|
198
|
+
if (inLoop(cell) && !inLoop(cell + 1)) {
|
|
199
|
+
ruler[index] = "]";
|
|
200
|
+
rulerStyle[index] = bracketStyle;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
const head = view.playheadCell - firstCell;
|
|
205
|
+
if (head >= 0 && head < cells) {
|
|
206
|
+
// On a loop bracket the bracket stays and the playhead reverses it, so
|
|
207
|
+
// both read without color (the rows still carry the `│` column).
|
|
208
|
+
if (ruler[head] === "[" || ruler[head] === "]")
|
|
209
|
+
rulerStyle[head] = { ...roles.knob1, reverse: true };
|
|
210
|
+
else {
|
|
211
|
+
ruler[head] = unicode ? "▼" : "v";
|
|
212
|
+
rulerStyle[head] = roles.knob1;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
buffer.text(rect.x, y, " bar", roles.faint);
|
|
216
|
+
// The reels sit in the gutter: a glyph that turns, so never color-only.
|
|
217
|
+
if (view.reel) buffer.text(rect.x + 6, y, view.reel, roles.warning);
|
|
218
|
+
ruler.forEach((ch, index) =>
|
|
219
|
+
buffer.set(left + index, y, ch, rulerStyle[index]),
|
|
220
|
+
);
|
|
221
|
+
const rulerY = y;
|
|
222
|
+
options.hits?.add(left, y, cells, 1, { kind: "tape-ruler", left, firstCell });
|
|
223
|
+
y += 1;
|
|
224
|
+
|
|
225
|
+
// Sections: `▀name▀▀▀`; the looped one reversed; a form repeat `░name░`.
|
|
226
|
+
if (y < bottom - FOOTER_ROWS) {
|
|
227
|
+
buffer.text(rect.x, y, " sect", roles.faint);
|
|
228
|
+
const sectionAt = (cell: number): TapeSection | undefined => {
|
|
229
|
+
const bar = cellBar(cell);
|
|
230
|
+
return view.sections.find(
|
|
231
|
+
(item) => bar >= item.startBar && bar < item.startBar + item.bars,
|
|
232
|
+
);
|
|
233
|
+
};
|
|
234
|
+
const sectionStyle = (section: TapeSection, cell: number): Style => ({
|
|
235
|
+
...accentStyle(theme, `section:${section.name}`),
|
|
236
|
+
...(ghost(cell) ? { dim: true } : {}),
|
|
237
|
+
...(section.name === view.loopSection && !ghost(cell)
|
|
238
|
+
? { reverse: true }
|
|
239
|
+
: {}),
|
|
240
|
+
});
|
|
241
|
+
for (let index = 0; index < cells; index += 1) {
|
|
242
|
+
const cell = firstCell + index;
|
|
243
|
+
const section = sectionAt(cell);
|
|
244
|
+
if (!section) {
|
|
245
|
+
buffer.set(left + index, y, " ", undefined);
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
buffer.set(
|
|
249
|
+
left + index,
|
|
250
|
+
y,
|
|
251
|
+
ghost(cell) ? (unicode ? GHOST : ASCII_GHOST) : unicode ? "▀" : "-",
|
|
252
|
+
sectionStyle(section, cell),
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
// A label at each section's first cell (each form pass when unrolled).
|
|
256
|
+
for (let index = 0; index < cells; index += 1) {
|
|
257
|
+
const cell = firstCell + index;
|
|
258
|
+
const section = sectionAt(cell);
|
|
259
|
+
if (!section) continue;
|
|
260
|
+
const pass = passOf(cell);
|
|
261
|
+
const starts =
|
|
262
|
+
cell === firstCell
|
|
263
|
+
? true
|
|
264
|
+
: sectionAt(cell - 1) !== section || pass !== passOf(cell - 1);
|
|
265
|
+
if (!starts) continue;
|
|
266
|
+
let room = 1;
|
|
267
|
+
while (
|
|
268
|
+
index + room < cells &&
|
|
269
|
+
sectionAt(cell + room) === section &&
|
|
270
|
+
passOf(cell + room) === pass
|
|
271
|
+
)
|
|
272
|
+
room += 1;
|
|
273
|
+
const name =
|
|
274
|
+
pass !== undefined && pass >= 0
|
|
275
|
+
? (view.passNames?.[pass] ?? section.name)
|
|
276
|
+
: section.name;
|
|
277
|
+
// A ghost keeps a leading `░` so the repeat reads in mono too.
|
|
278
|
+
if (ghost(cell) && room <= 2) continue;
|
|
279
|
+
const offset = ghost(cell) ? 1 : 0;
|
|
280
|
+
const label = truncate(name, room - offset, "");
|
|
281
|
+
buffer.text(left + index + offset, y, label, {
|
|
282
|
+
...sectionStyle(section, cell),
|
|
283
|
+
bold: true,
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
y += 1;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Tempo marks, only when the tempo changes.
|
|
290
|
+
if (view.tempo.length > 0 && y < bottom - FOOTER_ROWS) {
|
|
291
|
+
buffer.text(rect.x, y, " tempo", roles.faint);
|
|
292
|
+
for (const mark of view.tempo) {
|
|
293
|
+
const at = mark.cell - firstCell;
|
|
294
|
+
if (at < 0 || at >= cells) continue;
|
|
295
|
+
buffer.text(
|
|
296
|
+
left + at,
|
|
297
|
+
y,
|
|
298
|
+
truncate(`${unicode ? "♩" : "q"}${mark.bpm}`, cells - at, ""),
|
|
299
|
+
roles.knob3,
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
y += 1;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Track rows.
|
|
306
|
+
const rowsY = y;
|
|
307
|
+
const rowRoom = Math.max(0, bottom - FOOTER_ROWS - y);
|
|
308
|
+
const firstRow = tapeFirstRow(view, rowRoom);
|
|
309
|
+
const levels = unicode ? LEVELS : ASCII_LEVELS;
|
|
310
|
+
const visible = view.rows.slice(firstRow, firstRow + rowRoom);
|
|
311
|
+
visible.forEach((row, offset) => {
|
|
312
|
+
const index = firstRow + offset;
|
|
313
|
+
const focused = index === view.focused;
|
|
314
|
+
const number = index < 9 ? String(index + 1) : " ";
|
|
315
|
+
const accent = accentStyle(theme, row.id);
|
|
316
|
+
buffer.text(rect.x, y, ` ${number}`, roles.faint);
|
|
317
|
+
buffer.text(rect.x + 2, y, focused ? (unicode ? "›" : ">") : " ", {
|
|
318
|
+
...roles.text,
|
|
319
|
+
bold: true,
|
|
320
|
+
});
|
|
321
|
+
const name = truncate(row.name, 6, "").padEnd(6);
|
|
322
|
+
buffer.text(
|
|
323
|
+
rect.x + 3,
|
|
324
|
+
y,
|
|
325
|
+
name,
|
|
326
|
+
focused
|
|
327
|
+
? { ...accent, bold: true, reverse: true }
|
|
328
|
+
: row.muted
|
|
329
|
+
? roles.mutedNote
|
|
330
|
+
: accent,
|
|
331
|
+
);
|
|
332
|
+
for (let column = 0; column < cells; column += 1) {
|
|
333
|
+
const cell = firstCell + column;
|
|
334
|
+
const level = row.levels[cell] ?? 0;
|
|
335
|
+
const repeat = ghost(cell);
|
|
336
|
+
const style: Style | undefined =
|
|
337
|
+
column === head
|
|
338
|
+
? // Over a note the playhead reverses it: visible in mono too.
|
|
339
|
+
{ ...roles.knob1, ...(level > 0 ? { reverse: true } : {}) }
|
|
340
|
+
: row.muted
|
|
341
|
+
? roles.mutedNote
|
|
342
|
+
: level > 0
|
|
343
|
+
? repeat
|
|
344
|
+
? { ...accent, dim: true }
|
|
345
|
+
: accent
|
|
346
|
+
: barStart(cell)
|
|
347
|
+
? roles.barRule
|
|
348
|
+
: roles.beatRule;
|
|
349
|
+
const ch =
|
|
350
|
+
level > 0
|
|
351
|
+
? repeat
|
|
352
|
+
? unicode
|
|
353
|
+
? GHOST
|
|
354
|
+
: ASCII_GHOST
|
|
355
|
+
: levels[Math.min(8, level)]!
|
|
356
|
+
: column === head
|
|
357
|
+
? unicode
|
|
358
|
+
? "│"
|
|
359
|
+
: "|"
|
|
360
|
+
: barStart(cell)
|
|
361
|
+
? unicode
|
|
362
|
+
? "┊"
|
|
363
|
+
: ":"
|
|
364
|
+
: "·";
|
|
365
|
+
buffer.set(left + column, y, ch, style);
|
|
366
|
+
}
|
|
367
|
+
// Other panes, dim; a recording pane's `●` in the record red.
|
|
368
|
+
[
|
|
369
|
+
...truncate(
|
|
370
|
+
unicode ? row.marks : row.marks.replace(/●/g, "*"),
|
|
371
|
+
marksWidth - 1,
|
|
372
|
+
"",
|
|
373
|
+
),
|
|
374
|
+
].forEach((ch, k) =>
|
|
375
|
+
buffer.set(
|
|
376
|
+
left + cells + 1 + k,
|
|
377
|
+
y,
|
|
378
|
+
ch,
|
|
379
|
+
ch === "●" || ch === "*" ? roles.error : { ...roles.muted, dim: true },
|
|
380
|
+
),
|
|
381
|
+
);
|
|
382
|
+
options.hits?.add(rect.x, y, rect.width, 1, {
|
|
383
|
+
kind: "tape-row",
|
|
384
|
+
row: index,
|
|
385
|
+
left,
|
|
386
|
+
firstCell,
|
|
387
|
+
});
|
|
388
|
+
y += 1;
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
// Footer: range line with the clipboard chip, knob strip, gesture hints.
|
|
392
|
+
const footerY = bottom - FOOTER_ROWS;
|
|
393
|
+
if (footerY >= rowsY) {
|
|
394
|
+
buffer.text(
|
|
395
|
+
rect.x + 1,
|
|
396
|
+
footerY,
|
|
397
|
+
truncate(view.range, rect.width - 2),
|
|
398
|
+
roles.text,
|
|
399
|
+
);
|
|
400
|
+
if (view.clipboard) {
|
|
401
|
+
const chip = `${unicode ? "◆" : "*"} ${view.clipboard}`;
|
|
402
|
+
const room = rect.width - 2 - displayWidth(view.range) - 3;
|
|
403
|
+
if (room >= 12) {
|
|
404
|
+
const text = truncate(chip, room);
|
|
405
|
+
const x = rect.x + rect.width - 1 - displayWidth(text);
|
|
406
|
+
buffer.text(x, footerY, text, roles.warning);
|
|
407
|
+
options.hits?.add(x, footerY, displayWidth(text), 1, {
|
|
408
|
+
kind: "tape-clipboard",
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
paintKnobStrip(
|
|
413
|
+
buffer,
|
|
414
|
+
rect.x + 1,
|
|
415
|
+
footerY + 1,
|
|
416
|
+
rect.width - 2,
|
|
417
|
+
view.knobs,
|
|
418
|
+
view.selected,
|
|
419
|
+
{
|
|
420
|
+
theme,
|
|
421
|
+
unicode,
|
|
422
|
+
},
|
|
423
|
+
);
|
|
424
|
+
buffer.text(
|
|
425
|
+
rect.x + 1,
|
|
426
|
+
footerY + 2,
|
|
427
|
+
truncate(view.hint, rect.width - 2),
|
|
428
|
+
roles.muted,
|
|
429
|
+
);
|
|
430
|
+
}
|
|
431
|
+
return {
|
|
432
|
+
rulerY,
|
|
433
|
+
firstCell,
|
|
434
|
+
cells,
|
|
435
|
+
firstRow,
|
|
436
|
+
rowsY,
|
|
437
|
+
rowCount: visible.length,
|
|
438
|
+
};
|
|
439
|
+
}
|