@hraness/dawg 0.0.0-stage → 0.3.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 +327 -0
- package/LICENSE +21 -0
- package/README.md +213 -2
- package/core/diff.ts +249 -0
- package/core/drums.ts +102 -0
- package/core/key.ts +43 -0
- package/core/loop.ts +78 -0
- package/core/pitch.ts +60 -0
- package/core/score.ts +1388 -0
- package/core/sdk/eval-child.ts +113 -0
- package/core/sdk/eval.ts +257 -0
- package/core/sdk/print.ts +393 -0
- package/core/sdk/v1.ts +954 -0
- package/core/slug.ts +19 -0
- package/package.json +45 -4
- package/src/agent/agent.ts +853 -0
- package/src/agent/brief.ts +160 -0
- package/src/agent/gateway.ts +441 -0
- package/src/agent/models.ts +633 -0
- package/src/agent/ops.ts +157 -0
- package/src/agent/planner.ts +259 -0
- package/src/agent/provider.ts +454 -0
- package/src/agent/sse.ts +114 -0
- package/src/agent/tools.ts +1373 -0
- package/src/agent/usage.ts +296 -0
- package/src/agent/workspace.ts +683 -0
- package/src/agent/xcb-agent.ts +262 -0
- package/src/agent/xcb.ts +579 -0
- package/src/audio/click.ts +125 -0
- package/src/audio/clock.ts +68 -0
- package/src/audio/engine.ts +841 -0
- package/src/audio/live.ts +152 -0
- package/src/audio/lock.ts +57 -0
- package/src/audio/player.ts +134 -0
- package/src/audio/render-worker.ts +68 -0
- package/src/audio/renderer.ts +174 -0
- package/src/audio/sampler.ts +292 -0
- package/src/audio/samples.ts +683 -0
- package/src/audio/wav.ts +861 -0
- package/src/auth/cli.ts +231 -0
- package/src/auth/credentials.ts +411 -0
- package/src/auth/discover.ts +481 -0
- package/src/auth/login.ts +1191 -0
- package/src/auth/openrouter.ts +206 -0
- package/src/auth/picker.ts +282 -0
- package/src/auth/runner.ts +207 -0
- package/src/auth/tui.ts +107 -0
- package/src/commands/edit.ts +170 -0
- package/src/commands/help.ts +247 -0
- package/src/commands/history.ts +69 -0
- package/src/commands/music.ts +461 -0
- package/src/commands/sample.ts +302 -0
- package/src/daemon.ts +31 -0
- package/src/main.ts +2209 -0
- package/src/media/analyze.ts +364 -0
- package/src/media/backend.ts +253 -0
- package/src/media/cli.ts +173 -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 +292 -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/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 +121 -0
- package/src/session/attach.ts +181 -0
- package/src/session/client.ts +498 -0
- package/src/session/daemon.ts +740 -0
- package/src/session/delta.ts +249 -0
- package/src/session/list.ts +180 -0
- package/src/session/lock.ts +92 -0
- package/src/session/meta.ts +253 -0
- package/src/session/naming.ts +430 -0
- package/src/session/port.ts +481 -0
- package/src/session/presence.ts +159 -0
- package/src/session/protocol.ts +618 -0
- package/src/session/rebase.ts +168 -0
- package/src/session/store.ts +581 -0
- package/src/tui/menu.ts +1083 -0
- package/src/tui/play-mode.ts +442 -0
- package/src/tui/play-session.ts +636 -0
- package/src/web/fetch.ts +340 -0
- package/src/web/http.ts +137 -0
- package/src/web/search.ts +681 -0
- package/tui/activity.ts +364 -0
- package/tui/app.ts +1372 -0
- package/tui/drums.ts +65 -0
- package/tui/highway.ts +921 -0
- package/tui/input.ts +63 -0
- package/tui/keys.ts +102 -0
- package/tui/layers.ts +80 -0
- package/tui/play-strip.ts +143 -0
- package/tui/prompt.ts +609 -0
- package/tui/render.ts +124 -0
- package/tui/screen.ts +247 -0
- package/tui/text.ts +72 -0
- package/tui/theme.ts +451 -0
package/tui/input.ts
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Incremental decoder for the small set of terminal sequences dawg accepts.
|
|
3
|
+
* PTYs may split one CSI sequence across reads or combine several key events
|
|
4
|
+
* into one read, so the main loop must consume framed events rather than raw
|
|
5
|
+
* chunks.
|
|
6
|
+
*/
|
|
7
|
+
export type TerminalInputEvent =
|
|
8
|
+
string | Readonly<{ type: "paste"; text: string }>;
|
|
9
|
+
|
|
10
|
+
export class TerminalInputDecoder {
|
|
11
|
+
private buffer = "";
|
|
12
|
+
|
|
13
|
+
push(chunk: string): TerminalInputEvent[] {
|
|
14
|
+
this.buffer += chunk;
|
|
15
|
+
const events: TerminalInputEvent[] = [];
|
|
16
|
+
while (this.buffer.length > 0) {
|
|
17
|
+
if (this.buffer.startsWith("\u001b[200~")) {
|
|
18
|
+
const end = this.buffer.indexOf("\u001b[201~", 6);
|
|
19
|
+
if (end < 0) break;
|
|
20
|
+
events.push({ type: "paste", text: this.buffer.slice(6, end) });
|
|
21
|
+
this.buffer = this.buffer.slice(end + 6);
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (this.buffer[0] === "\u001b") {
|
|
26
|
+
const sequence = this.readEscapeSequence();
|
|
27
|
+
if (sequence === undefined) break;
|
|
28
|
+
events.push(sequence);
|
|
29
|
+
this.buffer = this.buffer.slice(sequence.length);
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const codePoint = Array.from(this.buffer)[0];
|
|
34
|
+
if (codePoint === undefined) break;
|
|
35
|
+
events.push(codePoint);
|
|
36
|
+
this.buffer = this.buffer.slice(codePoint.length);
|
|
37
|
+
}
|
|
38
|
+
return events;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
flush(): TerminalInputEvent[] {
|
|
42
|
+
if (this.buffer === "\u001b") {
|
|
43
|
+
this.buffer = "";
|
|
44
|
+
return ["\u001b"];
|
|
45
|
+
}
|
|
46
|
+
return [];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
private readEscapeSequence(): string | undefined {
|
|
50
|
+
if (this.buffer.length === 1) return undefined;
|
|
51
|
+
if (this.buffer.startsWith("\u001b\r")) return "\u001b\r";
|
|
52
|
+
// ESC followed by another control key (or ESC) is a lone Esc press that
|
|
53
|
+
// shared a read with the next key, e.g. Esc then Ctrl+Z.
|
|
54
|
+
const next = this.buffer.charCodeAt(1);
|
|
55
|
+
if (next < 0x20 || next === 0x7f) return "\u001b";
|
|
56
|
+
const final = this.buffer.match(/^\u001b\[[\x20-?]*[\x40-~]/)?.[0];
|
|
57
|
+
if (final !== undefined) return final;
|
|
58
|
+
// An unrecognised Alt-prefixed character is still one complete event.
|
|
59
|
+
if (this.buffer.length >= 2 && this.buffer[1] !== "[")
|
|
60
|
+
return this.buffer.slice(0, 2);
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
}
|
package/tui/keys.ts
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Map decoded terminal sequences to prompt keys and TUI commands.
|
|
3
|
+
*
|
|
4
|
+
* The prompt is always focused, so printable keys always type. Control
|
|
5
|
+
* chords carry the global actions:
|
|
6
|
+
* Ctrl+C quit · Ctrl+Q toggle STEER/QUEUE · Ctrl+Z undo · Ctrl+Y redo
|
|
7
|
+
* Ctrl+O transcript overlay · Esc closes the overlay
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { PromptKey } from "./prompt.ts";
|
|
11
|
+
|
|
12
|
+
export type UiCommand =
|
|
13
|
+
"quit" | "undo" | "redo" | "toggle-log" | "close-overlay" | "redraw";
|
|
14
|
+
|
|
15
|
+
export type KeyResult =
|
|
16
|
+
| { type: "prompt"; key: PromptKey }
|
|
17
|
+
| { type: "text"; text: string }
|
|
18
|
+
| { type: "ui"; command: UiCommand }
|
|
19
|
+
| { type: "ignore" };
|
|
20
|
+
|
|
21
|
+
const PROMPT_SEQUENCES: Record<string, string> = {
|
|
22
|
+
"\u001b[13;2u": "SHIFT+ENTER",
|
|
23
|
+
"\u001b[27;2;13~": "SHIFT+ENTER",
|
|
24
|
+
// Many terminals send ESC CR (Alt+Enter) for Shift+Enter when configured;
|
|
25
|
+
// dawg treats Alt+Enter as "queue this prompt".
|
|
26
|
+
"\u001b\r": "ALT+ENTER",
|
|
27
|
+
"\u0011": "CTRL+Q",
|
|
28
|
+
"\u007f": "BACKSPACE",
|
|
29
|
+
"\b": "BACKSPACE",
|
|
30
|
+
"\r": "ENTER",
|
|
31
|
+
"\n": "SHIFT+ENTER",
|
|
32
|
+
"\u001b[A": "UP",
|
|
33
|
+
"\u001bOA": "UP",
|
|
34
|
+
"\u001b[B": "DOWN",
|
|
35
|
+
"\u001bOB": "DOWN",
|
|
36
|
+
"\u001b[C": "RIGHT",
|
|
37
|
+
"\u001bOC": "RIGHT",
|
|
38
|
+
"\u001b[D": "LEFT",
|
|
39
|
+
"\u001bOD": "LEFT",
|
|
40
|
+
"\u001b[H": "HOME",
|
|
41
|
+
"\u001b[1~": "HOME",
|
|
42
|
+
"\u0001": "HOME",
|
|
43
|
+
"\u001b[F": "END",
|
|
44
|
+
"\u001b[4~": "END",
|
|
45
|
+
"\u0005": "END",
|
|
46
|
+
"\u001b[3~": "DELETE",
|
|
47
|
+
"\u001b[1;5D": "CTRL+LEFT",
|
|
48
|
+
"\u001bb": "CTRL+LEFT",
|
|
49
|
+
"\u001b[1;5C": "CTRL+RIGHT",
|
|
50
|
+
"\u001bf": "CTRL+RIGHT",
|
|
51
|
+
"\u001b[1;5H": "CTRL+HOME",
|
|
52
|
+
"\u001b[1;5F": "CTRL+END",
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const UI_SEQUENCES: Record<string, UiCommand> = {
|
|
56
|
+
"\u0003": "quit",
|
|
57
|
+
"\u001a": "undo",
|
|
58
|
+
"\u0019": "redo",
|
|
59
|
+
"\u000f": "toggle-log",
|
|
60
|
+
"\u000c": "redraw",
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export function classifyKey(value: string): KeyResult {
|
|
64
|
+
const ui = UI_SEQUENCES[value];
|
|
65
|
+
if (ui) return { type: "ui", command: ui };
|
|
66
|
+
if (value.startsWith("\u001b[200~") && value.endsWith("\u001b[201~"))
|
|
67
|
+
return { type: "prompt", key: { type: "paste", text: value.slice(6, -6) } };
|
|
68
|
+
const key = PROMPT_SEQUENCES[value];
|
|
69
|
+
if (key) return { type: "prompt", key };
|
|
70
|
+
if (value === "\u001b") return { type: "ui", command: "close-overlay" };
|
|
71
|
+
if (value.startsWith("\u001b")) return { type: "ignore" };
|
|
72
|
+
// Strip other C0 controls so stray chords never insert garbage.
|
|
73
|
+
const text = Array.from(value)
|
|
74
|
+
.filter((character) => {
|
|
75
|
+
const code = character.codePointAt(0) ?? 0;
|
|
76
|
+
return code >= 0x20 && code !== 0x7f;
|
|
77
|
+
})
|
|
78
|
+
.join("");
|
|
79
|
+
return text ? { type: "text", text } : { type: "ignore" };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export type OverlayKey =
|
|
83
|
+
"up" | "down" | "pgup" | "pgdn" | "home" | "end" | "enter";
|
|
84
|
+
|
|
85
|
+
const OVERLAY_SEQUENCES: Record<string, OverlayKey> = {
|
|
86
|
+
"\u001b[A": "up",
|
|
87
|
+
"\u001bOA": "up",
|
|
88
|
+
"\u001b[B": "down",
|
|
89
|
+
"\u001bOB": "down",
|
|
90
|
+
"\u001b[5~": "pgup",
|
|
91
|
+
"\u001b[6~": "pgdn",
|
|
92
|
+
"\u001b[H": "home",
|
|
93
|
+
"\u001b[1~": "home",
|
|
94
|
+
"\u001b[F": "end",
|
|
95
|
+
"\u001b[4~": "end",
|
|
96
|
+
"\r": "enter",
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
/** Navigation keys an open overlay (transcript, picker) consumes. */
|
|
100
|
+
export function overlayKey(value: string): OverlayKey | undefined {
|
|
101
|
+
return OVERLAY_SEQUENCES[value];
|
|
102
|
+
}
|
package/tui/layers.ts
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Multi-track highway: every other audible track as a dimmed overlay layer.
|
|
3
|
+
* Solo rules match the mixer: when any track is soloed, only soloed unmuted
|
|
4
|
+
* tracks are drawn.
|
|
5
|
+
*/
|
|
6
|
+
import { DRUM_VOICES, drumLane, isDrumInstrument } from "../core/drums.ts";
|
|
7
|
+
import {
|
|
8
|
+
drumLaneProjection,
|
|
9
|
+
MAX_LAYER_NOTES,
|
|
10
|
+
type HighwayLayer,
|
|
11
|
+
} from "./highway.ts";
|
|
12
|
+
import { samplerLanes } from "./drums.ts";
|
|
13
|
+
import type { Sampler } from "../core/score.ts";
|
|
14
|
+
|
|
15
|
+
export interface LayerTrack {
|
|
16
|
+
id: string;
|
|
17
|
+
name: string;
|
|
18
|
+
instrument: string;
|
|
19
|
+
muted: boolean;
|
|
20
|
+
solo?: boolean | undefined;
|
|
21
|
+
sampler?: Sampler | undefined;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface LayerNote {
|
|
25
|
+
trackId: string;
|
|
26
|
+
startTick: number;
|
|
27
|
+
durationTicks: number;
|
|
28
|
+
pitch: number;
|
|
29
|
+
velocity: number;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function highwayLayers(
|
|
33
|
+
tracks: readonly LayerTrack[],
|
|
34
|
+
notes: readonly LayerNote[],
|
|
35
|
+
ticksPerBeat: number,
|
|
36
|
+
focusedTrackId: string,
|
|
37
|
+
): HighwayLayer[] {
|
|
38
|
+
const soloing = tracks.some((track) => track.solo && !track.muted);
|
|
39
|
+
const audible = tracks.filter(
|
|
40
|
+
(track) =>
|
|
41
|
+
track.id !== focusedTrackId &&
|
|
42
|
+
!track.muted &&
|
|
43
|
+
(!soloing || track.solo === true),
|
|
44
|
+
);
|
|
45
|
+
const tpb = Math.max(1, ticksPerBeat);
|
|
46
|
+
let budget = MAX_LAYER_NOTES;
|
|
47
|
+
return audible.map((track) => {
|
|
48
|
+
const drum = isDrumInstrument(track.instrument);
|
|
49
|
+
const sampled = drum ? undefined : samplerLanes(track);
|
|
50
|
+
const own = [];
|
|
51
|
+
for (const note of notes) {
|
|
52
|
+
if (note.trackId !== track.id) continue;
|
|
53
|
+
if (budget <= 0) break;
|
|
54
|
+
const lane = drum
|
|
55
|
+
? drumLane(note.pitch)
|
|
56
|
+
: sampled
|
|
57
|
+
? sampled.laneOf(note.pitch)
|
|
58
|
+
: undefined;
|
|
59
|
+
if (sampled && lane === undefined) continue;
|
|
60
|
+
budget -= 1;
|
|
61
|
+
own.push({
|
|
62
|
+
startBeat: note.startTick / tpb,
|
|
63
|
+
durationBeats: note.durationTicks / tpb,
|
|
64
|
+
pitch: note.pitch,
|
|
65
|
+
velocity: note.velocity,
|
|
66
|
+
...(lane !== undefined ? { lane } : {}),
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
return {
|
|
70
|
+
trackId: track.id,
|
|
71
|
+
trackName: track.name,
|
|
72
|
+
notes: own,
|
|
73
|
+
projection: drum
|
|
74
|
+
? drumLaneProjection(DRUM_VOICES.map((info) => info.label))
|
|
75
|
+
: sampled
|
|
76
|
+
? drumLaneProjection(sampled.labels)
|
|
77
|
+
: undefined,
|
|
78
|
+
};
|
|
79
|
+
});
|
|
80
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Play mode's two fixed rows: the header (`PLAY C3–F4 vel 100 ● REC
|
|
3
|
+
* click ✓` plus a beat flash) and a one-line keyboard strip with the keys
|
|
4
|
+
* that are sounding lit. Both repaint in place every frame, so playing never
|
|
5
|
+
* scrolls the transcript.
|
|
6
|
+
*/
|
|
7
|
+
import type { CellBuffer } from "./screen.ts";
|
|
8
|
+
import { displayWidth, truncate } from "./text.ts";
|
|
9
|
+
import { onBackground, type Theme } from "./theme.ts";
|
|
10
|
+
|
|
11
|
+
export type PlayStripKey = Readonly<{
|
|
12
|
+
key: string;
|
|
13
|
+
label: string;
|
|
14
|
+
black: boolean;
|
|
15
|
+
lit: boolean;
|
|
16
|
+
}>;
|
|
17
|
+
|
|
18
|
+
export type PlayHeaderView = Readonly<{
|
|
19
|
+
/** `C3–F4`. */
|
|
20
|
+
range: string;
|
|
21
|
+
velocity: number;
|
|
22
|
+
/** Record armed. */
|
|
23
|
+
armed: boolean;
|
|
24
|
+
/** Recording right now (armed and the transport is running). */
|
|
25
|
+
recording: boolean;
|
|
26
|
+
replace: boolean;
|
|
27
|
+
click: boolean;
|
|
28
|
+
sustain: boolean;
|
|
29
|
+
/** `count-in 3` while counting in. */
|
|
30
|
+
countIn?: string | undefined;
|
|
31
|
+
/** Beat within the bar (1-based) and whether it is the flash window. */
|
|
32
|
+
beat?: Readonly<{ index: number; of: number; flash: boolean }> | undefined;
|
|
33
|
+
/** `grid 1/16`. */
|
|
34
|
+
grid: string;
|
|
35
|
+
/** Short status (`octave C2`, `no audio`). */
|
|
36
|
+
status?: string | undefined;
|
|
37
|
+
keys: readonly PlayStripKey[];
|
|
38
|
+
}>;
|
|
39
|
+
|
|
40
|
+
/** Text of the header row, for tests and narrow terminals. */
|
|
41
|
+
export function playHeaderText(view: PlayHeaderView, unicode = true): string {
|
|
42
|
+
const parts = [
|
|
43
|
+
"PLAY",
|
|
44
|
+
view.range,
|
|
45
|
+
`vel ${view.velocity}`,
|
|
46
|
+
view.armed
|
|
47
|
+
? `${unicode ? "●" : "*"} ${view.recording ? "REC" : "rec armed"}${view.replace ? " replace" : ""}`
|
|
48
|
+
: "",
|
|
49
|
+
`click ${view.click ? (unicode ? "✓" : "on") : "off"}`,
|
|
50
|
+
view.sustain ? "SUSTAIN" : "",
|
|
51
|
+
view.grid,
|
|
52
|
+
view.countIn ?? "",
|
|
53
|
+
].filter(Boolean);
|
|
54
|
+
return parts.join(" ");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function paintPlayHeader(
|
|
58
|
+
buffer: CellBuffer,
|
|
59
|
+
y: number,
|
|
60
|
+
width: number,
|
|
61
|
+
view: PlayHeaderView,
|
|
62
|
+
theme: Theme,
|
|
63
|
+
unicode: boolean,
|
|
64
|
+
): void {
|
|
65
|
+
const roles = theme.roles;
|
|
66
|
+
const panel = roles.panel;
|
|
67
|
+
buffer.fill(0, y, width, 1, panel);
|
|
68
|
+
let x = 1;
|
|
69
|
+
const put = (text: string, style = roles.text) => {
|
|
70
|
+
if (x >= width - 1) return;
|
|
71
|
+
x += buffer.text(
|
|
72
|
+
x,
|
|
73
|
+
y,
|
|
74
|
+
truncate(text, Math.max(1, width - x - 1)),
|
|
75
|
+
onBackground(style, panel),
|
|
76
|
+
);
|
|
77
|
+
x += buffer.text(x, y, " ", panel);
|
|
78
|
+
};
|
|
79
|
+
put(" PLAY ", roles.pillSteer);
|
|
80
|
+
put(view.range, { ...roles.text, bold: true });
|
|
81
|
+
put(`vel ${view.velocity}`);
|
|
82
|
+
if (view.armed)
|
|
83
|
+
put(
|
|
84
|
+
`${unicode ? "●" : "*"} ${view.recording ? "REC" : "rec armed"}${view.replace ? " replace" : ""}`,
|
|
85
|
+
view.recording ? roles.error : roles.warning,
|
|
86
|
+
);
|
|
87
|
+
put(
|
|
88
|
+
`click ${view.click ? (unicode ? "✓" : "on") : "off"}`,
|
|
89
|
+
view.click ? roles.success : roles.muted,
|
|
90
|
+
);
|
|
91
|
+
if (view.sustain) put("SUSTAIN", roles.pillQueue);
|
|
92
|
+
if (view.countIn) put(view.countIn, roles.warning);
|
|
93
|
+
if (view.beat) {
|
|
94
|
+
const cells = Array.from({ length: view.beat.of }, (_, index) =>
|
|
95
|
+
index + 1 === view.beat!.index
|
|
96
|
+
? unicode
|
|
97
|
+
? "●"
|
|
98
|
+
: "o"
|
|
99
|
+
: unicode
|
|
100
|
+
? "·"
|
|
101
|
+
: ".",
|
|
102
|
+
).join("");
|
|
103
|
+
put(
|
|
104
|
+
cells,
|
|
105
|
+
view.beat.flash
|
|
106
|
+
? view.beat.index === 1
|
|
107
|
+
? roles.error
|
|
108
|
+
: roles.hit
|
|
109
|
+
: roles.muted,
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
put(view.grid, roles.muted);
|
|
113
|
+
if (view.status) put(view.status, roles.muted);
|
|
114
|
+
// Mode keys at the right edge while there is room.
|
|
115
|
+
const hint = "z/x oct · c/v vel · r rec · m click · esc exit";
|
|
116
|
+
const room = width - 1 - displayWidth(hint);
|
|
117
|
+
if (room > x) buffer.text(room, y, hint, onBackground(roles.faint, panel));
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** `a C3 │ w C#3 …`: each key with its note; lit keys reversed. */
|
|
121
|
+
export function paintPlayStrip(
|
|
122
|
+
buffer: CellBuffer,
|
|
123
|
+
y: number,
|
|
124
|
+
width: number,
|
|
125
|
+
keys: readonly PlayStripKey[],
|
|
126
|
+
theme: Theme,
|
|
127
|
+
): void {
|
|
128
|
+
const roles = theme.roles;
|
|
129
|
+
buffer.fill(0, y, width, 1, roles.canvas);
|
|
130
|
+
let x = 1;
|
|
131
|
+
for (const key of keys) {
|
|
132
|
+
const cell = `${key.key.toUpperCase()} ${key.label}`;
|
|
133
|
+
const cellWidth = displayWidth(cell) + 1;
|
|
134
|
+
if (x + cellWidth > width - 1) break;
|
|
135
|
+
const style = key.lit
|
|
136
|
+
? { ...(key.black ? roles.selected : roles.hit), reverse: true }
|
|
137
|
+
: key.black
|
|
138
|
+
? roles.muted
|
|
139
|
+
: roles.text;
|
|
140
|
+
buffer.text(x, y, cell, style);
|
|
141
|
+
x += cellWidth;
|
|
142
|
+
}
|
|
143
|
+
}
|