@hraness/dawg 0.6.1 → 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 +92 -0
- package/DAWG.md +343 -116
- package/README.md +27 -25
- package/core/autotune.ts +1119 -0
- package/core/chords.ts +271 -23
- package/core/clips.ts +499 -0
- package/core/diff.ts +182 -104
- package/core/expression.ts +15 -0
- package/core/fx.ts +99 -29
- package/core/instruments.ts +19 -0
- package/core/keys.ts +3 -3
- package/core/loop.ts +5 -0
- package/core/lyrics.ts +297 -0
- package/core/master.ts +3 -3
- package/core/resonators.ts +16 -2
- package/core/routing.ts +165 -0
- package/core/score.ts +832 -13
- package/core/sdk/eval-child.ts +7 -2
- package/core/sdk/eval.ts +35 -6
- package/core/sdk/print.ts +276 -3
- package/core/sdk/sync-lyrics.ts +49 -0
- package/core/sdk/v1.ts +1849 -42
- package/core/sections.ts +292 -26
- 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 +30 -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 +12 -6
- package/guides/music.md +23 -8
- package/guides/notes.md +15 -9
- package/guides/performance.md +15 -13
- package/guides/play.md +21 -13
- package/guides/project.md +25 -8
- package/guides/providers.md +20 -14
- package/guides/resample.md +15 -9
- 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 -13
- 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 +126 -14
- package/src/agent/calibration-tools.ts +53 -0
- 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 +1 -1
- package/src/agent/gateway.ts +246 -60
- package/src/agent/models.ts +53 -12
- package/src/agent/ops.ts +12 -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 +4 -1
- package/src/agent/provider.ts +22 -8
- 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 +108 -23
- package/src/agent/usage.ts +2 -2
- package/src/agent/voice-tools.ts +925 -0
- package/src/agent/xcb-agent.ts +11 -7
- package/src/argv.ts +38 -0
- package/src/audio/analysis.ts +253 -0
- package/src/audio/arrange.ts +37 -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 +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 +160 -28
- 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 +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 +1 -1
- package/src/commands/formant.ts +221 -0
- package/src/commands/fx.ts +101 -32
- package/src/commands/grammar.ts +558 -0
- package/src/commands/help.ts +610 -380
- package/src/commands/history.ts +18 -0
- package/src/commands/keys.ts +8 -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 +130 -0
- package/src/commands/progression.ts +170 -0
- package/src/commands/rhythm.ts +3 -0
- package/src/commands/rig.ts +3 -24
- package/src/commands/sing.ts +478 -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 +616 -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/lang/glossary.ts +493 -0
- package/src/launch-args.ts +163 -0
- package/src/main.ts +1397 -253
- 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/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 +34 -4
- 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 +759 -177
- 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 +47 -7
- 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 +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 +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/app.ts
CHANGED
|
@@ -14,19 +14,31 @@
|
|
|
14
14
|
|
|
15
15
|
import {
|
|
16
16
|
ActivityFeed,
|
|
17
|
+
ERROR_FADE_MS,
|
|
18
|
+
fail,
|
|
17
19
|
revisionLabel,
|
|
18
20
|
spinnerFrame,
|
|
19
21
|
type ActivityCard,
|
|
22
|
+
type Receipt,
|
|
20
23
|
type TranscriptEntry,
|
|
21
24
|
} from "./activity.ts";
|
|
22
25
|
import {
|
|
23
26
|
paintHighway,
|
|
27
|
+
projectionFor,
|
|
24
28
|
resolveBeat,
|
|
25
29
|
type TrackScoreSnapshot,
|
|
26
30
|
} from "./highway.ts";
|
|
31
|
+
import { placeholderHint } from "./hints.ts";
|
|
32
|
+
import {
|
|
33
|
+
Delight,
|
|
34
|
+
downbeatGlint,
|
|
35
|
+
FIRST_LOOP_CARD,
|
|
36
|
+
sweepProgress,
|
|
37
|
+
} from "./delight.ts";
|
|
27
38
|
import { asciiHint, fitHint, HINTS } from "./grammar.ts";
|
|
28
39
|
import { GuideBrowser } from "./guide.ts";
|
|
29
40
|
import { listGuides } from "../guides/index.ts";
|
|
41
|
+
import { topicMiss } from "../src/lang/glossary.ts";
|
|
30
42
|
import { classifyKey, overlayKey, type UiCommand } from "./keys.ts";
|
|
31
43
|
import { PromptModel, type PromptAction, type PromptMode } from "./prompt.ts";
|
|
32
44
|
import {
|
|
@@ -70,11 +82,17 @@ export interface AppView {
|
|
|
70
82
|
types?: TypesIndicator | undefined;
|
|
71
83
|
/**
|
|
72
84
|
* The line under the prompt: `$0.12 session · $0.48 today · opus-5.5 ·
|
|
73
|
-
* gateway`, or `
|
|
85
|
+
* gateway`, or `commands only`. The caller sizes it to the width.
|
|
74
86
|
*/
|
|
75
87
|
spend?: string | undefined;
|
|
76
|
-
/** No agent provider: the placeholder teaches commands, no
|
|
88
|
+
/** No agent provider: the placeholder teaches commands, no NOW pill. */
|
|
77
89
|
agentOffline?: boolean | undefined;
|
|
90
|
+
/**
|
|
91
|
+
* Show-me: the command the agent is writing, as it streams (ghost text in
|
|
92
|
+
* an empty prompt bar), and the caption naming the human gesture.
|
|
93
|
+
*/
|
|
94
|
+
showMe?:
|
|
95
|
+
{ ghost?: string | undefined; caption?: string | undefined } | undefined;
|
|
78
96
|
/** Play mode: replaces the header and adds the keyboard strip row. */
|
|
79
97
|
play?: PlayHeaderView | undefined;
|
|
80
98
|
/** Song master meter: integrated LUFS and true peak of the playing loop. */
|
|
@@ -136,6 +154,8 @@ export interface UiState {
|
|
|
136
154
|
guide?: GuideBrowser | undefined;
|
|
137
155
|
/** The fader drawer, docked over the bottom of the piano roll. */
|
|
138
156
|
drawer?: DrawerView | undefined;
|
|
157
|
+
/** Downbeat glint, first-loop sweep and ghost-note lanes (delight.ts). */
|
|
158
|
+
delight?: Delight | undefined;
|
|
139
159
|
}
|
|
140
160
|
|
|
141
161
|
/**
|
|
@@ -412,18 +432,47 @@ function syncSegment(
|
|
|
412
432
|
return { text: `${glyph[sync]} ${sync}`, style: style[sync], priority: 4 };
|
|
413
433
|
}
|
|
414
434
|
|
|
435
|
+
/**
|
|
436
|
+
* The song header compressed for play mode's row: transport, tempo, the
|
|
437
|
+
* track and the revision (`▶ 120 BPM · drums · rev 1`).
|
|
438
|
+
*/
|
|
439
|
+
export function playContext(
|
|
440
|
+
score: TrackScoreSnapshot,
|
|
441
|
+
unicode: boolean,
|
|
442
|
+
): string {
|
|
443
|
+
const playing = score.playing === true;
|
|
444
|
+
const glyph = playing ? (unicode ? "▶" : ">") : unicode ? "⏸" : "||";
|
|
445
|
+
const name = score.trackName ?? score.trackId ?? "track";
|
|
446
|
+
return [
|
|
447
|
+
`${glyph} ${score.bpm ?? 120} BPM`,
|
|
448
|
+
name,
|
|
449
|
+
...(score.revision !== undefined ? [`rev ${score.revision}`] : []),
|
|
450
|
+
].join(" · ");
|
|
451
|
+
}
|
|
452
|
+
|
|
415
453
|
function paintHeader(
|
|
416
454
|
buffer: CellBuffer,
|
|
417
455
|
view: AppView,
|
|
418
456
|
ui: UiState,
|
|
419
457
|
width: number,
|
|
420
458
|
hits?: HitMap,
|
|
459
|
+
beat = 0,
|
|
421
460
|
): void {
|
|
422
461
|
const { theme, capabilities } = ui;
|
|
423
462
|
const roles = theme.roles;
|
|
424
463
|
const score = view.score;
|
|
425
464
|
const unicode = capabilities.unicode;
|
|
426
465
|
const playing = score.playing === true;
|
|
466
|
+
// The ▶ brightens for a moment on each bar's downbeat.
|
|
467
|
+
const glint = downbeatGlint({
|
|
468
|
+
playing,
|
|
469
|
+
beat,
|
|
470
|
+
bpm: score.bpm ?? 120,
|
|
471
|
+
beatsPerBar: score.beatsPerBar ?? 4,
|
|
472
|
+
barBeats: score.barBeats,
|
|
473
|
+
loopBeats: score.loopBeats,
|
|
474
|
+
reducedMotion: ui.reducedMotion,
|
|
475
|
+
});
|
|
427
476
|
const transport = `${playing ? (unicode ? "▶" : ">") : unicode ? "⏸" : "||"} ${score.bpm ?? 120} BPM`;
|
|
428
477
|
const name = score.trackName ?? score.trackId ?? "track";
|
|
429
478
|
const left: Segment[] = [
|
|
@@ -436,7 +485,11 @@ function paintHeader(
|
|
|
436
485
|
},
|
|
437
486
|
{
|
|
438
487
|
text: transport,
|
|
439
|
-
style: playing
|
|
488
|
+
style: playing
|
|
489
|
+
? glint
|
|
490
|
+
? { ...shade(roles.transport, 0.55), bold: true }
|
|
491
|
+
: roles.transport
|
|
492
|
+
: roles.paused,
|
|
440
493
|
priority: 0,
|
|
441
494
|
target: { kind: "transport" },
|
|
442
495
|
},
|
|
@@ -527,7 +580,9 @@ function cardMarker(card: ActivityCard, unicode: boolean): string {
|
|
|
527
580
|
}
|
|
528
581
|
|
|
529
582
|
export function cardText(card: ActivityCard, unicode: boolean): string {
|
|
530
|
-
const
|
|
583
|
+
const count =
|
|
584
|
+
card.count && card.count > 1 ? ` ${unicode ? "×" : "x"}${card.count}` : "";
|
|
585
|
+
const parts = [`${cardMarker(card, unicode)} ${card.text}${count}`];
|
|
531
586
|
const revision = revisionLabel(
|
|
532
587
|
card.baseRevision,
|
|
533
588
|
card.resultRevision,
|
|
@@ -535,15 +590,46 @@ export function cardText(card: ActivityCard, unicode: boolean): string {
|
|
|
535
590
|
);
|
|
536
591
|
if (revision) parts.push(revision);
|
|
537
592
|
if (card.hint) parts.push(card.hint);
|
|
593
|
+
if (card.suffix) parts.push(card.suffix);
|
|
538
594
|
return parts.join(" · ");
|
|
539
595
|
}
|
|
540
596
|
|
|
597
|
+
/** How long each one-time note leads when they cannot all fit at once. */
|
|
598
|
+
export const ONCE_ROTATE_MS = 3_000;
|
|
599
|
+
|
|
600
|
+
/**
|
|
601
|
+
* The cards for the strip's width: when the one-time notes cannot all show
|
|
602
|
+
* whole beside the receipts, they take turns (one every `ONCE_ROTATE_MS`,
|
|
603
|
+
* on the frame clock), so each is read in full at least once.
|
|
604
|
+
*/
|
|
605
|
+
export function stripCards(
|
|
606
|
+
cards: readonly ActivityCard[],
|
|
607
|
+
room: number,
|
|
608
|
+
nowMs: number,
|
|
609
|
+
unicode: boolean,
|
|
610
|
+
): ActivityCard[] {
|
|
611
|
+
const width = (list: readonly ActivityCard[]) =>
|
|
612
|
+
list.reduce(
|
|
613
|
+
(sum, card, index) =>
|
|
614
|
+
sum + (index > 0 ? 3 : 0) + displayWidth(cardText(card, unicode)),
|
|
615
|
+
0,
|
|
616
|
+
);
|
|
617
|
+
if (width(cards) <= room) return [...cards];
|
|
618
|
+
const steady = cards.filter((card) => !card.once);
|
|
619
|
+
const once = cards.filter((card) => card.once);
|
|
620
|
+
if (once.length < 2) return [...cards];
|
|
621
|
+
const turn =
|
|
622
|
+
once[Math.floor(Math.max(0, nowMs) / ONCE_ROTATE_MS) % once.length]!;
|
|
623
|
+
return [...steady, turn];
|
|
624
|
+
}
|
|
625
|
+
|
|
541
626
|
function paintActivity(
|
|
542
627
|
buffer: CellBuffer,
|
|
543
628
|
y: number,
|
|
544
629
|
ui: UiState,
|
|
545
630
|
width: number,
|
|
546
631
|
nowMs: number,
|
|
632
|
+
agentOffline = false,
|
|
547
633
|
): void {
|
|
548
634
|
const { theme, capabilities, activity } = ui;
|
|
549
635
|
const roles = theme.roles;
|
|
@@ -567,9 +653,10 @@ function paintActivity(
|
|
|
567
653
|
...roles.agent,
|
|
568
654
|
bold: true,
|
|
569
655
|
});
|
|
570
|
-
|
|
656
|
+
// The agent's latest sentence, faint: what it is doing, in its words.
|
|
657
|
+
if (activity.streamSentence && x < limit - 4) {
|
|
571
658
|
x += buffer.text(x, y, " · ", roles.faint);
|
|
572
|
-
const tail = activity.
|
|
659
|
+
const tail = activity.streamSentence;
|
|
573
660
|
const room = Math.max(0, limit - x);
|
|
574
661
|
const shown =
|
|
575
662
|
displayWidth(tail) > room
|
|
@@ -577,13 +664,24 @@ function paintActivity(
|
|
|
577
664
|
.slice(-(room - 1))
|
|
578
665
|
.join("")}`
|
|
579
666
|
: tail;
|
|
580
|
-
x += buffer.text(x, y, truncate(shown, room), roles.
|
|
667
|
+
x += buffer.text(x, y, truncate(shown, room), roles.faint);
|
|
581
668
|
}
|
|
582
669
|
} else {
|
|
583
|
-
const cards =
|
|
670
|
+
const cards = stripCards(
|
|
671
|
+
activity.visible(nowMs),
|
|
672
|
+
Math.max(0, limit - x),
|
|
673
|
+
nowMs,
|
|
674
|
+
capabilities.unicode,
|
|
675
|
+
);
|
|
584
676
|
cards.forEach((card, index) => {
|
|
585
677
|
if (x >= limit) return;
|
|
586
678
|
const text = cardText(card, capabilities.unicode);
|
|
679
|
+
// A one-time note behind the first slot shows whole or not at all
|
|
680
|
+
// (it stays in ctrl-o); a cut note loses its key word.
|
|
681
|
+
if (index > 0 && card.once && x + 3 + displayWidth(text) > limit) {
|
|
682
|
+
x = limit;
|
|
683
|
+
return;
|
|
684
|
+
}
|
|
587
685
|
if (index > 0) {
|
|
588
686
|
if (x + 3 + Math.min(12, displayWidth(text)) > limit) {
|
|
589
687
|
x = limit;
|
|
@@ -602,7 +700,14 @@ function paintActivity(
|
|
|
602
700
|
: card.tone === "success"
|
|
603
701
|
? roles.success
|
|
604
702
|
: roles.text;
|
|
605
|
-
|
|
703
|
+
// Behind the first slot everything is faint; an error keeps its
|
|
704
|
+
// colour until it is old, then fades with the rest.
|
|
705
|
+
if (index > 0)
|
|
706
|
+
style =
|
|
707
|
+
card.tone === "error" && age < ERROR_FADE_MS
|
|
708
|
+
? roles.error
|
|
709
|
+
: roles.faint;
|
|
710
|
+
else if (card.once) style = roles.faint;
|
|
606
711
|
else if (!ui.reducedMotion && age >= 0 && age < CARD_GLOW_MS)
|
|
607
712
|
style = { ...shade(style, 0.4 * (1 - age / CARD_GLOW_MS)), bold: true };
|
|
608
713
|
x += buffer.text(x, y, truncate(text, Math.max(0, limit - x)), style);
|
|
@@ -612,7 +717,9 @@ function paintActivity(
|
|
|
612
717
|
x,
|
|
613
718
|
y,
|
|
614
719
|
truncate(
|
|
615
|
-
|
|
720
|
+
agentOffline
|
|
721
|
+
? "type a command · space plays on an empty prompt · /help"
|
|
722
|
+
: "type a request · space plays on an empty prompt · /help",
|
|
616
723
|
limit - x,
|
|
617
724
|
),
|
|
618
725
|
roles.faint,
|
|
@@ -626,7 +733,7 @@ function paintActivity(
|
|
|
626
733
|
// Prompt panel
|
|
627
734
|
|
|
628
735
|
function pill(mode: PromptMode): string {
|
|
629
|
-
return mode === "queue" ? "
|
|
736
|
+
return mode === "queue" ? " NEXT " : " NOW ";
|
|
630
737
|
}
|
|
631
738
|
|
|
632
739
|
function paintPrompt(
|
|
@@ -698,12 +805,27 @@ function paintPrompt(
|
|
|
698
805
|
if (index === rows - 1 && layoutInfo.first + rows < layoutInfo.total)
|
|
699
806
|
buffer.text(width - 3, y, capabilities.unicode ? "↓" : "v", faint);
|
|
700
807
|
}
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
808
|
+
const ghost = view.showMe?.ghost;
|
|
809
|
+
const caption = view.showMe?.caption;
|
|
810
|
+
if (prompt.value.length === 0 && (ghost || caption)) {
|
|
811
|
+
// The agent's command as it streams, then the gesture it stands for.
|
|
812
|
+
const typed = ghost ? `${ghost}${capabilities.unicode ? "▏" : "_"}` : "";
|
|
813
|
+
const line = typed && caption ? `${typed} ${caption}` : typed || caption!;
|
|
814
|
+
buffer.text(5, top + 1, truncate(line, editorWidth - 1), faint);
|
|
815
|
+
} else if (prompt.value.length === 0) {
|
|
816
|
+
// Seeded per session (tui/hints.ts): the words stay put while you
|
|
817
|
+
// work and change between sessions, chosen by agent and song state.
|
|
818
|
+
const score = view.score;
|
|
819
|
+
const placeholder = placeholderHint({
|
|
820
|
+
seed: score.sessionId ?? "dawg",
|
|
821
|
+
agent: !view.agentOffline,
|
|
822
|
+
filled:
|
|
823
|
+
score.notes.length > 0 ||
|
|
824
|
+
(score.clips ?? []).length > 0 ||
|
|
825
|
+
(score.layers ?? []).some((layer) => layer.notes.length > 0),
|
|
826
|
+
drums: projectionFor(score).kind !== "pitch",
|
|
827
|
+
queue: mode === "queue",
|
|
828
|
+
});
|
|
707
829
|
buffer.text(5, top + 1, truncate(placeholder, editorWidth - 1), faint);
|
|
708
830
|
}
|
|
709
831
|
|
|
@@ -714,13 +836,14 @@ function paintPrompt(
|
|
|
714
836
|
for (let column = 1; column < width - 1; column += 1)
|
|
715
837
|
buffer.set(column, y, box.h, border);
|
|
716
838
|
buffer.set(width - 1, y, box.br, border);
|
|
839
|
+
// One notation everywhere: ctrl-<key>, as /help and the menu spell it.
|
|
717
840
|
const hints =
|
|
718
|
-
width >=
|
|
719
|
-
? " enter send · shift
|
|
720
|
-
: width >=
|
|
721
|
-
? " enter send ·
|
|
722
|
-
: width >=
|
|
723
|
-
? " enter ·
|
|
841
|
+
width >= 112
|
|
842
|
+
? " enter send · shift-enter newline · ctrl-q now/next · ctrl-z undo · ctrl-o log · ctrl-c quit "
|
|
843
|
+
: width >= 80
|
|
844
|
+
? " enter send · ctrl-j newline · ctrl-q now/next · ctrl-z undo · ctrl-o log "
|
|
845
|
+
: width >= 52
|
|
846
|
+
? " enter · ctrl-q now/next · ctrl-z undo · ctrl-o log "
|
|
724
847
|
: "";
|
|
725
848
|
const spend = view.spend ? ` ${view.spend} ` : "";
|
|
726
849
|
const spendWidth = displayWidth(spend);
|
|
@@ -1164,7 +1287,10 @@ export function composeFrame(
|
|
|
1164
1287
|
buffer,
|
|
1165
1288
|
layout.header,
|
|
1166
1289
|
width,
|
|
1167
|
-
|
|
1290
|
+
{
|
|
1291
|
+
...view.play,
|
|
1292
|
+
context: playContext(view.score, ui.capabilities.unicode),
|
|
1293
|
+
},
|
|
1168
1294
|
ui.theme,
|
|
1169
1295
|
ui.capabilities.unicode,
|
|
1170
1296
|
);
|
|
@@ -1188,7 +1314,15 @@ export function composeFrame(
|
|
|
1188
1314
|
height: layout.highway.height - 1,
|
|
1189
1315
|
};
|
|
1190
1316
|
}
|
|
1191
|
-
} else
|
|
1317
|
+
} else
|
|
1318
|
+
paintHeader(
|
|
1319
|
+
buffer,
|
|
1320
|
+
view,
|
|
1321
|
+
ui,
|
|
1322
|
+
width,
|
|
1323
|
+
hits,
|
|
1324
|
+
view.beat ?? resolveBeat(view.score, nowMs),
|
|
1325
|
+
);
|
|
1192
1326
|
if (view.arrange && layout.highway.height > 4) {
|
|
1193
1327
|
paintArrangeStrip(
|
|
1194
1328
|
buffer,
|
|
@@ -1228,6 +1362,16 @@ export function composeFrame(
|
|
|
1228
1362
|
theme: ui.theme,
|
|
1229
1363
|
capabilities: ui.capabilities,
|
|
1230
1364
|
reducedMotion: ui.reducedMotion,
|
|
1365
|
+
hint: {
|
|
1366
|
+
seed: view.score.sessionId ?? "dawg",
|
|
1367
|
+
agent: !view.agentOffline,
|
|
1368
|
+
},
|
|
1369
|
+
sweep: sweepProgress(
|
|
1370
|
+
ui.delight?.sweepStartedAtMs,
|
|
1371
|
+
nowMs,
|
|
1372
|
+
ui.reducedMotion,
|
|
1373
|
+
),
|
|
1374
|
+
glows: ui.delight?.glows(nowMs, ui.reducedMotion),
|
|
1231
1375
|
},
|
|
1232
1376
|
);
|
|
1233
1377
|
if (ui.drawer)
|
|
@@ -1236,7 +1380,11 @@ export function composeFrame(
|
|
|
1236
1380
|
layout.highway,
|
|
1237
1381
|
width,
|
|
1238
1382
|
ui.drawer,
|
|
1239
|
-
{
|
|
1383
|
+
{
|
|
1384
|
+
theme: ui.theme,
|
|
1385
|
+
unicode: ui.capabilities.unicode,
|
|
1386
|
+
reducedMotion: ui.reducedMotion,
|
|
1387
|
+
},
|
|
1240
1388
|
hits,
|
|
1241
1389
|
);
|
|
1242
1390
|
}
|
|
@@ -1245,7 +1393,14 @@ export function composeFrame(
|
|
|
1245
1393
|
text: ui.keys,
|
|
1246
1394
|
hint: HINTS.keys,
|
|
1247
1395
|
});
|
|
1248
|
-
paintActivity(
|
|
1396
|
+
paintActivity(
|
|
1397
|
+
buffer,
|
|
1398
|
+
layout.activity,
|
|
1399
|
+
ui,
|
|
1400
|
+
width,
|
|
1401
|
+
nowMs,
|
|
1402
|
+
view.agentOffline ?? false,
|
|
1403
|
+
);
|
|
1249
1404
|
const prompt = paintPrompt(buffer, view, ui, layout, width);
|
|
1250
1405
|
return {
|
|
1251
1406
|
buffer,
|
|
@@ -1260,8 +1415,15 @@ export function composeFrame(
|
|
|
1260
1415
|
// ---------------------------------------------------------------------------
|
|
1261
1416
|
// Mutable app
|
|
1262
1417
|
|
|
1418
|
+
/** The longest a lagging terminal waits between frames. */
|
|
1419
|
+
export const MAX_BACKOFF_MS = 250;
|
|
1420
|
+
|
|
1263
1421
|
export interface TerminalIO {
|
|
1264
|
-
|
|
1422
|
+
/**
|
|
1423
|
+
* Writes a frame. `false` means the terminal is not keeping up (the
|
|
1424
|
+
* stream's buffer is full): frames back off until a write is accepted.
|
|
1425
|
+
*/
|
|
1426
|
+
write(data: string): void | boolean;
|
|
1265
1427
|
columns(): number;
|
|
1266
1428
|
rows(): number;
|
|
1267
1429
|
}
|
|
@@ -1316,6 +1478,15 @@ export class TuiApp {
|
|
|
1316
1478
|
private lastFrameAt = Number.NEGATIVE_INFINITY;
|
|
1317
1479
|
/** Minimum interval between frames (~30 fps). */
|
|
1318
1480
|
frameIntervalMs = 33;
|
|
1481
|
+
/**
|
|
1482
|
+
* Extra spacing while the terminal lags: doubles on each refused write up
|
|
1483
|
+
* to `MAX_BACKOFF_MS`, resets on the first accepted one.
|
|
1484
|
+
*/
|
|
1485
|
+
backoffMs = 0;
|
|
1486
|
+
/** Session-only delight; `heardLoop` comes from the session's metadata. */
|
|
1487
|
+
readonly delight = new Delight();
|
|
1488
|
+
/** The song wrapped for the first time: the host records it in meta. */
|
|
1489
|
+
onFirstLoop: (() => void) | undefined;
|
|
1319
1490
|
|
|
1320
1491
|
constructor(options: TuiAppOptions) {
|
|
1321
1492
|
this.io = options.io;
|
|
@@ -1348,6 +1519,7 @@ export class TuiApp {
|
|
|
1348
1519
|
keys: this.keys,
|
|
1349
1520
|
guide: this.guide,
|
|
1350
1521
|
drawer: this.drawer,
|
|
1522
|
+
delight: this.delight,
|
|
1351
1523
|
};
|
|
1352
1524
|
}
|
|
1353
1525
|
|
|
@@ -1367,9 +1539,13 @@ export class TuiApp {
|
|
|
1367
1539
|
*/
|
|
1368
1540
|
render(view: AppView, options: { force?: boolean } = {}): string {
|
|
1369
1541
|
const now = this.clock();
|
|
1370
|
-
if (
|
|
1542
|
+
if (
|
|
1543
|
+
!options.force &&
|
|
1544
|
+
now - this.lastFrameAt < this.frameIntervalMs + this.backoffMs
|
|
1545
|
+
)
|
|
1371
1546
|
return "";
|
|
1372
1547
|
this.lastFrameAt = now;
|
|
1548
|
+
this.watchFirstLoop(view, now);
|
|
1373
1549
|
const frame = composeFrame(
|
|
1374
1550
|
view,
|
|
1375
1551
|
this.ui,
|
|
@@ -1378,10 +1554,43 @@ export class TuiApp {
|
|
|
1378
1554
|
);
|
|
1379
1555
|
this.lastFrame = frame;
|
|
1380
1556
|
const out = this.writer.frame(frame.buffer, frame.cursor);
|
|
1381
|
-
if (out)
|
|
1557
|
+
if (out) {
|
|
1558
|
+
const accepted = this.io.write(out);
|
|
1559
|
+
this.backoffMs =
|
|
1560
|
+
accepted === false
|
|
1561
|
+
? Math.min(
|
|
1562
|
+
MAX_BACKOFF_MS,
|
|
1563
|
+
Math.max(this.frameIntervalMs, this.backoffMs * 2),
|
|
1564
|
+
)
|
|
1565
|
+
: 0;
|
|
1566
|
+
}
|
|
1382
1567
|
return out;
|
|
1383
1568
|
}
|
|
1384
1569
|
|
|
1570
|
+
/**
|
|
1571
|
+
* The first wrap of a song that never wrapped before: a one-shot sweep
|
|
1572
|
+
* and the card `↻ first loop`. The card shows with motion off too (it is
|
|
1573
|
+
* information); the sweep does not.
|
|
1574
|
+
*/
|
|
1575
|
+
private watchFirstLoop(view: AppView, now: number): void {
|
|
1576
|
+
const score = view.score;
|
|
1577
|
+
const wrapped = this.delight.observe(
|
|
1578
|
+
{
|
|
1579
|
+
playing: score.playing === true,
|
|
1580
|
+
beat: view.beat ?? resolveBeat(score, now),
|
|
1581
|
+
loopBeats: score.loopBeats,
|
|
1582
|
+
empty:
|
|
1583
|
+
score.notes.length === 0 &&
|
|
1584
|
+
(score.clips ?? []).length === 0 &&
|
|
1585
|
+
!(score.layers ?? []).some((layer) => layer.notes.length > 0),
|
|
1586
|
+
},
|
|
1587
|
+
now,
|
|
1588
|
+
);
|
|
1589
|
+
if (!wrapped) return;
|
|
1590
|
+
this.activity.pushCard(FIRST_LOOP_CARD, { tone: "info", once: true });
|
|
1591
|
+
this.onFirstLoop?.();
|
|
1592
|
+
}
|
|
1593
|
+
|
|
1385
1594
|
/** Decode one key sequence (from TerminalInputDecoder) into an input. */
|
|
1386
1595
|
input(value: string | { type: "paste"; text: string }): AppInput {
|
|
1387
1596
|
if (typeof value !== "string")
|
|
@@ -1485,7 +1694,7 @@ export class TuiApp {
|
|
|
1485
1694
|
}
|
|
1486
1695
|
if (this.overlay === "text" && this.text) {
|
|
1487
1696
|
const nav = overlayKey(value);
|
|
1488
|
-
const page =
|
|
1697
|
+
const page = this.overlayRows();
|
|
1489
1698
|
if (nav && nav !== "enter") {
|
|
1490
1699
|
const step =
|
|
1491
1700
|
nav === "down"
|
|
@@ -1497,19 +1706,22 @@ export class TuiApp {
|
|
|
1497
1706
|
: nav === "pgup"
|
|
1498
1707
|
? -page
|
|
1499
1708
|
: 0;
|
|
1500
|
-
|
|
1709
|
+
// Clamp to the last full page, the same bound paintText uses, so
|
|
1710
|
+
// End then ↑ moves the view on the first press.
|
|
1711
|
+
const max = Math.max(0, this.text.lines.length - page);
|
|
1712
|
+
const from = Math.min(max, this.text.scroll);
|
|
1501
1713
|
this.text.scroll =
|
|
1502
1714
|
nav === "home"
|
|
1503
1715
|
? 0
|
|
1504
1716
|
: nav === "end"
|
|
1505
|
-
?
|
|
1506
|
-
: Math.max(0, Math.min(
|
|
1717
|
+
? max
|
|
1718
|
+
: Math.max(0, Math.min(max, from + step));
|
|
1507
1719
|
return { type: "overlay" };
|
|
1508
1720
|
}
|
|
1509
1721
|
}
|
|
1510
1722
|
if (this.overlay === "log") {
|
|
1511
1723
|
const nav = overlayKey(value);
|
|
1512
|
-
const page =
|
|
1724
|
+
const page = this.overlayRows();
|
|
1513
1725
|
if (nav && nav !== "enter") {
|
|
1514
1726
|
const step =
|
|
1515
1727
|
nav === "up"
|
|
@@ -1525,12 +1737,15 @@ export class TuiApp {
|
|
|
1525
1737
|
this.activity.transcript,
|
|
1526
1738
|
this.log.filter,
|
|
1527
1739
|
).length;
|
|
1740
|
+
// Scroll counts rows up from the newest; paintOverlay's bound.
|
|
1741
|
+
const max = Math.max(0, total - page);
|
|
1742
|
+
const from = Math.min(max, this.log.scroll);
|
|
1528
1743
|
this.log.scroll =
|
|
1529
1744
|
nav === "home"
|
|
1530
|
-
?
|
|
1745
|
+
? max
|
|
1531
1746
|
: nav === "end"
|
|
1532
1747
|
? 0
|
|
1533
|
-
: Math.max(0, Math.min(
|
|
1748
|
+
: Math.max(0, Math.min(max, from + step));
|
|
1534
1749
|
return { type: "overlay" };
|
|
1535
1750
|
}
|
|
1536
1751
|
if (key.type === "text" && key.text === "/") {
|
|
@@ -1624,6 +1839,19 @@ export class TuiApp {
|
|
|
1624
1839
|
if (this.overlay === "picker") this.overlay = undefined;
|
|
1625
1840
|
}
|
|
1626
1841
|
|
|
1842
|
+
/**
|
|
1843
|
+
* Rows of text a panel over the highway shows (its height less the two
|
|
1844
|
+
* borders), as last rendered; a guess from the terminal height before the
|
|
1845
|
+
* first frame.
|
|
1846
|
+
*/
|
|
1847
|
+
private overlayRows(): number {
|
|
1848
|
+
const highway = this.lastFrame?.layout.highway.height;
|
|
1849
|
+
return Math.max(
|
|
1850
|
+
1,
|
|
1851
|
+
highway !== undefined && highway > 2 ? highway - 2 : this.io.rows() - 10,
|
|
1852
|
+
);
|
|
1853
|
+
}
|
|
1854
|
+
|
|
1627
1855
|
/** Show static lines over the highway (`/help`, lists); replaces any overlay. */
|
|
1628
1856
|
openText(title: string, lines: readonly string[]): void {
|
|
1629
1857
|
this.text = { title, lines: lines.slice(0, 512), scroll: 0 };
|
|
@@ -1681,9 +1909,10 @@ export class TuiApp {
|
|
|
1681
1909
|
|
|
1682
1910
|
/**
|
|
1683
1911
|
* Handle TUI-local slash commands (`/log`, `/theme`, `/motion`, `/guide`).
|
|
1684
|
-
* Returns a receipt
|
|
1912
|
+
* Returns a receipt (a structured one for a refusal), or undefined when
|
|
1913
|
+
* the command is not a UI command.
|
|
1685
1914
|
*/
|
|
1686
|
-
command(text: string): string | undefined {
|
|
1915
|
+
command(text: string): string | Receipt | undefined {
|
|
1687
1916
|
const command = text.trim();
|
|
1688
1917
|
if (/^\/(log|transcript)$/i.test(command)) {
|
|
1689
1918
|
this.overlay = this.overlay === "log" ? undefined : "log";
|
|
@@ -1708,7 +1937,12 @@ export class TuiApp {
|
|
|
1708
1937
|
if (guide) {
|
|
1709
1938
|
const topic = guide[1]?.trim();
|
|
1710
1939
|
if (!this.openGuide(topic))
|
|
1711
|
-
return
|
|
1940
|
+
return fail(
|
|
1941
|
+
topicMiss(
|
|
1942
|
+
topic!,
|
|
1943
|
+
(this.guide?.guides ?? listGuides()).map((g) => g.id),
|
|
1944
|
+
),
|
|
1945
|
+
);
|
|
1712
1946
|
return topic
|
|
1713
1947
|
? `guide · ${this.guide?.guides.find((g) => g.id === this.guide?.page)?.title ?? topic} · esc back`
|
|
1714
1948
|
: "guides · → open · esc closes";
|