@hraness/dawg 0.2.0 → 0.4.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 +201 -0
- package/DAWG.md +762 -11
- package/README.md +79 -30
- package/core/chords.ts +1724 -0
- package/core/diff.ts +261 -0
- package/core/euclid.ts +670 -0
- package/core/fx.ts +1065 -0
- package/core/key.ts +43 -0
- package/core/kits.ts +320 -0
- package/core/params.ts +111 -0
- package/core/pitch.ts +60 -0
- package/core/rhythm.ts +287 -0
- package/core/score.ts +1054 -14
- package/core/sdk/eval-child.ts +125 -0
- package/core/sdk/eval.ts +260 -0
- package/core/sdk/print.ts +675 -0
- package/core/sdk/sync-chords.ts +54 -0
- package/core/sdk/v1.ts +4159 -0
- package/core/slug.ts +19 -0
- package/core/synth.ts +1001 -0
- package/package.json +8 -5
- package/src/agent/agent.ts +317 -16
- package/src/agent/brief.ts +50 -5
- package/src/agent/chord-tools.ts +357 -0
- package/src/agent/drum-tools.ts +135 -0
- package/src/agent/gateway.ts +215 -41
- package/src/agent/models.ts +633 -0
- package/src/agent/ops.ts +3 -18
- package/src/agent/pack-tools.ts +369 -0
- package/src/agent/planner.ts +55 -2
- package/src/agent/preview-tool.ts +270 -0
- package/src/agent/provider.ts +234 -106
- package/src/agent/rhythm-tools.ts +145 -0
- package/src/agent/sse.ts +31 -7
- package/src/agent/tools.ts +705 -14
- package/src/agent/usage.ts +296 -0
- package/src/agent/workspace.ts +683 -0
- package/src/agent/xcb-agent.ts +35 -14
- package/src/agent/xcb.ts +233 -21
- package/src/audio/audition.ts +93 -0
- package/src/audio/cache.ts +160 -0
- package/src/audio/click.ts +125 -0
- package/src/audio/effects/bus.ts +147 -0
- package/src/audio/effects/chain.ts +99 -0
- package/src/audio/effects/common.ts +251 -0
- package/src/audio/effects/convolution.ts +301 -0
- package/src/audio/effects/drive.ts +142 -0
- package/src/audio/effects/duck.ts +122 -0
- package/src/audio/effects/dynamics.ts +121 -0
- package/src/audio/effects/filter.ts +319 -0
- package/src/audio/effects/modulation.ts +174 -0
- package/src/audio/effects/space.ts +337 -0
- package/src/audio/engine.ts +430 -33
- package/src/audio/kits.ts +200 -0
- package/src/audio/live.ts +152 -0
- package/src/audio/packs.ts +1787 -0
- package/src/audio/player.ts +19 -4
- package/src/audio/preview.ts +470 -0
- package/src/audio/random.ts +15 -0
- package/src/audio/render-worker.ts +68 -0
- package/src/audio/renderer.ts +174 -0
- package/src/audio/sampler.ts +420 -0
- package/src/audio/samples.ts +1025 -0
- package/src/audio/synth/oscillators.ts +268 -0
- package/src/audio/synth/voice.ts +555 -0
- package/src/audio/synth/zzfx.ts +137 -0
- package/src/audio/wav.ts +550 -334
- package/src/audio/wavetable-maker.ts +717 -0
- package/src/audio/wavetable.ts +624 -0
- package/src/auth/cli.ts +146 -27
- package/src/auth/credentials.ts +167 -41
- package/src/auth/discover.ts +481 -0
- package/src/auth/login.ts +885 -128
- package/src/auth/openrouter.ts +206 -0
- package/src/auth/picker.ts +282 -0
- package/src/auth/runner.ts +25 -2
- package/src/auth/tui.ts +60 -43
- package/src/commands/drums.ts +299 -0
- package/src/commands/edit.ts +192 -0
- package/src/commands/fx.ts +360 -0
- package/src/commands/help.ts +535 -0
- package/src/commands/history.ts +32 -19
- package/src/commands/music.ts +34 -15
- package/src/commands/pack.ts +423 -0
- package/src/commands/rhythm.ts +230 -0
- package/src/commands/sample.ts +462 -0
- package/src/commands/synth.ts +229 -0
- package/src/commands/wavetable.ts +370 -0
- package/src/main.ts +2136 -170
- package/src/media/analyze.ts +364 -0
- package/src/media/backend.ts +253 -0
- package/src/media/cli.ts +186 -0
- package/src/media/download.ts +281 -0
- package/src/media/dsp.ts +281 -0
- package/src/media/import.ts +130 -0
- package/src/media/lyrics.ts +201 -0
- package/src/media/notes.ts +363 -0
- package/src/media/paths.ts +168 -0
- package/src/media/process.ts +226 -0
- package/src/media/registry.ts +9 -0
- package/src/media/sidecar.ts +72 -0
- package/src/media/stemdeck.ts +254 -0
- package/src/media/stems.ts +173 -0
- package/src/media/tools.ts +397 -0
- package/src/media/types.ts +92 -0
- package/src/media/vendor/basic-pitch.ts +261 -0
- package/src/media/vendor/drums.ts +817 -0
- package/src/media/vendor/grid.ts +203 -0
- package/src/media/vendor/util.ts +139 -0
- package/src/media/vendor/wav.ts +233 -0
- package/src/media/wavetable.ts +202 -0
- package/src/project/check.ts +80 -0
- package/src/project/init.ts +253 -0
- package/src/project/sync.ts +432 -0
- package/src/project/typecheck.ts +149 -0
- package/src/render.ts +49 -6
- package/src/session/attach.ts +3 -4
- package/src/session/daemon.ts +25 -8
- package/src/session/delta.ts +249 -0
- package/src/session/naming.ts +6 -38
- package/src/session/port.ts +26 -6
- package/src/session/rebase.ts +38 -8
- package/src/session/store.ts +116 -21
- package/src/tui/audition.ts +501 -0
- package/src/tui/euclid.ts +472 -0
- package/src/tui/menu.ts +2125 -0
- package/src/tui/play-chords.ts +538 -0
- package/src/tui/play-mode.ts +442 -0
- package/src/tui/play-session.ts +984 -0
- package/src/tui/sketch.ts +108 -0
- package/src/web/fetch.ts +340 -0
- package/src/web/http.ts +137 -0
- package/src/web/search.ts +684 -0
- package/tui/activity.ts +42 -3
- package/tui/app.ts +398 -33
- package/tui/drums.ts +44 -0
- package/tui/grammar.ts +281 -0
- package/tui/highway.ts +24 -2
- package/tui/layers.ts +14 -2
- package/tui/play-strip.ts +188 -0
package/tui/app.ts
CHANGED
|
@@ -24,8 +24,15 @@ import {
|
|
|
24
24
|
resolveBeat,
|
|
25
25
|
type TrackScoreSnapshot,
|
|
26
26
|
} from "./highway.ts";
|
|
27
|
+
import { asciiHint, fitHint, HINTS } from "./grammar.ts";
|
|
27
28
|
import { classifyKey, overlayKey, type UiCommand } from "./keys.ts";
|
|
28
29
|
import { PromptModel, type PromptAction, type PromptMode } from "./prompt.ts";
|
|
30
|
+
import {
|
|
31
|
+
paintChordLegend,
|
|
32
|
+
paintPlayHeader,
|
|
33
|
+
paintPlayStrip,
|
|
34
|
+
type PlayHeaderView,
|
|
35
|
+
} from "./play-strip.ts";
|
|
29
36
|
import { CellBuffer, ScreenWriter, type CursorPosition } from "./screen.ts";
|
|
30
37
|
import { displayWidth, truncate } from "./text.ts";
|
|
31
38
|
import {
|
|
@@ -54,18 +61,45 @@ export interface AppView {
|
|
|
54
61
|
sessionName?: string | undefined;
|
|
55
62
|
/** Live windows on this session (presence); shown when more than one. */
|
|
56
63
|
windows?: number | undefined;
|
|
64
|
+
/** Project typecheck result; `types ✓` or `types ✗ N` beside sync. */
|
|
65
|
+
types?: TypesIndicator | undefined;
|
|
66
|
+
/**
|
|
67
|
+
* The line under the prompt: `$0.12 session · $0.48 today · opus-5.5 ·
|
|
68
|
+
* gateway`, or `no model · dawg login`. The caller sizes it to the width.
|
|
69
|
+
*/
|
|
70
|
+
spend?: string | undefined;
|
|
71
|
+
/** No agent provider: the placeholder teaches commands, no STEER pill. */
|
|
72
|
+
agentOffline?: boolean | undefined;
|
|
73
|
+
/** Play mode: replaces the header and adds the keyboard strip row. */
|
|
74
|
+
play?: PlayHeaderView | undefined;
|
|
57
75
|
}
|
|
58
76
|
|
|
77
|
+
export type TypesIndicator = Readonly<{ ok: boolean; errors: number }>;
|
|
78
|
+
|
|
59
79
|
export interface UiState {
|
|
60
80
|
prompt: PromptModel;
|
|
61
81
|
activity: ActivityFeed;
|
|
62
82
|
theme: Theme;
|
|
63
83
|
capabilities: TerminalCapabilities;
|
|
64
84
|
reducedMotion: boolean;
|
|
65
|
-
overlay:
|
|
85
|
+
overlay: Overlay;
|
|
66
86
|
/** Transcript scroll (entries up from the newest) and filter. */
|
|
67
87
|
log?: LogView | undefined;
|
|
68
88
|
picker?: PickerState | undefined;
|
|
89
|
+
text?: TextView | undefined;
|
|
90
|
+
/** The `?` panel: keys for the screen underneath, drawn over it. */
|
|
91
|
+
keys?: TextView | undefined;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** `log` is the transcript, `picker` an arrow-key list, `text` static lines. */
|
|
95
|
+
export type Overlay = "log" | "picker" | "text" | undefined;
|
|
96
|
+
|
|
97
|
+
/** A scrollable read-only panel (`/help`, `/sessions`, `/tracks`). */
|
|
98
|
+
export interface TextView {
|
|
99
|
+
title: string;
|
|
100
|
+
lines: readonly string[];
|
|
101
|
+
/** Rows scrolled down from the top. */
|
|
102
|
+
scroll: number;
|
|
69
103
|
}
|
|
70
104
|
|
|
71
105
|
export type LogFilter = "all" | "requests" | "ops" | "errors";
|
|
@@ -88,6 +122,22 @@ export interface PickerState {
|
|
|
88
122
|
title: string;
|
|
89
123
|
items: readonly PickerItem[];
|
|
90
124
|
index: number;
|
|
125
|
+
/** Type-to-filter on label and detail (`/model`). */
|
|
126
|
+
filterable?: boolean | undefined;
|
|
127
|
+
/** Current filter text; `items` is then the matching subset of `all`. */
|
|
128
|
+
query?: string | undefined;
|
|
129
|
+
/** `/` was pressed: printable keys type into the filter. */
|
|
130
|
+
filtering?: boolean | undefined;
|
|
131
|
+
all?: readonly PickerItem[] | undefined;
|
|
132
|
+
/** Footer keys, replacing the default move/choose/cancel hint. */
|
|
133
|
+
hint?: string | undefined;
|
|
134
|
+
/** A dim line under the rows: what the focused row does. */
|
|
135
|
+
note?: string | undefined;
|
|
136
|
+
/**
|
|
137
|
+
* Hosts the audition loop (src/tui/audition.ts): Space, `a` and `c` are
|
|
138
|
+
* returned as `pick-audition` instead of being swallowed.
|
|
139
|
+
*/
|
|
140
|
+
audition?: boolean | undefined;
|
|
91
141
|
}
|
|
92
142
|
|
|
93
143
|
export interface PickerItem {
|
|
@@ -95,6 +145,8 @@ export interface PickerItem {
|
|
|
95
145
|
/** Opaque value returned on Enter. */
|
|
96
146
|
value: string;
|
|
97
147
|
detail?: string | undefined;
|
|
148
|
+
/** Marked with ● (the current model). */
|
|
149
|
+
current?: boolean | undefined;
|
|
98
150
|
}
|
|
99
151
|
|
|
100
152
|
/** Most picker rows kept; longer lists are truncated by the caller's order. */
|
|
@@ -350,6 +402,14 @@ function paintHeader(
|
|
|
350
402
|
style: roles.text,
|
|
351
403
|
priority: 1,
|
|
352
404
|
});
|
|
405
|
+
if (view.types)
|
|
406
|
+
right.push({
|
|
407
|
+
text: view.types.ok
|
|
408
|
+
? `types ${unicode ? "✓" : "ok"}`
|
|
409
|
+
: `types ${unicode ? "✗" : "x"} ${view.types.errors}`,
|
|
410
|
+
style: view.types.ok ? roles.success : roles.error,
|
|
411
|
+
priority: 4,
|
|
412
|
+
});
|
|
353
413
|
const sync = syncSegment(view.sync, theme, unicode);
|
|
354
414
|
if (sync) right.push(sync);
|
|
355
415
|
paintSegments(buffer, 0, left, width, " · ", roles.faint, roles.panel, right);
|
|
@@ -500,9 +560,14 @@ function paintPrompt(
|
|
|
500
560
|
const mode = prompt.snapshot.mode;
|
|
501
561
|
const pillStyle = mode === "queue" ? roles.pillQueue : roles.pillSteer;
|
|
502
562
|
let x = 2;
|
|
503
|
-
x += buffer.text(x, top, pill(mode), pillStyle);
|
|
563
|
+
if (!view.agentOffline) x += buffer.text(x, top, pill(mode), pillStyle);
|
|
504
564
|
const status: string[] = [];
|
|
505
|
-
|
|
565
|
+
// The model shows once, in the header; the spend line carries it here.
|
|
566
|
+
// Views without a spend line (embedders such as the site demo) keep the
|
|
567
|
+
// model label.
|
|
568
|
+
if (view.spend) {
|
|
569
|
+
if (!layout.footer) status.push(view.spend);
|
|
570
|
+
} else if (view.model) status.push(view.model);
|
|
506
571
|
if (activity.queueDepth > 0) status.push(`queue ${activity.queueDepth}`);
|
|
507
572
|
const layoutInfo = prompt.layout(rows);
|
|
508
573
|
if (layoutInfo.total > rows)
|
|
@@ -541,8 +606,9 @@ function paintPrompt(
|
|
|
541
606
|
buffer.text(width - 3, y, capabilities.unicode ? "↓" : "v", faint);
|
|
542
607
|
}
|
|
543
608
|
if (prompt.value.length === 0) {
|
|
544
|
-
const placeholder =
|
|
545
|
-
|
|
609
|
+
const placeholder = view.agentOffline
|
|
610
|
+
? "try: tempo 96 · add C4 at 0 · /help (dawg login enables the agent)"
|
|
611
|
+
: mode === "queue"
|
|
546
612
|
? "queue a request for after the current one…"
|
|
547
613
|
: "describe a change — “add a walking bass in A minor”";
|
|
548
614
|
buffer.text(5, top + 1, truncate(placeholder, editorWidth - 1), faint);
|
|
@@ -563,8 +629,19 @@ function paintPrompt(
|
|
|
563
629
|
: width >= 44
|
|
564
630
|
? " enter · ^q queue · ^z undo · ^o log "
|
|
565
631
|
: "";
|
|
566
|
-
const
|
|
632
|
+
const spend = view.spend ? ` ${view.spend} ` : "";
|
|
633
|
+
const spendWidth = displayWidth(spend);
|
|
634
|
+
// Spend wins over hints when both do not fit.
|
|
635
|
+
const shown =
|
|
636
|
+
hints && displayWidth(hints) + spendWidth + 6 <= width ? hints : "";
|
|
567
637
|
if (shown) buffer.text(2, y, shown, onBackground(roles.muted, panel));
|
|
638
|
+
if (spend && spendWidth + 4 <= width)
|
|
639
|
+
buffer.text(
|
|
640
|
+
width - 2 - spendWidth,
|
|
641
|
+
y,
|
|
642
|
+
spend,
|
|
643
|
+
onBackground(roles.muted, panel),
|
|
644
|
+
);
|
|
568
645
|
}
|
|
569
646
|
|
|
570
647
|
// Cursor: shown as a reverse cell and as the real terminal cursor.
|
|
@@ -637,11 +714,10 @@ function paintOverlay(
|
|
|
637
714
|
onBackground({ ...roles.text, bold: true }, panel),
|
|
638
715
|
boxWidth - 4,
|
|
639
716
|
);
|
|
640
|
-
const
|
|
641
|
-
|
|
642
|
-
if (boxWidth > hintText.length + 4)
|
|
717
|
+
const hintText = footerHint(HINTS.log, boxWidth - 4, capabilities.unicode);
|
|
718
|
+
if (hintText)
|
|
643
719
|
buffer.text(
|
|
644
|
-
left + boxWidth - 2 - hintText
|
|
720
|
+
left + boxWidth - 2 - displayWidth(hintText),
|
|
645
721
|
top + height - 1,
|
|
646
722
|
hintText,
|
|
647
723
|
onBackground(roles.muted, panel),
|
|
@@ -678,6 +754,70 @@ function paintOverlay(
|
|
|
678
754
|
});
|
|
679
755
|
}
|
|
680
756
|
|
|
757
|
+
/** Static lines in a bordered panel; the title sits on the top border. */
|
|
758
|
+
function paintText(
|
|
759
|
+
buffer: CellBuffer,
|
|
760
|
+
ui: UiState,
|
|
761
|
+
region: { y: number; height: number },
|
|
762
|
+
width: number,
|
|
763
|
+
options: { text?: TextView | undefined; hint?: string } = {},
|
|
764
|
+
): void {
|
|
765
|
+
const text = options.text ?? ui.text;
|
|
766
|
+
if (!text) return;
|
|
767
|
+
const roles = ui.theme.roles;
|
|
768
|
+
const left = width >= 60 ? 2 : 0;
|
|
769
|
+
const boxWidth = width - left * 2;
|
|
770
|
+
const height = region.height;
|
|
771
|
+
if (height < 3 || boxWidth < 10) return;
|
|
772
|
+
const panel = paintBox(buffer, ui, {
|
|
773
|
+
left,
|
|
774
|
+
top: region.y,
|
|
775
|
+
width: boxWidth,
|
|
776
|
+
height,
|
|
777
|
+
});
|
|
778
|
+
const inner = height - 2;
|
|
779
|
+
const maxScroll = Math.max(0, text.lines.length - inner);
|
|
780
|
+
const scroll = Math.max(0, Math.min(maxScroll, text.scroll));
|
|
781
|
+
const position =
|
|
782
|
+
maxScroll > 0
|
|
783
|
+
? ` · ${scroll + 1}-${scroll + inner}/${text.lines.length}`
|
|
784
|
+
: "";
|
|
785
|
+
buffer.text(
|
|
786
|
+
left + 2,
|
|
787
|
+
region.y,
|
|
788
|
+
` ${text.title}${position} `,
|
|
789
|
+
onBackground({ ...roles.text, bold: true }, panel),
|
|
790
|
+
boxWidth - 4,
|
|
791
|
+
);
|
|
792
|
+
const hint = footerHint(
|
|
793
|
+
options.hint ?? HINTS.text,
|
|
794
|
+
boxWidth - 4,
|
|
795
|
+
ui.capabilities.unicode,
|
|
796
|
+
);
|
|
797
|
+
if (hint)
|
|
798
|
+
buffer.text(
|
|
799
|
+
left + boxWidth - 2 - displayWidth(hint),
|
|
800
|
+
region.y + height - 1,
|
|
801
|
+
hint,
|
|
802
|
+
onBackground(roles.muted, panel),
|
|
803
|
+
);
|
|
804
|
+
text.lines.slice(scroll, scroll + inner).forEach((line, index) => {
|
|
805
|
+
const heading = line.startsWith("── ");
|
|
806
|
+
buffer.text(
|
|
807
|
+
left + 2,
|
|
808
|
+
region.y + 1 + index,
|
|
809
|
+
truncate(
|
|
810
|
+
heading && !ui.capabilities.unicode ? line.replace("── ", "-- ") : line,
|
|
811
|
+
boxWidth - 4,
|
|
812
|
+
),
|
|
813
|
+
onBackground(
|
|
814
|
+
heading ? { ...roles.borderFocus, bold: true } : roles.text,
|
|
815
|
+
panel,
|
|
816
|
+
),
|
|
817
|
+
);
|
|
818
|
+
});
|
|
819
|
+
}
|
|
820
|
+
|
|
681
821
|
function paintBox(
|
|
682
822
|
buffer: CellBuffer,
|
|
683
823
|
ui: UiState,
|
|
@@ -715,7 +855,11 @@ function paintPicker(
|
|
|
715
855
|
const roles = ui.theme.roles;
|
|
716
856
|
const left = width >= 60 ? 2 : 0;
|
|
717
857
|
const boxWidth = width - left * 2;
|
|
718
|
-
const
|
|
858
|
+
const noteRows = picker.note && region.height >= 6 ? 1 : 0;
|
|
859
|
+
const height = Math.min(
|
|
860
|
+
region.height,
|
|
861
|
+
Math.max(1, picker.items.length) + 2 + noteRows,
|
|
862
|
+
);
|
|
719
863
|
if (height < 3 || boxWidth < 10) return;
|
|
720
864
|
const panel = paintBox(buffer, ui, {
|
|
721
865
|
left,
|
|
@@ -726,25 +870,49 @@ function paintPicker(
|
|
|
726
870
|
buffer.text(
|
|
727
871
|
left + 2,
|
|
728
872
|
region.y,
|
|
729
|
-
` ${picker.title} `,
|
|
873
|
+
` ${picker.title}${picker.filtering || picker.query ? ` · /${picker.query ?? ""}${picker.filtering ? "▏" : ""}` : ""} `,
|
|
730
874
|
onBackground({ ...roles.text, bold: true }, panel),
|
|
731
875
|
boxWidth - 4,
|
|
732
876
|
);
|
|
733
|
-
const hint =
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
877
|
+
const hint = footerHint(
|
|
878
|
+
picker.filtering
|
|
879
|
+
? HINTS.filtering
|
|
880
|
+
: (picker.hint ??
|
|
881
|
+
(picker.filterable
|
|
882
|
+
? HINTS.list
|
|
883
|
+
: HINTS.list.replace(" · / filter", ""))),
|
|
884
|
+
boxWidth - 4,
|
|
885
|
+
ui.capabilities.unicode,
|
|
886
|
+
);
|
|
887
|
+
if (hint)
|
|
737
888
|
buffer.text(
|
|
738
|
-
left + boxWidth - 2 - hint
|
|
889
|
+
left + boxWidth - 2 - displayWidth(hint),
|
|
739
890
|
region.y + height - 1,
|
|
740
891
|
hint,
|
|
741
892
|
onBackground(roles.muted, panel),
|
|
742
893
|
);
|
|
743
|
-
const inner = height - 2;
|
|
894
|
+
const inner = height - 2 - noteRows;
|
|
895
|
+
if (noteRows && picker.note)
|
|
896
|
+
buffer.text(
|
|
897
|
+
left + 2,
|
|
898
|
+
region.y + height - 2,
|
|
899
|
+
truncate(picker.note, boxWidth - 4),
|
|
900
|
+
onBackground(roles.muted, panel),
|
|
901
|
+
);
|
|
744
902
|
const first = Math.max(
|
|
745
903
|
0,
|
|
746
904
|
Math.min(picker.items.length - inner, picker.index - inner + 1),
|
|
747
905
|
);
|
|
906
|
+
const marks = (picker.all ?? picker.items).some(
|
|
907
|
+
(item) => item.current !== undefined,
|
|
908
|
+
);
|
|
909
|
+
if (picker.items.length === 0)
|
|
910
|
+
buffer.text(
|
|
911
|
+
left + 4,
|
|
912
|
+
region.y + 1,
|
|
913
|
+
"no matches",
|
|
914
|
+
onBackground(roles.muted, panel),
|
|
915
|
+
);
|
|
748
916
|
picker.items.slice(first, first + inner).forEach((item, offset) => {
|
|
749
917
|
const index = first + offset;
|
|
750
918
|
const y = region.y + 1 + offset;
|
|
@@ -754,11 +922,25 @@ function paintPicker(
|
|
|
754
922
|
? onBackground({ ...roles.borderFocus, bold: true }, panel)
|
|
755
923
|
: onBackground(roles.text, panel);
|
|
756
924
|
buffer.text(left + 2, y, marker, style);
|
|
757
|
-
|
|
758
|
-
const
|
|
925
|
+
// A column for the current-item mark only when the picker uses one.
|
|
926
|
+
const pad = marks ? 2 : 0;
|
|
927
|
+
if (item.current)
|
|
928
|
+
buffer.text(
|
|
929
|
+
left + 4,
|
|
930
|
+
y,
|
|
931
|
+
ui.capabilities.unicode ? "●" : "*",
|
|
932
|
+
onBackground(roles.success, panel),
|
|
933
|
+
);
|
|
934
|
+
const room = boxWidth - 6 - pad;
|
|
935
|
+
const used = buffer.text(
|
|
936
|
+
left + 4 + pad,
|
|
937
|
+
y,
|
|
938
|
+
truncate(item.label, room),
|
|
939
|
+
style,
|
|
940
|
+
);
|
|
759
941
|
if (item.detail && used + 3 < room)
|
|
760
942
|
buffer.text(
|
|
761
|
-
left + 4 + used + 2,
|
|
943
|
+
left + 4 + pad + used + 2,
|
|
762
944
|
y,
|
|
763
945
|
truncate(item.detail, room - used - 2),
|
|
764
946
|
onBackground(roles.muted, panel),
|
|
@@ -766,6 +948,24 @@ function paintPicker(
|
|
|
766
948
|
});
|
|
767
949
|
}
|
|
768
950
|
|
|
951
|
+
/** The `?` panel sits at the bottom of the highway, as tall as it needs. */
|
|
952
|
+
function keysRegion(
|
|
953
|
+
region: { y: number; height: number },
|
|
954
|
+
keys: TextView,
|
|
955
|
+
): { y: number; height: number } {
|
|
956
|
+
const height = Math.min(region.height, keys.lines.length + 2);
|
|
957
|
+
return { y: region.y + region.height - height, height };
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
/** A footer hint fitted to `width`, spelled in ASCII when needed. */
|
|
961
|
+
export function footerHint(
|
|
962
|
+
hint: string,
|
|
963
|
+
width: number,
|
|
964
|
+
unicode: boolean,
|
|
965
|
+
): string {
|
|
966
|
+
return fitHint(unicode ? hint : asciiHint(hint), width);
|
|
967
|
+
}
|
|
968
|
+
|
|
769
969
|
// ---------------------------------------------------------------------------
|
|
770
970
|
|
|
771
971
|
function paintTooSmall(buffer: CellBuffer, ui: UiState, size: FrameSize): void {
|
|
@@ -802,12 +1002,43 @@ export function composeFrame(
|
|
|
802
1002
|
paintTooSmall(buffer, ui, { width, height });
|
|
803
1003
|
return { buffer, cursor: undefined, promptRows: 0, layout };
|
|
804
1004
|
}
|
|
805
|
-
|
|
1005
|
+
if (view.play) {
|
|
1006
|
+
paintPlayHeader(
|
|
1007
|
+
buffer,
|
|
1008
|
+
layout.header,
|
|
1009
|
+
width,
|
|
1010
|
+
view.play,
|
|
1011
|
+
ui.theme,
|
|
1012
|
+
ui.capabilities.unicode,
|
|
1013
|
+
);
|
|
1014
|
+
if (layout.highway.height > 1) {
|
|
1015
|
+
paintPlayStrip(buffer, layout.highway.y, width, view.play.keys, ui.theme);
|
|
1016
|
+
layout.highway = {
|
|
1017
|
+
y: layout.highway.y + 1,
|
|
1018
|
+
height: layout.highway.height - 1,
|
|
1019
|
+
};
|
|
1020
|
+
}
|
|
1021
|
+
if (view.play.legend && layout.highway.height > 4) {
|
|
1022
|
+
paintChordLegend(
|
|
1023
|
+
buffer,
|
|
1024
|
+
layout.highway.y,
|
|
1025
|
+
width,
|
|
1026
|
+
view.play.legend,
|
|
1027
|
+
ui.theme,
|
|
1028
|
+
);
|
|
1029
|
+
layout.highway = {
|
|
1030
|
+
y: layout.highway.y + 1,
|
|
1031
|
+
height: layout.highway.height - 1,
|
|
1032
|
+
};
|
|
1033
|
+
}
|
|
1034
|
+
} else paintHeader(buffer, view, ui, width);
|
|
806
1035
|
const beat = view.beat ?? resolveBeat(view.score, nowMs);
|
|
807
1036
|
if (layout.highway.height > 0) {
|
|
808
1037
|
if (ui.overlay === "log") paintOverlay(buffer, ui, layout.highway, width);
|
|
809
1038
|
else if (ui.overlay === "picker" && ui.picker)
|
|
810
1039
|
paintPicker(buffer, ui, layout.highway, width);
|
|
1040
|
+
else if (ui.overlay === "text" && ui.text)
|
|
1041
|
+
paintText(buffer, ui, layout.highway, width);
|
|
811
1042
|
else
|
|
812
1043
|
paintHighway(
|
|
813
1044
|
buffer,
|
|
@@ -821,6 +1052,11 @@ export function composeFrame(
|
|
|
821
1052
|
},
|
|
822
1053
|
);
|
|
823
1054
|
}
|
|
1055
|
+
if (ui.keys && layout.highway.height > 0)
|
|
1056
|
+
paintText(buffer, ui, keysRegion(layout.highway, ui.keys), width, {
|
|
1057
|
+
text: ui.keys,
|
|
1058
|
+
hint: HINTS.keys,
|
|
1059
|
+
});
|
|
824
1060
|
paintActivity(buffer, layout.activity, ui, width, nowMs);
|
|
825
1061
|
const prompt = paintPrompt(buffer, view, ui, layout, width);
|
|
826
1062
|
return { buffer, cursor: prompt.cursor, promptRows: prompt.rows, layout };
|
|
@@ -852,6 +1088,14 @@ export type AppInput =
|
|
|
852
1088
|
| { type: "pick"; picker: string; value: string }
|
|
853
1089
|
/** Esc on a picker. */
|
|
854
1090
|
| { type: "pick-cancel"; picker: string }
|
|
1091
|
+
/**
|
|
1092
|
+
* The highlighted row changed (move, page, filter): the hook for live
|
|
1093
|
+
* previews (/pattern today; an audition controller can listen here too).
|
|
1094
|
+
* Pickers raise `pick-move` → `pick` (commit) or `pick-cancel` (cancel).
|
|
1095
|
+
*/
|
|
1096
|
+
| { type: "pick-move"; picker: string; value: string }
|
|
1097
|
+
/** Space, `a` or `c` on a picker that hosts the audition loop. */
|
|
1098
|
+
| { type: "pick-audition"; picker: string; key: "loop" | "ab" | "context" }
|
|
855
1099
|
/** Consumed by an overlay (scroll, filter, move). */
|
|
856
1100
|
| { type: "overlay" }
|
|
857
1101
|
| { type: "none" };
|
|
@@ -864,9 +1108,10 @@ export class TuiApp {
|
|
|
864
1108
|
capabilities: TerminalCapabilities;
|
|
865
1109
|
themeName: ThemeName;
|
|
866
1110
|
reducedMotion: boolean;
|
|
867
|
-
overlay:
|
|
1111
|
+
overlay: Overlay;
|
|
868
1112
|
log: LogView = { scroll: 0, filter: "all" };
|
|
869
1113
|
picker: PickerState | undefined;
|
|
1114
|
+
text: TextView | undefined;
|
|
870
1115
|
/** `/view all` overlays every unmuted track; `/view focus` shows one. */
|
|
871
1116
|
highwayView: "all" | "focus" = "all";
|
|
872
1117
|
private writer: ScreenWriter;
|
|
@@ -902,6 +1147,8 @@ export class TuiApp {
|
|
|
902
1147
|
overlay: this.overlay,
|
|
903
1148
|
log: this.log,
|
|
904
1149
|
picker: this.picker,
|
|
1150
|
+
text: this.text,
|
|
1151
|
+
keys: this.keys,
|
|
905
1152
|
};
|
|
906
1153
|
}
|
|
907
1154
|
|
|
@@ -950,15 +1197,17 @@ export class TuiApp {
|
|
|
950
1197
|
if (nav === "up" || nav === "down" || nav === "pgup" || nav === "pgdn") {
|
|
951
1198
|
const step =
|
|
952
1199
|
nav === "up" ? -1 : nav === "down" ? 1 : nav === "pgup" ? -8 : 8;
|
|
1200
|
+
// Moving ends typing into the filter; the filter itself stays.
|
|
1201
|
+
picker.filtering = false;
|
|
953
1202
|
picker.index = Math.max(
|
|
954
1203
|
0,
|
|
955
1204
|
Math.min(picker.items.length - 1, picker.index + step),
|
|
956
1205
|
);
|
|
957
|
-
return
|
|
1206
|
+
return this.pickerMoved(picker);
|
|
958
1207
|
}
|
|
959
1208
|
if (nav === "home" || nav === "end") {
|
|
960
1209
|
picker.index = nav === "home" ? 0 : picker.items.length - 1;
|
|
961
|
-
return
|
|
1210
|
+
return this.pickerMoved(picker);
|
|
962
1211
|
}
|
|
963
1212
|
if (nav === "enter") {
|
|
964
1213
|
const item = picker.items[picker.index];
|
|
@@ -968,18 +1217,51 @@ export class TuiApp {
|
|
|
968
1217
|
: { type: "pick-cancel", picker: picker.id };
|
|
969
1218
|
}
|
|
970
1219
|
if (key.type === "ui" && key.command === "close-overlay") {
|
|
1220
|
+
// Esc clears the filter first, then closes.
|
|
1221
|
+
if (picker.query || picker.filtering) {
|
|
1222
|
+
this.filterPicker("");
|
|
1223
|
+
if (this.picker) this.picker.filtering = false;
|
|
1224
|
+
return { type: "overlay" };
|
|
1225
|
+
}
|
|
971
1226
|
this.closePicker();
|
|
972
1227
|
return { type: "pick-cancel", picker: picker.id };
|
|
973
1228
|
}
|
|
974
|
-
if (
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
this.
|
|
979
|
-
return { type: "
|
|
1229
|
+
if (picker.filterable && picker.filtering) {
|
|
1230
|
+
if (value === "\u007f" || value === "\b") {
|
|
1231
|
+
const query = (picker.query ?? "").slice(0, -1);
|
|
1232
|
+
this.filterPicker(query);
|
|
1233
|
+
if (!query && this.picker) this.picker.filtering = false;
|
|
1234
|
+
return { type: "overlay" };
|
|
1235
|
+
}
|
|
1236
|
+
if (key.type === "text") {
|
|
1237
|
+
this.filterPicker(((picker.query ?? "") + key.text).slice(0, 40));
|
|
1238
|
+
return { type: "overlay" };
|
|
980
1239
|
}
|
|
1240
|
+
}
|
|
1241
|
+
if (picker.audition && !picker.filtering) {
|
|
1242
|
+
const audition =
|
|
1243
|
+
value === " "
|
|
1244
|
+
? "loop"
|
|
1245
|
+
: value === "a"
|
|
1246
|
+
? "ab"
|
|
1247
|
+
: value === "c"
|
|
1248
|
+
? "context"
|
|
1249
|
+
: undefined;
|
|
1250
|
+
if (audition)
|
|
1251
|
+
return { type: "pick-audition", picker: picker.id, key: audition };
|
|
1252
|
+
}
|
|
1253
|
+
if (picker.filterable && key.type === "text" && key.text === "/") {
|
|
1254
|
+
picker.filtering = true;
|
|
981
1255
|
return { type: "overlay" };
|
|
982
1256
|
}
|
|
1257
|
+
if (key.type === "text" && (key.text === "j" || key.text === "k")) {
|
|
1258
|
+
const step = key.text === "k" ? -1 : 1;
|
|
1259
|
+
picker.index = Math.max(
|
|
1260
|
+
0,
|
|
1261
|
+
Math.min(picker.items.length - 1, picker.index + step),
|
|
1262
|
+
);
|
|
1263
|
+
return this.pickerMoved(picker);
|
|
1264
|
+
}
|
|
983
1265
|
// Quit and redraw still work; everything else is swallowed.
|
|
984
1266
|
if (
|
|
985
1267
|
key.type === "ui" &&
|
|
@@ -990,6 +1272,30 @@ export class TuiApp {
|
|
|
990
1272
|
}
|
|
991
1273
|
return { type: "overlay" };
|
|
992
1274
|
}
|
|
1275
|
+
if (this.overlay === "text" && this.text) {
|
|
1276
|
+
const nav = overlayKey(value);
|
|
1277
|
+
const page = Math.max(1, this.io.rows() - 10);
|
|
1278
|
+
if (nav && nav !== "enter") {
|
|
1279
|
+
const step =
|
|
1280
|
+
nav === "down"
|
|
1281
|
+
? 1
|
|
1282
|
+
: nav === "up"
|
|
1283
|
+
? -1
|
|
1284
|
+
: nav === "pgdn"
|
|
1285
|
+
? page
|
|
1286
|
+
: nav === "pgup"
|
|
1287
|
+
? -page
|
|
1288
|
+
: 0;
|
|
1289
|
+
const total = this.text.lines.length;
|
|
1290
|
+
this.text.scroll =
|
|
1291
|
+
nav === "home"
|
|
1292
|
+
? 0
|
|
1293
|
+
: nav === "end"
|
|
1294
|
+
? total
|
|
1295
|
+
: Math.max(0, Math.min(total, this.text.scroll + step));
|
|
1296
|
+
return { type: "overlay" };
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
993
1299
|
if (this.overlay === "log") {
|
|
994
1300
|
const nav = overlayKey(value);
|
|
995
1301
|
const page = Math.max(1, this.io.rows() - 10);
|
|
@@ -1041,8 +1347,13 @@ export class TuiApp {
|
|
|
1041
1347
|
}
|
|
1042
1348
|
if (key.command === "redraw") this.invalidate();
|
|
1043
1349
|
return { type: "ui", command: key.command };
|
|
1044
|
-
case "prompt":
|
|
1045
|
-
|
|
1350
|
+
case "prompt": {
|
|
1351
|
+
const action = this.prompt.handle(key.key);
|
|
1352
|
+
// Submitting from under a text panel closes it so the receipt shows.
|
|
1353
|
+
if (action.kind === "submit" && this.overlay === "text")
|
|
1354
|
+
this.closeText();
|
|
1355
|
+
return { type: "action", action };
|
|
1356
|
+
}
|
|
1046
1357
|
case "text": {
|
|
1047
1358
|
let action: PromptAction = {
|
|
1048
1359
|
kind: "noop",
|
|
@@ -1058,6 +1369,14 @@ export class TuiApp {
|
|
|
1058
1369
|
}
|
|
1059
1370
|
|
|
1060
1371
|
/** Show an arrow-key picker over the highway; replaces any overlay. */
|
|
1372
|
+
/** `pick-move` for the row now highlighted, or a plain overlay input. */
|
|
1373
|
+
private pickerMoved(picker: PickerState): AppInput {
|
|
1374
|
+
const item = picker.items[picker.index];
|
|
1375
|
+
return item
|
|
1376
|
+
? { type: "pick-move", picker: picker.id, value: item.value }
|
|
1377
|
+
: { type: "overlay" };
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1061
1380
|
openPicker(picker: Omit<PickerState, "index"> & { index?: number }): void {
|
|
1062
1381
|
const items = picker.items.slice(0, MAX_PICKER_ITEMS);
|
|
1063
1382
|
if (items.length === 0) return;
|
|
@@ -1069,11 +1388,57 @@ export class TuiApp {
|
|
|
1069
1388
|
this.overlay = "picker";
|
|
1070
1389
|
}
|
|
1071
1390
|
|
|
1391
|
+
/** Narrow a filterable picker to rows matching `query`. */
|
|
1392
|
+
private filterPicker(query: string): void {
|
|
1393
|
+
const picker = this.picker;
|
|
1394
|
+
if (!picker) return;
|
|
1395
|
+
const all = picker.all ?? picker.items;
|
|
1396
|
+
const needle = query.toLowerCase();
|
|
1397
|
+
const items = all.filter(
|
|
1398
|
+
(item) =>
|
|
1399
|
+
!needle ||
|
|
1400
|
+
item.label.toLowerCase().includes(needle) ||
|
|
1401
|
+
(item.detail ?? "").toLowerCase().includes(needle),
|
|
1402
|
+
);
|
|
1403
|
+
const keep = picker.items[picker.index]?.value;
|
|
1404
|
+
const index = Math.max(
|
|
1405
|
+
0,
|
|
1406
|
+
items.findIndex((item) => item.value === keep),
|
|
1407
|
+
);
|
|
1408
|
+
this.picker = { ...picker, all, query, items, index };
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1072
1411
|
closePicker(): void {
|
|
1073
1412
|
this.picker = undefined;
|
|
1074
1413
|
if (this.overlay === "picker") this.overlay = undefined;
|
|
1075
1414
|
}
|
|
1076
1415
|
|
|
1416
|
+
/** Show static lines over the highway (`/help`, lists); replaces any overlay. */
|
|
1417
|
+
openText(title: string, lines: readonly string[]): void {
|
|
1418
|
+
this.text = { title, lines: lines.slice(0, 512), scroll: 0 };
|
|
1419
|
+
this.overlay = "text";
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
/** The `?` panel over the current screen; any key closes it. */
|
|
1423
|
+
keys: TextView | undefined;
|
|
1424
|
+
showKeys(title: string, lines: readonly string[]): void {
|
|
1425
|
+
this.keys = { title, lines, scroll: 0 };
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1428
|
+
closeKeys(): void {
|
|
1429
|
+
this.keys = undefined;
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
/** True while a picker's `/` filter is taking typed text. */
|
|
1433
|
+
get pickerTyping(): boolean {
|
|
1434
|
+
return this.overlay === "picker" && this.picker?.filtering === true;
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1437
|
+
closeText(): void {
|
|
1438
|
+
this.text = undefined;
|
|
1439
|
+
if (this.overlay === "text") this.overlay = undefined;
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1077
1442
|
/**
|
|
1078
1443
|
* Handle TUI-local slash commands (`/log`, `/theme`, `/motion`).
|
|
1079
1444
|
* Returns a receipt, or undefined when the command is not a UI command.
|