@hraness/dawg 0.0.0-stage → 0.2.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 +47 -0
- package/DAWG.md +142 -0
- package/LICENSE +21 -0
- package/README.md +164 -2
- package/core/drums.ts +102 -0
- package/core/loop.ts +78 -0
- package/core/score.ts +1065 -0
- package/package.json +42 -4
- package/src/agent/agent.ts +587 -0
- package/src/agent/brief.ts +140 -0
- package/src/agent/gateway.ts +267 -0
- package/src/agent/ops.ts +172 -0
- package/src/agent/planner.ts +221 -0
- package/src/agent/provider.ts +326 -0
- package/src/agent/sse.ts +90 -0
- package/src/agent/tools.ts +976 -0
- package/src/agent/xcb-agent.ts +247 -0
- package/src/agent/xcb.ts +367 -0
- package/src/audio/clock.ts +68 -0
- package/src/audio/engine.ts +467 -0
- package/src/audio/lock.ts +57 -0
- package/src/audio/player.ts +119 -0
- package/src/audio/wav.ts +647 -0
- package/src/auth/cli.ts +112 -0
- package/src/auth/credentials.ts +285 -0
- package/src/auth/login.ts +434 -0
- package/src/auth/runner.ts +184 -0
- package/src/auth/tui.ts +90 -0
- package/src/commands/history.ts +56 -0
- package/src/commands/music.ts +444 -0
- package/src/daemon.ts +31 -0
- package/src/main.ts +1332 -0
- package/src/render.ts +99 -0
- package/src/session/attach.ts +182 -0
- package/src/session/client.ts +498 -0
- package/src/session/daemon.ts +723 -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 +464 -0
- package/src/session/port.ts +461 -0
- package/src/session/presence.ts +159 -0
- package/src/session/protocol.ts +618 -0
- package/src/session/rebase.ts +138 -0
- package/src/session/store.ts +486 -0
- package/tui/activity.ts +325 -0
- package/tui/app.ts +1129 -0
- package/tui/drums.ts +21 -0
- package/tui/highway.ts +905 -0
- package/tui/input.ts +63 -0
- package/tui/keys.ts +102 -0
- package/tui/layers.ts +68 -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,68 @@
|
|
|
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
|
+
|
|
13
|
+
export interface LayerTrack {
|
|
14
|
+
id: string;
|
|
15
|
+
name: string;
|
|
16
|
+
instrument: string;
|
|
17
|
+
muted: boolean;
|
|
18
|
+
solo?: boolean | undefined;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface LayerNote {
|
|
22
|
+
trackId: string;
|
|
23
|
+
startTick: number;
|
|
24
|
+
durationTicks: number;
|
|
25
|
+
pitch: number;
|
|
26
|
+
velocity: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function highwayLayers(
|
|
30
|
+
tracks: readonly LayerTrack[],
|
|
31
|
+
notes: readonly LayerNote[],
|
|
32
|
+
ticksPerBeat: number,
|
|
33
|
+
focusedTrackId: string,
|
|
34
|
+
): HighwayLayer[] {
|
|
35
|
+
const soloing = tracks.some((track) => track.solo && !track.muted);
|
|
36
|
+
const audible = tracks.filter(
|
|
37
|
+
(track) =>
|
|
38
|
+
track.id !== focusedTrackId &&
|
|
39
|
+
!track.muted &&
|
|
40
|
+
(!soloing || track.solo === true),
|
|
41
|
+
);
|
|
42
|
+
const tpb = Math.max(1, ticksPerBeat);
|
|
43
|
+
let budget = MAX_LAYER_NOTES;
|
|
44
|
+
return audible.map((track) => {
|
|
45
|
+
const drum = isDrumInstrument(track.instrument);
|
|
46
|
+
const own = [];
|
|
47
|
+
for (const note of notes) {
|
|
48
|
+
if (note.trackId !== track.id) continue;
|
|
49
|
+
if (budget <= 0) break;
|
|
50
|
+
budget -= 1;
|
|
51
|
+
own.push({
|
|
52
|
+
startBeat: note.startTick / tpb,
|
|
53
|
+
durationBeats: note.durationTicks / tpb,
|
|
54
|
+
pitch: note.pitch,
|
|
55
|
+
velocity: note.velocity,
|
|
56
|
+
...(drum ? { lane: drumLane(note.pitch) } : {}),
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
return {
|
|
60
|
+
trackId: track.id,
|
|
61
|
+
trackName: track.name,
|
|
62
|
+
notes: own,
|
|
63
|
+
projection: drum
|
|
64
|
+
? drumLaneProjection(DRUM_VOICES.map((info) => info.label))
|
|
65
|
+
: undefined,
|
|
66
|
+
};
|
|
67
|
+
});
|
|
68
|
+
}
|