@hraness/dawg 0.6.0 → 0.7.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 +126 -0
- package/DAWG.md +655 -141
- package/README.md +27 -25
- package/core/autotune.ts +1119 -0
- package/core/chords.ts +759 -23
- package/core/clips.ts +499 -0
- package/core/diff.ts +182 -99
- package/core/expression.ts +158 -6
- package/core/fx.ts +354 -32
- package/core/granular.ts +91 -0
- package/core/instruments.ts +180 -0
- package/core/keys.ts +573 -4
- package/core/loop.ts +5 -0
- package/core/lyrics.ts +297 -0
- package/core/master.ts +3 -3
- package/core/resonators.ts +306 -4
- package/core/routing.ts +165 -0
- package/core/score.ts +1181 -16
- package/core/sdk/eval-child.ts +7 -2
- package/core/sdk/eval.ts +35 -6
- package/core/sdk/print.ts +354 -6
- package/core/sdk/sync-lyrics.ts +49 -0
- package/core/sdk/v1.ts +3160 -69
- package/core/sections.ts +298 -24
- package/core/sing.ts +815 -0
- package/core/strings.ts +237 -2
- 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/core/winds.ts +652 -0
- package/guides/agent.md +29 -0
- package/guides/arrange.md +30 -0
- package/guides/audition.md +20 -14
- package/guides/automation.md +12 -6
- package/guides/chords.md +16 -14
- package/guides/effects.md +17 -15
- 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 +12 -6
- package/guides/music.md +23 -8
- package/guides/notes.md +15 -9
- package/guides/performance.md +15 -10
- package/guides/play.md +21 -13
- package/guides/project.md +25 -8
- package/guides/providers.md +20 -14
- package/guides/resample.md +32 -0
- package/guides/rhythm.md +17 -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 -12
- package/guides/styles.md +31 -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 +5 -2
- package/src/agent/agent.ts +131 -13
- package/src/agent/calibration-tools.ts +53 -0
- package/src/agent/chord-tools.ts +153 -1
- package/src/agent/clip-tools.ts +453 -0
- package/src/agent/command-agent.ts +369 -0
- package/src/agent/drum-tools.ts +2 -2
- package/src/agent/expression-tools.ts +91 -0
- package/src/agent/gateway.ts +246 -60
- package/src/agent/models.ts +53 -12
- package/src/agent/ops.ts +15 -1
- package/src/agent/pack-tools.ts +1 -1
- package/src/agent/planner.ts +13 -0
- package/src/agent/portable-schema.ts +80 -0
- package/src/agent/preview-tool.ts +5 -1
- package/src/agent/provider.ts +22 -8
- package/src/agent/resample-tool.ts +131 -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 +307 -37
- package/src/agent/usage.ts +2 -2
- package/src/agent/voice-tools.ts +925 -0
- package/src/agent/xcb-agent.ts +16 -6
- package/src/argv.ts +38 -0
- package/src/audio/analysis.ts +253 -0
- package/src/audio/arrange.ts +58 -3
- 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/doctor.ts +86 -0
- package/src/audio/dsp/bandbank.ts +138 -0
- package/src/audio/dsp/envelope.ts +171 -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/dsp/shift.ts +256 -0
- package/src/audio/effects/chain.ts +21 -4
- package/src/audio/effects/common.ts +57 -0
- package/src/audio/effects/convolution.ts +113 -5
- package/src/audio/effects/filter.ts +48 -69
- package/src/audio/effects/formant.ts +263 -0
- package/src/audio/effects/gaze.ts +354 -0
- package/src/audio/engine.ts +206 -33
- package/src/audio/fit.ts +80 -3
- package/src/audio/granular.ts +352 -21
- package/src/audio/instrument-check.ts +67 -47
- package/src/audio/instruments.ts +36 -3
- package/src/audio/keys/calibration.ts +56 -0
- package/src/audio/keys/electric.ts +427 -0
- package/src/audio/keys/engine.ts +156 -20
- package/src/audio/keys/organ.ts +1335 -0
- package/src/audio/keys/piano.ts +72 -8
- package/src/audio/keys/sympathetic.ts +127 -0
- package/src/audio/kits.ts +135 -6
- package/src/audio/live-worker.ts +3 -1
- package/src/audio/live.ts +247 -26
- package/src/audio/native.ts +615 -0
- package/src/audio/preview.ts +45 -3
- package/src/audio/render-worker.ts +2 -0
- package/src/audio/renderer.ts +2 -0
- package/src/audio/resample.ts +286 -0
- package/src/audio/resonators.ts +10 -2
- package/src/audio/sampler.ts +252 -24
- package/src/audio/samples.ts +46 -3
- package/src/audio/sing/analysis.ts +193 -0
- package/src/audio/sing/engine.ts +949 -0
- package/src/audio/strings/body.ts +29 -16
- package/src/audio/strings/bow.ts +699 -0
- package/src/audio/strings/engine.ts +274 -22
- package/src/audio/strings/measure.test-helpers.ts +2 -0
- 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 +608 -85
- package/src/audio/winds/engine.ts +306 -0
- package/src/audio/winds/filters.ts +153 -0
- package/src/audio/winds/pitch.ts +104 -0
- package/src/audio/winds/trim.ts +104 -0
- package/src/audio/winds/trims.ts +917 -0
- package/src/audio/winds/trims1.ts +297 -0
- package/src/audio/winds/voice.ts +492 -0
- 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 +44 -29
- 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 +104 -23
- package/src/commands/formant.ts +221 -0
- package/src/commands/fx.ts +105 -33
- package/src/commands/grammar.ts +558 -0
- package/src/commands/granular.ts +7 -2
- package/src/commands/help.ts +652 -339
- package/src/commands/history.ts +18 -0
- package/src/commands/keys.ts +293 -17
- package/src/commands/modal.ts +67 -4
- 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 +130 -0
- package/src/commands/progression.ts +170 -0
- package/src/commands/resample.ts +281 -0
- package/src/commands/rhythm.ts +3 -0
- package/src/commands/rig.ts +8 -25
- package/src/commands/sample.ts +49 -2
- package/src/commands/shift.ts +119 -0
- package/src/commands/sing.ts +478 -0
- package/src/commands/string.ts +28 -2
- package/src/commands/strum.ts +485 -0
- 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 +616 -0
- package/src/commands/vocal.ts +147 -0
- package/src/commands/vocoder.ts +627 -0
- package/src/commands/wind.ts +244 -0
- package/src/fs/durable.ts +50 -0
- package/src/lang/glossary.ts +493 -0
- package/src/launch-args.ts +163 -0
- package/src/main.ts +1521 -255
- package/src/media/cli.ts +20 -3
- package/src/media/import.ts +3 -1
- package/src/project/check.ts +26 -3
- 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 +36 -1
- package/src/session/daemon.ts +3 -0
- package/src/session/meta.ts +14 -0
- package/src/session/origin.ts +154 -0
- package/src/session/port.ts +22 -4
- package/src/session/presence.ts +50 -7
- package/src/session/protocol.ts +5 -1
- package/src/session/rebase.ts +18 -5
- package/src/session/receipt.ts +258 -0
- package/src/session/store.ts +65 -43
- package/src/tui/arrange-menu.ts +65 -31
- package/src/tui/audition.ts +1 -1
- package/src/tui/euclid.ts +18 -13
- package/src/tui/fader.ts +228 -41
- package/src/tui/granular-menu.ts +2 -4
- 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 +1377 -193
- package/src/tui/modal-menu.ts +38 -11
- package/src/tui/performance-menu.ts +46 -1
- package/src/tui/play-chords.ts +108 -5
- package/src/tui/play-mode.ts +16 -1
- package/src/tui/play-session.ts +81 -16
- package/src/tui/sing-menu.ts +278 -0
- package/src/tui/style-menu.ts +104 -0
- package/src/tui/vocoder-menu.ts +244 -0
- package/src/tui/wind-menu.ts +144 -0
- package/src/version.ts +8 -0
- package/src/web/fetch.ts +115 -29
- package/tui/activity.ts +180 -9
- package/tui/app.ts +274 -40
- package/tui/clip-row.ts +132 -0
- package/tui/delight.ts +144 -0
- package/tui/drawer.ts +70 -22
- package/tui/frame-gate.ts +76 -0
- package/tui/grammar.ts +112 -63
- package/tui/guide.ts +42 -4
- package/tui/highway.ts +269 -25
- package/tui/hints.ts +192 -0
- package/tui/input.ts +60 -9
- package/tui/keys.ts +1 -1
- package/tui/play-strip.ts +68 -14
- package/tui/prompt.ts +1 -1
- package/tui/screen.ts +144 -14
- package/tui/theme.ts +27 -0
package/tui/play-strip.ts
CHANGED
|
@@ -1,21 +1,31 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Play mode's two fixed rows: the header (`PLAY
|
|
2
|
+
* Play mode's two fixed rows: the header (`PLAY MODE ▶ 120 BPM · lead ·
|
|
3
|
+
* rev 3 C3–F4 vel 100 ● REC
|
|
3
4
|
* click ✓` plus a beat flash) and a one-line keyboard strip with the keys
|
|
4
5
|
* that are sounding lit. Both repaint in place every frame, so playing never
|
|
5
6
|
* scrolls the transcript.
|
|
6
7
|
*/
|
|
7
8
|
import type { CellBuffer } from "./screen.ts";
|
|
8
9
|
import { displayWidth, truncate } from "./text.ts";
|
|
9
|
-
import { onBackground, type Theme } from "./theme.ts";
|
|
10
|
+
import { onBackground, type Style, type Theme } from "./theme.ts";
|
|
10
11
|
|
|
11
12
|
export type PlayStripKey = Readonly<{
|
|
12
13
|
key: string;
|
|
13
14
|
label: string;
|
|
14
15
|
black: boolean;
|
|
15
16
|
lit: boolean;
|
|
17
|
+
/** Chord mode: the label names the chord this key plays. */
|
|
18
|
+
chord?: boolean | undefined;
|
|
19
|
+
/** That chord is built on a degree of the song's key. */
|
|
20
|
+
diatonic?: boolean | undefined;
|
|
16
21
|
}>;
|
|
17
22
|
|
|
18
23
|
export type PlayHeaderView = Readonly<{
|
|
24
|
+
/**
|
|
25
|
+
* The song header compressed to the left of the row while play mode
|
|
26
|
+
* covers it: `▶ 120 BPM · drums · rev 1`.
|
|
27
|
+
*/
|
|
28
|
+
context?: string | undefined;
|
|
19
29
|
/** `C3–F4`. */
|
|
20
30
|
range: string;
|
|
21
31
|
velocity: number;
|
|
@@ -51,7 +61,8 @@ export type ChordLegendCell = Readonly<{
|
|
|
51
61
|
/** Text of the header row, for tests and narrow terminals. */
|
|
52
62
|
export function playHeaderText(view: PlayHeaderView, unicode = true): string {
|
|
53
63
|
const parts = [
|
|
54
|
-
"PLAY",
|
|
64
|
+
"PLAY MODE",
|
|
65
|
+
view.context ?? "",
|
|
55
66
|
view.range,
|
|
56
67
|
view.armed
|
|
57
68
|
? `${unicode ? "●" : "*"} ${view.recording ? "REC" : "rec armed"}${view.replace ? " replace" : ""}`
|
|
@@ -87,8 +98,12 @@ export function paintPlayHeader(
|
|
|
87
98
|
);
|
|
88
99
|
x += buffer.text(x, y, " ", panel);
|
|
89
100
|
};
|
|
90
|
-
put(" PLAY ", roles.pillSteer);
|
|
91
|
-
|
|
101
|
+
put(" PLAY MODE ", roles.pillSteer);
|
|
102
|
+
const context = fitContext(view, width, unicode);
|
|
103
|
+
if (context) put(context, roles.muted);
|
|
104
|
+
// A kit names its range by the track already in the context.
|
|
105
|
+
if (!context.includes(" · ") || view.range !== "drums")
|
|
106
|
+
put(view.range, { ...roles.text, bold: true });
|
|
92
107
|
if (view.armed)
|
|
93
108
|
put(
|
|
94
109
|
`${unicode ? "●" : "*"} ${view.recording ? "REC" : "rec armed"}${view.replace ? " replace" : ""}`,
|
|
@@ -99,7 +114,8 @@ export function paintPlayHeader(
|
|
|
99
114
|
if (view.sustain) put("SUSTAIN", roles.pillQueue);
|
|
100
115
|
if (view.chords) put(view.chords, roles.hit);
|
|
101
116
|
if (view.countIn) put(view.countIn, roles.warning);
|
|
102
|
-
|
|
117
|
+
const hintWidth = displayWidth("? keys · esc leave");
|
|
118
|
+
if (view.beat && x + view.beat.of + 2 <= width - 1 - hintWidth) {
|
|
103
119
|
const cells = Array.from({ length: view.beat.of }, (_, index) =>
|
|
104
120
|
index + 1 === view.beat!.index
|
|
105
121
|
? unicode
|
|
@@ -136,6 +152,31 @@ export function paintPlayHeader(
|
|
|
136
152
|
if (right > x) buffer.text(right, y, hint, onBackground(roles.faint, panel));
|
|
137
153
|
}
|
|
138
154
|
|
|
155
|
+
/**
|
|
156
|
+
* The compressed song header (`▶ 120 BPM · bass · rev 1`). It is reserved
|
|
157
|
+
* first: the beat dots and the status give way before it does, and only a
|
|
158
|
+
* terminal too narrow for it with the badge and the way out shortens it to
|
|
159
|
+
* its first clause.
|
|
160
|
+
*/
|
|
161
|
+
function fitContext(
|
|
162
|
+
view: PlayHeaderView,
|
|
163
|
+
width: number,
|
|
164
|
+
unicode: boolean,
|
|
165
|
+
): string {
|
|
166
|
+
if (!view.context) return "";
|
|
167
|
+
const rest = displayWidth(
|
|
168
|
+
playHeaderText(
|
|
169
|
+
{ ...view, context: undefined, status: undefined, beat: undefined },
|
|
170
|
+
unicode,
|
|
171
|
+
),
|
|
172
|
+
);
|
|
173
|
+
// Badge padding, the right-hand hint and the gaps between parts.
|
|
174
|
+
const room = width - 2 - displayWidth("? keys · esc leave") - 4 - rest;
|
|
175
|
+
for (const candidate of [view.context, view.context.split(" · ")[0]!])
|
|
176
|
+
if (displayWidth(candidate) + 2 <= room) return candidate;
|
|
177
|
+
return "";
|
|
178
|
+
}
|
|
179
|
+
|
|
139
180
|
/** Chord mode's number row: `1 dim 2 min … 9 block b bass off n next`. */
|
|
140
181
|
export function paintChordLegend(
|
|
141
182
|
buffer: CellBuffer,
|
|
@@ -162,7 +203,11 @@ export function paintChordLegend(
|
|
|
162
203
|
}
|
|
163
204
|
}
|
|
164
205
|
|
|
165
|
-
/**
|
|
206
|
+
/**
|
|
207
|
+
* `A C3 W C#3 …`: each key with what it plays. The key letter is muted so
|
|
208
|
+
* the music reads first; a chord label is bold, and chords in the song's
|
|
209
|
+
* key are lit (they always fit). Sounding keys are reversed.
|
|
210
|
+
*/
|
|
166
211
|
export function paintPlayStrip(
|
|
167
212
|
buffer: CellBuffer,
|
|
168
213
|
y: number,
|
|
@@ -174,15 +219,24 @@ export function paintPlayStrip(
|
|
|
174
219
|
buffer.fill(0, y, width, 1, roles.canvas);
|
|
175
220
|
let x = 1;
|
|
176
221
|
for (const key of keys) {
|
|
177
|
-
const
|
|
178
|
-
const cellWidth = displayWidth(
|
|
222
|
+
const letter = key.key.toUpperCase();
|
|
223
|
+
const cellWidth = displayWidth(letter) + 1 + displayWidth(key.label) + 1;
|
|
179
224
|
if (x + cellWidth > width - 1) break;
|
|
180
|
-
const
|
|
225
|
+
const labelStyle: Style = key.lit
|
|
181
226
|
? { ...(key.black ? roles.selected : roles.hit), reverse: true }
|
|
227
|
+
: key.chord
|
|
228
|
+
? { ...(key.diatonic ? roles.hit : roles.text), bold: true }
|
|
229
|
+
: key.black
|
|
230
|
+
? roles.muted
|
|
231
|
+
: roles.text;
|
|
232
|
+
const letterStyle: Style = key.lit
|
|
233
|
+
? labelStyle
|
|
182
234
|
: key.black
|
|
183
|
-
? roles.
|
|
184
|
-
: roles.
|
|
185
|
-
buffer.text(x, y,
|
|
186
|
-
x +=
|
|
235
|
+
? roles.faint
|
|
236
|
+
: roles.muted;
|
|
237
|
+
x += buffer.text(x, y, letter, letterStyle);
|
|
238
|
+
x += buffer.text(x, y, " ", key.lit ? labelStyle : roles.canvas);
|
|
239
|
+
x += buffer.text(x, y, key.label, labelStyle);
|
|
240
|
+
x += 1;
|
|
187
241
|
}
|
|
188
242
|
}
|
package/tui/prompt.ts
CHANGED
|
@@ -316,7 +316,7 @@ export function applyPromptKey(
|
|
|
316
316
|
}
|
|
317
317
|
switch (keyInput.toUpperCase()) {
|
|
318
318
|
case "ENTER":
|
|
319
|
-
// The mode pill decides what Enter does:
|
|
319
|
+
// The mode pill decides what Enter does: NOW runs now, NEXT waits.
|
|
320
320
|
return submit(state, state.mode === "queue" ? "queue" : "submit");
|
|
321
321
|
case "ALT+ENTER":
|
|
322
322
|
return submit(state, "queue");
|
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;
|
|
@@ -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;
|
|
@@ -155,22 +233,65 @@ function sameColor(left: Style["fg"], right: Style["fg"]): boolean {
|
|
|
155
233
|
return left.r === right.r && left.g === right.g && left.b === right.b;
|
|
156
234
|
}
|
|
157
235
|
|
|
158
|
-
/**
|
|
236
|
+
/** True when only the foreground could differ and none of it shows. */
|
|
237
|
+
function invisibleStyle(style: Style | undefined): boolean {
|
|
238
|
+
return !style || (!style.bg && !style.reverse && !style.underline);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/** A space whose style draws nothing: its foreground never shows. */
|
|
242
|
+
export function isBlank(cell: Cell): boolean {
|
|
243
|
+
return cell.ch === " " && invisibleStyle(cell.style);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Serialise one row with minimal SGR changes: a blank keeps the current
|
|
248
|
+
* colour instead of resetting, and a colour-only change writes just the
|
|
249
|
+
* foreground code. Playback rows are mostly dim rules between spaces, so
|
|
250
|
+
* this halves the bytes a frame costs.
|
|
251
|
+
*/
|
|
159
252
|
export function encodeRow(
|
|
160
253
|
buffer: CellBuffer,
|
|
161
254
|
y: number,
|
|
162
255
|
capabilities: TerminalCapabilities,
|
|
256
|
+
from = 0,
|
|
257
|
+
to = buffer.width,
|
|
258
|
+
): string {
|
|
259
|
+
return encodeSpans(buffer, y, capabilities, [{ from, to }], false);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Serialise several runs of one row; each run after the first (or every
|
|
264
|
+
* run, with `address`) starts with a cursor move. The colour state carries
|
|
265
|
+
* across runs, since a cursor move leaves it alone.
|
|
266
|
+
*/
|
|
267
|
+
export function encodeSpans(
|
|
268
|
+
buffer: CellBuffer,
|
|
269
|
+
y: number,
|
|
270
|
+
capabilities: TerminalCapabilities,
|
|
271
|
+
spans: ReadonlyArray<{ from: number; to: number }>,
|
|
272
|
+
address = true,
|
|
163
273
|
): string {
|
|
164
274
|
let out = "";
|
|
165
275
|
let current: Style | undefined | null = null;
|
|
166
|
-
for (
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
current
|
|
276
|
+
for (const [index, span] of spans.entries()) {
|
|
277
|
+
if (address || index > 0) out += `\u001b[${y + 1};${span.from + 1}H`;
|
|
278
|
+
for (let x = span.from; x < span.to; x += 1) {
|
|
279
|
+
const cell = buffer.get(x, y)!;
|
|
280
|
+
if (cell.ch === "") continue;
|
|
281
|
+
if (current !== null && isBlank(cell) && invisibleStyle(current)) {
|
|
282
|
+
out += " ";
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
if (current === null || !sameStyle(current, cell.style)) {
|
|
286
|
+
const next = cell.style ?? EMPTY;
|
|
287
|
+
out +=
|
|
288
|
+
(current !== null &&
|
|
289
|
+
foregroundSgr(current ?? EMPTY, next, capabilities)) ||
|
|
290
|
+
styleSgr(next, capabilities);
|
|
291
|
+
current = cell.style;
|
|
292
|
+
}
|
|
293
|
+
out += cell.ch;
|
|
172
294
|
}
|
|
173
|
-
out += cell.ch;
|
|
174
295
|
}
|
|
175
296
|
if (capabilities.attributes !== false) out += "\u001b[0m";
|
|
176
297
|
return out;
|
|
@@ -193,9 +314,10 @@ export interface CursorPosition {
|
|
|
193
314
|
}
|
|
194
315
|
|
|
195
316
|
/**
|
|
196
|
-
* Differential writer: emits cursor-addressed
|
|
197
|
-
*
|
|
198
|
-
*
|
|
317
|
+
* Differential writer: emits one cursor-addressed rewrite per changed row,
|
|
318
|
+
* covering only that row's changed run of cells. Each frame is one string
|
|
319
|
+
* wrapped in synchronized-update markers, which capable terminals use to
|
|
320
|
+
* avoid tearing and others ignore.
|
|
199
321
|
*/
|
|
200
322
|
export class ScreenWriter {
|
|
201
323
|
private previous: CellBuffer | undefined;
|
|
@@ -227,8 +349,16 @@ export class ScreenWriter {
|
|
|
227
349
|
let rows = 0;
|
|
228
350
|
if (full) body += "\u001b[0m\u001b[2J";
|
|
229
351
|
for (let y = 0; y < buffer.height; y += 1) {
|
|
230
|
-
if (
|
|
231
|
-
|
|
352
|
+
if (full) {
|
|
353
|
+
body += `\u001b[${y + 1};1H${encodeRow(buffer, y, this.capabilities)}`;
|
|
354
|
+
rows += 1;
|
|
355
|
+
continue;
|
|
356
|
+
}
|
|
357
|
+
// Only the changed run of cells: a moving playhead or a ticking meter
|
|
358
|
+
// rewrites a few columns, not the whole row.
|
|
359
|
+
const spans = buffer.rowSpans(this.previous!, y);
|
|
360
|
+
if (spans.length === 0) continue;
|
|
361
|
+
body += encodeSpans(buffer, y, this.capabilities, spans);
|
|
232
362
|
rows += 1;
|
|
233
363
|
}
|
|
234
364
|
const cursorKey = cursor ? `${cursor.x},${cursor.y}` : "hidden";
|
package/tui/theme.ts
CHANGED
|
@@ -425,6 +425,33 @@ export function styleSgr(
|
|
|
425
425
|
return sequence;
|
|
426
426
|
}
|
|
427
427
|
|
|
428
|
+
/**
|
|
429
|
+
* The shortest SGR that moves the terminal from `from` to `to` when they
|
|
430
|
+
* differ only in foreground colour: `38;…` or `39`, never a full reset.
|
|
431
|
+
* Undefined when anything else differs (the caller emits `styleSgr`).
|
|
432
|
+
*/
|
|
433
|
+
export function foregroundSgr(
|
|
434
|
+
from: Style,
|
|
435
|
+
to: Style,
|
|
436
|
+
capabilities: TerminalCapabilities,
|
|
437
|
+
): string | undefined {
|
|
438
|
+
if (capabilities.attributes === false) return "";
|
|
439
|
+
if (
|
|
440
|
+
!!from.bold !== !!to.bold ||
|
|
441
|
+
!!from.dim !== !!to.dim ||
|
|
442
|
+
!!from.italic !== !!to.italic ||
|
|
443
|
+
!!from.underline !== !!to.underline ||
|
|
444
|
+
!!from.reverse !== !!to.reverse
|
|
445
|
+
)
|
|
446
|
+
return undefined;
|
|
447
|
+
const fromBg = from.bg && colorCode(from.bg, capabilities.colorDepth, true);
|
|
448
|
+
const toBg = to.bg && colorCode(to.bg, capabilities.colorDepth, true);
|
|
449
|
+
if (fromBg !== toBg) return undefined;
|
|
450
|
+
if (!to.fg) return "\u001b[39m";
|
|
451
|
+
const code = colorCode(to.fg, capabilities.colorDepth, false);
|
|
452
|
+
return code ? `\u001b[${code}m` : "\u001b[39m";
|
|
453
|
+
}
|
|
454
|
+
|
|
428
455
|
/** Resolve the effective theme: no-color terminals always use attributes only. */
|
|
429
456
|
export function effectiveTheme(
|
|
430
457
|
name: ThemeName,
|