tuner-cli 2026.4.1
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/LICENSE +21 -0
- package/README.md +101 -0
- package/dist/ansi.js +92 -0
- package/dist/ansi.js.map +1 -0
- package/dist/cli-lists.js +31 -0
- package/dist/cli-lists.js.map +1 -0
- package/dist/decibri-audio.js +78 -0
- package/dist/decibri-audio.js.map +1 -0
- package/dist/display/styles.js +41 -0
- package/dist/display/styles.js.map +1 -0
- package/dist/format-line.js +115 -0
- package/dist/format-line.js.map +1 -0
- package/dist/index.js +53 -0
- package/dist/index.js.map +1 -0
- package/dist/input/keys.js +88 -0
- package/dist/input/keys.js.map +1 -0
- package/dist/menus/select-list.js +91 -0
- package/dist/menus/select-list.js.map +1 -0
- package/dist/parse-args.js +172 -0
- package/dist/parse-args.js.map +1 -0
- package/dist/parsed-cli.js +35 -0
- package/dist/parsed-cli.js.map +1 -0
- package/dist/run-tuner-session.js +457 -0
- package/dist/run-tuner-session.js.map +1 -0
- package/dist/verbose-startup.js +56 -0
- package/dist/verbose-startup.js.map +1 -0
- package/package.json +64 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Tuner contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# tuner-cli
|
|
2
|
+
|
|
3
|
+
Chromatic **instrument tuner** in the terminal: live pitch from your microphone, needle-style cents display, and optional string hints. Built with **[tuner-core](https://www.npmjs.com/package/tuner-core)** (detectors + session) and **[decibri](https://www.npmjs.com/package/decibri)** (PortAudio capture).
|
|
4
|
+
|
|
5
|
+

|
|
6
|
+
|
|
7
|
+
## Requirements
|
|
8
|
+
|
|
9
|
+
- **Node.js 20+**
|
|
10
|
+
- A **microphone** and an OS where **decibri** can open an input (prebuilt binaries when available; otherwise a local build toolchain may be needed for that package)
|
|
11
|
+
|
|
12
|
+
## Install
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
npm install -g tuner-cli
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Run **`tuner`** on your `PATH`. To try without a global install:
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npx tuner-cli
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
## Usage
|
|
26
|
+
|
|
27
|
+
Start the tuner with defaults (guitar, host default input, 48 kHz):
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
tuner
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Discover inputs and built-in instruments:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
tuner --list-devices
|
|
37
|
+
tuner --list-instruments
|
|
38
|
+
tuner --list-tunings guitar
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Point at a specific input and instrument:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
tuner --device 1 --instrument bass --tuning bass-standard
|
|
45
|
+
tuner --rate 44100 --detector pyin --verbose
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Use `-h` / `--help` for full CLI text and more examples.
|
|
49
|
+
|
|
50
|
+
## Options
|
|
51
|
+
|
|
52
|
+
| Flag | Description |
|
|
53
|
+
|------|-------------|
|
|
54
|
+
| `-h`, `--help` | Help |
|
|
55
|
+
| `-v`, `--verbose` | Print resolved config on **stderr** before audio starts |
|
|
56
|
+
| `--list-devices` | List audio inputs |
|
|
57
|
+
| `--list-instruments` | List instrument ids |
|
|
58
|
+
| `--list-tunings <id>` | List tunings for an instrument |
|
|
59
|
+
| `--device …` | Input device: numeric index or substring of the device name |
|
|
60
|
+
| `--rate <hz>` | Sample rate (default `48000`) |
|
|
61
|
+
| `--instrument <id>` | Instrument (default `guitar`) |
|
|
62
|
+
| `--tuning <id>` | Tuning (default: first for that instrument) |
|
|
63
|
+
| `--detector <kind>` | `yin`, `pyin`, `mpm`, `autocorrelation` (default `yin`) |
|
|
64
|
+
| `--cents-threshold <n>` | In-tune window for the string hint (default `5`) |
|
|
65
|
+
| `--style <name>` | `standard`, `colors`, `ansi` |
|
|
66
|
+
| `--color <mode>` | `auto`, `always`, `never` (honours `NO_COLOR` / `FORCE_COLOR` when `auto`) |
|
|
67
|
+
|
|
68
|
+
## Interactive terminal
|
|
69
|
+
|
|
70
|
+
If stdin and stdout are a TTY, you can change settings without restarting the whole program:
|
|
71
|
+
|
|
72
|
+
| Key | Action |
|
|
73
|
+
|-----|--------|
|
|
74
|
+
| **i** | Instrument |
|
|
75
|
+
| **t** | Tuning |
|
|
76
|
+
| **s** | Display style |
|
|
77
|
+
| **a** | Advanced (detector, rate, device, cents, color, style) |
|
|
78
|
+
| **q**, **Esc**, **←** | Quit (when no menu is open; in lists, **Esc** / **←** goes back) |
|
|
79
|
+
| **Ctrl+C** | Quit |
|
|
80
|
+
|
|
81
|
+
In lists: **↑** / **↓** (or **j** / **k**) move; **Enter** or **→** (or **l**) select; **Esc** or **←** (or **h**) cancel. Changing detector, rate, or device in **Advanced** restarts the audio stream.
|
|
82
|
+
|
|
83
|
+
## Developing
|
|
84
|
+
|
|
85
|
+
This package lives in the **[Tuner](https://github.com/mducharme/Tuner)** monorepo.
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
git clone https://github.com/mducharme/Tuner.git
|
|
89
|
+
cd Tuner
|
|
90
|
+
pnpm install
|
|
91
|
+
pnpm dev:cli # run from source via tsx
|
|
92
|
+
pnpm --filter tuner-cli build
|
|
93
|
+
pnpm --filter tuner-cli test
|
|
94
|
+
pnpm --filter tuner-cli run package:assert
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Releases and versioning are handled in the monorepo (see the root **README** and [CHANGELOG](../../CHANGELOG.md)).
|
|
98
|
+
|
|
99
|
+
## License
|
|
100
|
+
|
|
101
|
+
MIT — see [LICENSE](./LICENSE).
|
package/dist/ansi.js
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/** Minimal ANSI SGR helpers; gated by {@link resolveColorMode}. */
|
|
2
|
+
export function resolveColorMode(choice, isTTY, env) {
|
|
3
|
+
if (choice === 'never')
|
|
4
|
+
return false;
|
|
5
|
+
if (choice === 'always')
|
|
6
|
+
return true;
|
|
7
|
+
if (env.NO_COLOR !== undefined && env.NO_COLOR !== '')
|
|
8
|
+
return false;
|
|
9
|
+
if (env.FORCE_COLOR !== undefined && env.FORCE_COLOR !== '0')
|
|
10
|
+
return true;
|
|
11
|
+
return isTTY;
|
|
12
|
+
}
|
|
13
|
+
const SGR = {
|
|
14
|
+
reset: '\x1b[0m',
|
|
15
|
+
bold: '\x1b[1m',
|
|
16
|
+
dim: '\x1b[2m',
|
|
17
|
+
green: '\x1b[32m',
|
|
18
|
+
yellow: '\x1b[33m',
|
|
19
|
+
red: '\x1b[31m',
|
|
20
|
+
cyan: '\x1b[36m',
|
|
21
|
+
magenta: '\x1b[35m',
|
|
22
|
+
white: '\x1b[37m',
|
|
23
|
+
};
|
|
24
|
+
/** Match ANSI SGR sequences (CSI … m). Built without `\x1b` in the pattern for tooling rules. */
|
|
25
|
+
const ANSI_SGR_PATTERN = new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, 'g');
|
|
26
|
+
export function stripAnsi(s) {
|
|
27
|
+
return s.replace(ANSI_SGR_PATTERN, '');
|
|
28
|
+
}
|
|
29
|
+
/** Chromatic cents: green near 0, yellow mid, red far. */
|
|
30
|
+
export function styleCents(color, cents) {
|
|
31
|
+
if (!color)
|
|
32
|
+
return { open: '', close: '' };
|
|
33
|
+
const a = Math.abs(cents);
|
|
34
|
+
if (a <= 5)
|
|
35
|
+
return { open: SGR.green, close: SGR.reset };
|
|
36
|
+
if (a <= 20)
|
|
37
|
+
return { open: SGR.yellow, close: SGR.reset };
|
|
38
|
+
return { open: SGR.red, close: SGR.reset };
|
|
39
|
+
}
|
|
40
|
+
export function styleNote(color) {
|
|
41
|
+
if (!color)
|
|
42
|
+
return { open: '', close: '' };
|
|
43
|
+
return { open: SGR.bold, close: SGR.reset };
|
|
44
|
+
}
|
|
45
|
+
export function styleDim(color) {
|
|
46
|
+
if (!color)
|
|
47
|
+
return { open: '', close: '' };
|
|
48
|
+
return { open: SGR.dim, close: SGR.reset };
|
|
49
|
+
}
|
|
50
|
+
export function styleInTune(color) {
|
|
51
|
+
if (!color)
|
|
52
|
+
return { open: '', close: '' };
|
|
53
|
+
return { open: SGR.green, close: SGR.reset };
|
|
54
|
+
}
|
|
55
|
+
/** Marker on gauge: green when string is in tune; else cents-based. */
|
|
56
|
+
export function styleGaugeMarker(color, cents, stringInTune) {
|
|
57
|
+
if (!color)
|
|
58
|
+
return { open: '', close: '' };
|
|
59
|
+
if (stringInTune)
|
|
60
|
+
return { open: SGR.green, close: SGR.reset };
|
|
61
|
+
return styleCents(color, cents);
|
|
62
|
+
}
|
|
63
|
+
/** One inactive track cell: slight flat / sharp tint by side of center. */
|
|
64
|
+
export function formatTrackCell(color, idx, centerIdx) {
|
|
65
|
+
if (!color)
|
|
66
|
+
return '·';
|
|
67
|
+
if (idx < centerIdx) {
|
|
68
|
+
return `${SGR.dim}${SGR.cyan}·${SGR.reset}`;
|
|
69
|
+
}
|
|
70
|
+
if (idx > centerIdx) {
|
|
71
|
+
return `${SGR.dim}${SGR.magenta}·${SGR.reset}`;
|
|
72
|
+
}
|
|
73
|
+
return `${SGR.dim}${SGR.white}·${SGR.reset}`;
|
|
74
|
+
}
|
|
75
|
+
export function formatMarkerCell(color, open, close) {
|
|
76
|
+
if (!color)
|
|
77
|
+
return '█';
|
|
78
|
+
return `${open}${SGR.bold}█${close}`;
|
|
79
|
+
}
|
|
80
|
+
/** Tinted track cell using shade blocks (fancy bar). */
|
|
81
|
+
export function formatTrackShade(color, idx, centerIdx, ch) {
|
|
82
|
+
if (!color)
|
|
83
|
+
return ch;
|
|
84
|
+
if (idx < centerIdx) {
|
|
85
|
+
return `${SGR.dim}${SGR.cyan}${ch}${SGR.reset}`;
|
|
86
|
+
}
|
|
87
|
+
if (idx > centerIdx) {
|
|
88
|
+
return `${SGR.dim}${SGR.magenta}${ch}${SGR.reset}`;
|
|
89
|
+
}
|
|
90
|
+
return `${SGR.dim}${SGR.white}${ch}${SGR.reset}`;
|
|
91
|
+
}
|
|
92
|
+
//# sourceMappingURL=ansi.js.map
|
package/dist/ansi.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ansi.js","sourceRoot":"","sources":["../src/ansi.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import Decibri from 'decibri';
|
|
2
|
+
import { INSTRUMENTS, findInstrument } from 'tuner-core';
|
|
3
|
+
export function listAudioDevices() {
|
|
4
|
+
const devices = Decibri.devices();
|
|
5
|
+
if (devices.length === 0) {
|
|
6
|
+
console.log('No input devices found.');
|
|
7
|
+
return;
|
|
8
|
+
}
|
|
9
|
+
for (const d of devices) {
|
|
10
|
+
const def = d.isDefault ? ' (default)' : '';
|
|
11
|
+
console.log(` [${d.index}] ${d.name}${def} ${d.defaultSampleRate} Hz`);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
export function listInstruments() {
|
|
15
|
+
for (const inst of INSTRUMENTS) {
|
|
16
|
+
console.log(` ${inst.id} ${inst.name}`);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
/** @returns false if instrument id is unknown */
|
|
20
|
+
export function listTunings(instrumentId) {
|
|
21
|
+
const inst = findInstrument(instrumentId);
|
|
22
|
+
if (!inst) {
|
|
23
|
+
console.error(`Unknown instrument: ${instrumentId}`);
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
for (const t of inst.tunings) {
|
|
27
|
+
console.log(` ${t.id} ${t.name}`);
|
|
28
|
+
}
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
//# sourceMappingURL=cli-lists.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli-lists.js","sourceRoot":"","sources":["../src/cli-lists.ts"],"names":[],"mappings":"AAAA,OAAO,OAAO,MAAM,SAAS,CAAA;AAC7B,OAAO,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,YAAY,CAAA;AAExD,MAAM,UAAU,gBAAgB;IAC9B,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,EAAE,CAAA;IACjC,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACzB,OAAO,CAAC,GAAG,CAAC,yBAAyB,CAAC,CAAA;QACtC,OAAM;IACR,CAAC;IACD,KAAK,MAAM,CAAC,IAAI,OAAO,EAAE,CAAC;QACxB,MAAM,GAAG,GAAG,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,EAAE,CAAA;QAC3C,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,KAAK,KAAK,CAAC,CAAC,IAAI,GAAG,GAAG,KAAK,CAAC,CAAC,iBAAiB,KAAK,CAAC,CAAA;IAC1E,CAAC;AACH,CAAC;AAED,MAAM,UAAU,eAAe;IAC7B,KAAK,MAAM,IAAI,IAAI,WAAW,EAAE,CAAC;QAC/B,OAAO,CAAC,GAAG,CAAC,KAAK,IAAI,CAAC,EAAE,KAAK,IAAI,CAAC,IAAI,EAAE,CAAC,CAAA;IAC3C,CAAC;AACH,CAAC;AAED,iDAAiD;AACjD,MAAM,UAAU,WAAW,CAAC,YAAoB;IAC9C,MAAM,IAAI,GAAG,cAAc,CAAC,YAAY,CAAC,CAAA;IACzC,IAAI,CAAC,IAAI,EAAE,CAAC;QACV,OAAO,CAAC,KAAK,CAAC,uBAAuB,YAAY,EAAE,CAAC,CAAA;QACpD,OAAO,KAAK,CAAA;IACd,CAAC;IACD,KAAK,MAAM,CAAC,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;QAC7B,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC,IAAI,EAAE,CAAC,CAAA;IACrC,CAAC;IACD,OAAO,IAAI,CAAA;AACb,CAAC"}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import Decibri from 'decibri';
|
|
2
|
+
/**
|
|
3
|
+
* Bridges decibri mic capture to {@link AudioProvider}: fixed-size float frames
|
|
4
|
+
* for {@link TunerSession}.
|
|
5
|
+
*/
|
|
6
|
+
export class DecibriAudioProvider {
|
|
7
|
+
sampleRate;
|
|
8
|
+
frameSamples;
|
|
9
|
+
device;
|
|
10
|
+
onStreamError;
|
|
11
|
+
mic = null;
|
|
12
|
+
frameCallback = null;
|
|
13
|
+
accumulator;
|
|
14
|
+
accWrite = 0;
|
|
15
|
+
constructor(sampleRate, frameSamples, options) {
|
|
16
|
+
this.sampleRate = sampleRate;
|
|
17
|
+
this.frameSamples = frameSamples;
|
|
18
|
+
this.device = options?.device;
|
|
19
|
+
this.onStreamError = options?.onStreamError;
|
|
20
|
+
this.accumulator = new Float32Array(frameSamples);
|
|
21
|
+
}
|
|
22
|
+
getSampleRate() {
|
|
23
|
+
return this.sampleRate;
|
|
24
|
+
}
|
|
25
|
+
onFrame(callback) {
|
|
26
|
+
this.frameCallback = callback;
|
|
27
|
+
}
|
|
28
|
+
async start() {
|
|
29
|
+
if (this.mic)
|
|
30
|
+
return;
|
|
31
|
+
const framesPerBuffer = Math.min(8192, Math.max(256, Math.floor(this.frameSamples / 2)));
|
|
32
|
+
const mic = new Decibri({
|
|
33
|
+
sampleRate: this.sampleRate,
|
|
34
|
+
channels: 1,
|
|
35
|
+
format: 'float32',
|
|
36
|
+
framesPerBuffer,
|
|
37
|
+
...(this.device !== undefined ? { device: this.device } : {}),
|
|
38
|
+
});
|
|
39
|
+
this.mic = mic;
|
|
40
|
+
this.accWrite = 0;
|
|
41
|
+
mic.on('data', (chunk) => {
|
|
42
|
+
this.pushPcm(chunk);
|
|
43
|
+
});
|
|
44
|
+
mic.on('error', (err) => {
|
|
45
|
+
this.onStreamError?.(err);
|
|
46
|
+
});
|
|
47
|
+
mic.resume();
|
|
48
|
+
}
|
|
49
|
+
stop() {
|
|
50
|
+
if (!this.mic)
|
|
51
|
+
return;
|
|
52
|
+
this.mic.removeAllListeners('data');
|
|
53
|
+
this.mic.removeAllListeners('error');
|
|
54
|
+
this.mic.stop();
|
|
55
|
+
this.mic = null;
|
|
56
|
+
this.accWrite = 0;
|
|
57
|
+
this.accumulator.fill(0);
|
|
58
|
+
}
|
|
59
|
+
pushPcm(chunk) {
|
|
60
|
+
const sampleCount = chunk.length >> 2;
|
|
61
|
+
const f32 = new Float32Array(sampleCount);
|
|
62
|
+
for (let i = 0; i < sampleCount; i++) {
|
|
63
|
+
f32[i] = chunk.readFloatLE(i * 4);
|
|
64
|
+
}
|
|
65
|
+
let offset = 0;
|
|
66
|
+
while (offset < f32.length) {
|
|
67
|
+
const take = Math.min(this.frameSamples - this.accWrite, f32.length - offset);
|
|
68
|
+
this.accumulator.set(f32.subarray(offset, offset + take), this.accWrite);
|
|
69
|
+
this.accWrite += take;
|
|
70
|
+
offset += take;
|
|
71
|
+
if (this.accWrite >= this.frameSamples) {
|
|
72
|
+
this.frameCallback?.(Float32Array.from(this.accumulator));
|
|
73
|
+
this.accWrite = 0;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
//# sourceMappingURL=decibri-audio.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"decibri-audio.js","sourceRoot":"","sources":["../src/decibri-audio.ts"],"names":[],"mappings":"AAAA,OAAO,OAAO,MAAM,SAAS,CAAA;AAY7B;;;GAGG;AACH,MAAM,OAAO,oBAAoB;IACd,UAAU,CAAQ;IAClB,YAAY,CAAQ;IACpB,MAAM,CAA6B;IACnC,aAAa,CAAoC;IAE1D,GAAG,GAA2B,IAAI,CAAA;IAClC,aAAa,GAA6C,IAAI,CAAA;IAC9D,WAAW,CAAc;IACzB,QAAQ,GAAG,CAAC,CAAA;IAEpB,YACE,UAAkB,EAClB,YAAoB,EACpB,OAA6B;QAE7B,IAAI,CAAC,UAAU,GAAG,UAAU,CAAA;QAC5B,IAAI,CAAC,YAAY,GAAG,YAAY,CAAA;QAChC,IAAI,CAAC,MAAM,GAAG,OAAO,EAAE,MAAM,CAAA;QAC7B,IAAI,CAAC,aAAa,GAAG,OAAO,EAAE,aAAa,CAAA;QAC3C,IAAI,CAAC,WAAW,GAAG,IAAI,YAAY,CAAC,YAAY,CAAC,CAAA;IACnD,CAAC;IAED,aAAa;QACX,OAAO,IAAI,CAAC,UAAU,CAAA;IACxB,CAAC;IAED,OAAO,CAAC,QAAyC;QAC/C,IAAI,CAAC,aAAa,GAAG,QAAQ,CAAA;IAC/B,CAAC;IAED,KAAK,CAAC,KAAK;QACT,IAAI,IAAI,CAAC,GAAG;YAAE,OAAM;QAEpB,MAAM,eAAe,GAAG,IAAI,CAAC,GAAG,CAC9B,IAAI,EACJ,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,YAAY,GAAG,CAAC,CAAC,CAAC,CACjD,CAAA;QAED,MAAM,GAAG,GAAG,IAAI,OAAO,CAAC;YACtB,UAAU,EAAE,IAAI,CAAC,UAAU;YAC3B,QAAQ,EAAE,CAAC;YACX,MAAM,EAAE,SAAS;YACjB,eAAe;YACf,GAAG,CAAC,IAAI,CAAC,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SAC9D,CAAC,CAAA;QAEF,IAAI,CAAC,GAAG,GAAG,GAAG,CAAA;QACd,IAAI,CAAC,QAAQ,GAAG,CAAC,CAAA;QAEjB,GAAG,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,KAAa,EAAE,EAAE;YAC/B,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,CAAA;QACrB,CAAC,CAAC,CAAA;QACF,GAAG,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,GAAU,EAAE,EAAE;YAC7B,IAAI,CAAC,aAAa,EAAE,CAAC,GAAG,CAAC,CAAA;QAC3B,CAAC,CAAC,CAAA;QAEF,GAAG,CAAC,MAAM,EAAE,CAAA;IACd,CAAC;IAED,IAAI;QACF,IAAI,CAAC,IAAI,CAAC,GAAG;YAAE,OAAM;QACrB,IAAI,CAAC,GAAG,CAAC,kBAAkB,CAAC,MAAM,CAAC,CAAA;QACnC,IAAI,CAAC,GAAG,CAAC,kBAAkB,CAAC,OAAO,CAAC,CAAA;QACpC,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,CAAA;QACf,IAAI,CAAC,GAAG,GAAG,IAAI,CAAA;QACf,IAAI,CAAC,QAAQ,GAAG,CAAC,CAAA;QACjB,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;IAC1B,CAAC;IAEO,OAAO,CAAC,KAAa;QAC3B,MAAM,WAAW,GAAG,KAAK,CAAC,MAAM,IAAI,CAAC,CAAA;QACrC,MAAM,GAAG,GAAG,IAAI,YAAY,CAAC,WAAW,CAAC,CAAA;QACzC,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,WAAW,EAAE,CAAC,EAAE,EAAE,CAAC;YACrC,GAAG,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC,WAAW,CAAC,CAAC,GAAG,CAAC,CAAC,CAAA;QACnC,CAAC;QAED,IAAI,MAAM,GAAG,CAAC,CAAA;QACd,OAAO,MAAM,GAAG,GAAG,CAAC,MAAM,EAAE,CAAC;YAC3B,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,CACnB,IAAI,CAAC,YAAY,GAAG,IAAI,CAAC,QAAQ,EACjC,GAAG,CAAC,MAAM,GAAG,MAAM,CACpB,CAAA;YACD,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,GAAG,CAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC,EAAE,IAAI,CAAC,QAAQ,CAAC,CAAA;YACxE,IAAI,CAAC,QAAQ,IAAI,IAAI,CAAA;YACrB,MAAM,IAAI,IAAI,CAAA;YACd,IAAI,IAAI,CAAC,QAAQ,IAAI,IAAI,CAAC,YAAY,EAAE,CAAC;gBACvC,IAAI,CAAC,aAAa,EAAE,CAAC,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,CAAA;gBACzD,IAAI,CAAC,QAAQ,GAAG,CAAC,CAAA;YACnB,CAAC;QACH,CAAC;IACH,CAAC;CACF"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { resolveColorMode, stripAnsi } from '../ansi.js';
|
|
2
|
+
import { COLOR_BAR_SLOTS, FANCY_BAR_SLOTS, buildFancyGaugeBar, buildGaugeBar, fitOneTTYLine, formatInfoLine, } from '../format-line.js';
|
|
3
|
+
const STYLE_LABELS = {
|
|
4
|
+
standard: 'Standard',
|
|
5
|
+
colors: 'Colors',
|
|
6
|
+
ansi: 'ANSI',
|
|
7
|
+
};
|
|
8
|
+
export function displayStyleLabel(style) {
|
|
9
|
+
return STYLE_LABELS[style];
|
|
10
|
+
}
|
|
11
|
+
export const DISPLAY_STYLES = ['standard', 'colors', 'ansi'];
|
|
12
|
+
/**
|
|
13
|
+
* Default when --style is omitted: ANSI if color mode would be on (rich escapes),
|
|
14
|
+
* else Colors on a TTY (layout with optional color when --color allows), else Standard.
|
|
15
|
+
*/
|
|
16
|
+
export function defaultDisplayStyle(isTTY, env) {
|
|
17
|
+
if (isTTY && resolveColorMode('auto', isTTY, env))
|
|
18
|
+
return 'ansi';
|
|
19
|
+
if (isTTY)
|
|
20
|
+
return 'colors';
|
|
21
|
+
return 'standard';
|
|
22
|
+
}
|
|
23
|
+
function effectiveColor(ctx) {
|
|
24
|
+
if (ctx.style === 'standard')
|
|
25
|
+
return false;
|
|
26
|
+
return resolveColorMode(ctx.colorChoice, ctx.isTTY, ctx.env);
|
|
27
|
+
}
|
|
28
|
+
/** One physical line: info + bar; no wrap past terminal width. */
|
|
29
|
+
export function renderTunerLine(result, ctx) {
|
|
30
|
+
const cols = Math.max(20, ctx.termWidth);
|
|
31
|
+
const color = effectiveColor(ctx);
|
|
32
|
+
if (ctx.style === 'ansi') {
|
|
33
|
+
const bar = buildFancyGaugeBar(result.cents, FANCY_BAR_SLOTS, color, result.closestString?.inTune ?? false);
|
|
34
|
+
const line = `${formatInfoLine(result, color)} ${bar}`;
|
|
35
|
+
return fitOneTTYLine(line, cols);
|
|
36
|
+
}
|
|
37
|
+
const bar = buildGaugeBar(result.cents, COLOR_BAR_SLOTS, color, result.closestString?.inTune ?? false);
|
|
38
|
+
const line = `${formatInfoLine(result, color)} ${bar}`;
|
|
39
|
+
return fitOneTTYLine(line, cols);
|
|
40
|
+
}
|
|
41
|
+
//# sourceMappingURL=styles.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"styles.js","sourceRoot":"","sources":["../../src/display/styles.ts"],"names":[],"mappings":"AACA,OAAO,EAAoB,gBAAgB,EAAE,SAAS,EAAE,MAAM,YAAY,CAAA;AAC1E,OAAO,EACL,eAAe,EACf,eAAe,EACf,kBAAkB,EAClB,aAAa,EACb,aAAa,EACb,cAAc,GACf,MAAM,mBAAmB,CAAA;AAI1B,MAAM,YAAY,GAAiC;IACjD,QAAQ,EAAE,UAAU;IACpB,MAAM,EAAE,QAAQ;IAChB,IAAI,EAAE,MAAM;CACb,CAAA;AAED,MAAM,UAAU,iBAAiB,CAAC,KAAmB;IACnD,OAAO,YAAY,CAAC,KAAK,CAAC,CAAA;AAC5B,CAAC;AAED,MAAM,CAAC,MAAM,cAAc,GAAmB,CAAC,UAAU,EAAE,QAAQ,EAAE,MAAM,CAAC,CAAA;AAE5E;;;GAGG;AACH,MAAM,UAAU,mBAAmB,CACjC,KAAc,EACd,GAAsB;IAEtB,IAAI,KAAK,IAAI,gBAAgB,CAAC,MAAM,EAAE,KAAK,EAAE,GAAG,CAAC;QAAE,OAAO,MAAM,CAAA;IAChE,IAAI,KAAK;QAAE,OAAO,QAAQ,CAAA;IAC1B,OAAO,UAAU,CAAA;AACnB,CAAC;AAUD,SAAS,cAAc,CAAC,GAAkB;IACxC,IAAI,GAAG,CAAC,KAAK,KAAK,UAAU;QAAE,OAAO,KAAK,CAAA;IAC1C,OAAO,gBAAgB,CAAC,GAAG,CAAC,WAAW,EAAE,GAAG,CAAC,KAAK,EAAE,GAAG,CAAC,GAAG,CAAC,CAAA;AAC9D,CAAC;AAED,kEAAkE;AAClE,MAAM,UAAU,eAAe,CAC7B,MAAmB,EACnB,GAAkB;IAElB,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,GAAG,CAAC,SAAS,CAAC,CAAA;IACxC,MAAM,KAAK,GAAG,cAAc,CAAC,GAAG,CAAC,CAAA;IAEjC,IAAI,GAAG,CAAC,KAAK,KAAK,MAAM,EAAE,CAAC;QACzB,MAAM,GAAG,GAAG,kBAAkB,CAC5B,MAAM,CAAC,KAAK,EACZ,eAAe,EACf,KAAK,EACL,MAAM,CAAC,aAAa,EAAE,MAAM,IAAI,KAAK,CACtC,CAAA;QACD,MAAM,IAAI,GAAG,GAAG,cAAc,CAAC,MAAM,EAAE,KAAK,CAAC,KAAK,GAAG,EAAE,CAAA;QACvD,OAAO,aAAa,CAAC,IAAI,EAAE,IAAI,CAAC,CAAA;IAClC,CAAC;IAED,MAAM,GAAG,GAAG,aAAa,CACvB,MAAM,CAAC,KAAK,EACZ,eAAe,EACf,KAAK,EACL,MAAM,CAAC,aAAa,EAAE,MAAM,IAAI,KAAK,CACtC,CAAA;IACD,MAAM,IAAI,GAAG,GAAG,cAAc,CAAC,MAAM,EAAE,KAAK,CAAC,KAAK,GAAG,EAAE,CAAA;IACvD,OAAO,aAAa,CAAC,IAAI,EAAE,IAAI,CAAC,CAAA;AAClC,CAAC"}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { formatMarkerCell, formatTrackCell, formatTrackShade, stripAnsi, styleCents, styleDim, styleGaugeMarker, styleInTune, styleNote, } from './ansi.js';
|
|
2
|
+
export const COLOR_BAR_SLOTS = 17;
|
|
3
|
+
export const FANCY_BAR_SLOTS = 28;
|
|
4
|
+
const SHADE_LIGHT = '░';
|
|
5
|
+
const SHADE_MID = '▒';
|
|
6
|
+
const SHADE_HEAVY = '▓';
|
|
7
|
+
function clampCents(cents) {
|
|
8
|
+
return Math.max(-50, Math.min(50, cents));
|
|
9
|
+
}
|
|
10
|
+
function centsToMarkerIndex(cents, barLen) {
|
|
11
|
+
const c = clampCents(cents);
|
|
12
|
+
const t = (c + 50) / 100;
|
|
13
|
+
return Math.round(t * (barLen - 1));
|
|
14
|
+
}
|
|
15
|
+
export function buildGaugeBar(cents, barLen, color, stringInTune) {
|
|
16
|
+
const c = clampCents(cents);
|
|
17
|
+
const markerIdx = centsToMarkerIndex(c, barLen);
|
|
18
|
+
const centerIdx = (barLen - 1) / 2;
|
|
19
|
+
const mk = styleGaugeMarker(color, c, stringInTune);
|
|
20
|
+
let s = '';
|
|
21
|
+
for (let i = 0; i < barLen; i++) {
|
|
22
|
+
if (i === markerIdx) {
|
|
23
|
+
s += formatMarkerCell(color, mk.open, mk.close);
|
|
24
|
+
}
|
|
25
|
+
else {
|
|
26
|
+
s += formatTrackCell(color, i, centerIdx);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return s;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Finer cents resolution: shade falloff around needle; tinted track when color is on.
|
|
33
|
+
*/
|
|
34
|
+
export function buildFancyGaugeBar(cents, barLen, color, stringInTune) {
|
|
35
|
+
const c = clampCents(cents);
|
|
36
|
+
const pos = (c + 50) / 100;
|
|
37
|
+
const centerIdx = (barLen - 1) / 2;
|
|
38
|
+
const mk = styleGaugeMarker(color, c, stringInTune);
|
|
39
|
+
let s = '';
|
|
40
|
+
for (let i = 0; i < barLen; i++) {
|
|
41
|
+
const cellCenter = (i + 0.5) / barLen;
|
|
42
|
+
const dist = Math.abs(cellCenter - pos) * barLen;
|
|
43
|
+
let ch;
|
|
44
|
+
if (dist < 0.2) {
|
|
45
|
+
s += formatMarkerCell(color, mk.open, mk.close);
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
if (dist < 0.55) {
|
|
49
|
+
ch = SHADE_HEAVY;
|
|
50
|
+
}
|
|
51
|
+
else if (dist < 1.0) {
|
|
52
|
+
ch = SHADE_MID;
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
ch = SHADE_LIGHT;
|
|
56
|
+
}
|
|
57
|
+
if (!color) {
|
|
58
|
+
s += ch;
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
if (dist < 1.0) {
|
|
62
|
+
const tint = styleGaugeMarker(color, c, stringInTune);
|
|
63
|
+
s += `${tint.open}${ch}${tint.close}`;
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
s += formatTrackShade(color, i, centerIdx, ch);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return s;
|
|
70
|
+
}
|
|
71
|
+
/** Fixed width so the following gauge column does not drift (−50…+50, one decimal). */
|
|
72
|
+
const CHROMATIC_CENTS_FIELD = 6;
|
|
73
|
+
/** e.g. 1234.5 Hz — pad so bar starts at same column. */
|
|
74
|
+
const HZ_FIELD = 8;
|
|
75
|
+
/** String-target cents (integer), signed; keeps │ block width stable. */
|
|
76
|
+
const STRING_CENTS_OFF_FIELD = 5;
|
|
77
|
+
function formatSignedOneDecimal(value, fieldWidth) {
|
|
78
|
+
const sign = value >= 0 ? '+' : '-';
|
|
79
|
+
const abs = Math.abs(value).toFixed(1);
|
|
80
|
+
return `${sign}${abs}`.padStart(fieldWidth, ' ');
|
|
81
|
+
}
|
|
82
|
+
function formatHzField(hz) {
|
|
83
|
+
return `${hz.toFixed(1).padStart(HZ_FIELD, ' ')} Hz`;
|
|
84
|
+
}
|
|
85
|
+
function formatStringCentsOff(off) {
|
|
86
|
+
const s = off >= 0 ? `+${off.toFixed(0)}` : off.toFixed(0);
|
|
87
|
+
return s.padStart(STRING_CENTS_OFF_FIELD, ' ');
|
|
88
|
+
}
|
|
89
|
+
/** Info field: note, Hz, cents, optional string row (shared by all styles). */
|
|
90
|
+
export function formatInfoLine(result, color) {
|
|
91
|
+
const centsStr = formatSignedOneDecimal(result.cents, CHROMATIC_CENTS_FIELD);
|
|
92
|
+
const n = styleNote(color);
|
|
93
|
+
const dim = styleDim(color);
|
|
94
|
+
const ct = styleCents(color, result.cents);
|
|
95
|
+
let line = `${n.open}${result.note}${result.octave}${n.close}` +
|
|
96
|
+
` ${dim.open}${formatHzField(result.frequency)}${dim.close}` +
|
|
97
|
+
` ${ct.open}${centsStr}¢${ct.close}`;
|
|
98
|
+
const cs = result.closestString;
|
|
99
|
+
if (cs) {
|
|
100
|
+
const off = formatStringCentsOff(cs.centsOff);
|
|
101
|
+
const ok = styleInTune(color);
|
|
102
|
+
const mark = cs.inTune ? `${ok.open} ✓${ok.close}` : '';
|
|
103
|
+
line += ` │ ${cs.name} ${off}¢${mark}`;
|
|
104
|
+
}
|
|
105
|
+
return line;
|
|
106
|
+
}
|
|
107
|
+
export function fitOneTTYLine(ansiLine, maxCols) {
|
|
108
|
+
const plain = stripAnsi(ansiLine);
|
|
109
|
+
if (plain.length <= maxCols)
|
|
110
|
+
return ansiLine;
|
|
111
|
+
const ell = '…';
|
|
112
|
+
const cut = Math.max(1, maxCols - ell.length);
|
|
113
|
+
return plain.slice(0, cut) + ell;
|
|
114
|
+
}
|
|
115
|
+
//# sourceMappingURL=format-line.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"format-line.js","sourceRoot":"","sources":["../src/format-line.ts"],"names":[],"mappings":"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"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import process from 'node:process';
|
|
3
|
+
import { listAudioDevices, listInstruments, listTunings } from './cli-lists.js';
|
|
4
|
+
import { createCliCommand, parseCliRuntime } from './parse-args.js';
|
|
5
|
+
import { runTunerSession } from './run-tuner-session.js';
|
|
6
|
+
/** Full help including `addHelpText('after', …)` (not just `helpInformation()`). */
|
|
7
|
+
function printProgramHelp() {
|
|
8
|
+
const cmd = createCliCommand();
|
|
9
|
+
const parts = [];
|
|
10
|
+
cmd.configureOutput({
|
|
11
|
+
writeOut: (s) => parts.push(s),
|
|
12
|
+
writeErr: (s) => parts.push(s),
|
|
13
|
+
});
|
|
14
|
+
cmd.outputHelp();
|
|
15
|
+
process.stdout.write(parts.join(''));
|
|
16
|
+
}
|
|
17
|
+
async function main() {
|
|
18
|
+
let parsed;
|
|
19
|
+
try {
|
|
20
|
+
parsed = parseCliRuntime(process.argv);
|
|
21
|
+
}
|
|
22
|
+
catch (e) {
|
|
23
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
24
|
+
console.error(msg);
|
|
25
|
+
printProgramHelp();
|
|
26
|
+
process.exitCode = 1;
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
if (parsed.kind === 'help') {
|
|
30
|
+
printProgramHelp();
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
if (parsed.kind === 'list-devices') {
|
|
34
|
+
listAudioDevices();
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
if (parsed.kind === 'list-instruments') {
|
|
38
|
+
listInstruments();
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
if (parsed.kind === 'list-tunings') {
|
|
42
|
+
if (!listTunings(parsed.instrumentId)) {
|
|
43
|
+
process.exitCode = 1;
|
|
44
|
+
}
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
await runTunerSession(parsed.args);
|
|
48
|
+
}
|
|
49
|
+
void main().catch((e) => {
|
|
50
|
+
console.error(e instanceof Error ? e.message : e);
|
|
51
|
+
process.exitCode = 1;
|
|
52
|
+
});
|
|
53
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA,OAAO,OAAO,MAAM,cAAc,CAAA;AAClC,OAAO,EAAE,gBAAgB,EAAE,eAAe,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAA;AAC/E,OAAO,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAA;AAEnE,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAA;AAExD,oFAAoF;AACpF,SAAS,gBAAgB;IACvB,MAAM,GAAG,GAAG,gBAAgB,EAAE,CAAA;IAC9B,MAAM,KAAK,GAAa,EAAE,CAAA;IAC1B,GAAG,CAAC,eAAe,CAAC;QAClB,QAAQ,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC;QAC9B,QAAQ,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC;KAC/B,CAAC,CAAA;IACF,GAAG,CAAC,UAAU,EAAE,CAAA;IAChB,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAA;AACtC,CAAC;AAED,KAAK,UAAU,IAAI;IACjB,IAAI,MAAiB,CAAA;IACrB,IAAI,CAAC;QACH,MAAM,GAAG,eAAe,CAAC,OAAO,CAAC,IAAI,CAAC,CAAA;IACxC,CAAC;IAAC,OAAO,CAAC,EAAE,CAAC;QACX,MAAM,GAAG,GAAG,CAAC,YAAY,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAA;QACtD,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,CAAA;QAClB,gBAAgB,EAAE,CAAA;QAClB,OAAO,CAAC,QAAQ,GAAG,CAAC,CAAA;QACpB,OAAM;IACR,CAAC;IAED,IAAI,MAAM,CAAC,IAAI,KAAK,MAAM,EAAE,CAAC;QAC3B,gBAAgB,EAAE,CAAA;QAClB,OAAM;IACR,CAAC;IAED,IAAI,MAAM,CAAC,IAAI,KAAK,cAAc,EAAE,CAAC;QACnC,gBAAgB,EAAE,CAAA;QAClB,OAAM;IACR,CAAC;IAED,IAAI,MAAM,CAAC,IAAI,KAAK,kBAAkB,EAAE,CAAC;QACvC,eAAe,EAAE,CAAA;QACjB,OAAM;IACR,CAAC;IAED,IAAI,MAAM,CAAC,IAAI,KAAK,cAAc,EAAE,CAAC;QACnC,IAAI,CAAC,WAAW,CAAC,MAAM,CAAC,YAAY,CAAC,EAAE,CAAC;YACtC,OAAO,CAAC,QAAQ,GAAG,CAAC,CAAA;QACtB,CAAC;QACD,OAAM;IACR,CAAC;IAED,MAAM,eAAe,CAAC,MAAM,CAAC,IAAI,CAAC,CAAA;AACpC,CAAC;AAED,KAAK,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE;IACtB,OAAO,CAAC,KAAK,CAAC,CAAC,YAAY,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;IACjD,OAAO,CAAC,QAAQ,GAAG,CAAC,CAAA;AACtB,CAAC,CAAC,CAAA"}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import readline from 'node:readline';
|
|
2
|
+
function isLetterChar(c) {
|
|
3
|
+
return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z');
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Decode readline keypress; Windows/ConPTY often differs from macOS/Linux (name vs sequence).
|
|
7
|
+
*/
|
|
8
|
+
export function parseMenuKey(key) {
|
|
9
|
+
if (!key)
|
|
10
|
+
return null;
|
|
11
|
+
const seq = key.sequence ?? '';
|
|
12
|
+
// Ctrl+C sends ETX in raw mode (especially Windows); does not always set key.name === 'c'
|
|
13
|
+
if (seq === '\u0003' || seq === '\x03') {
|
|
14
|
+
return { kind: 'ctrl-c' };
|
|
15
|
+
}
|
|
16
|
+
if (key.ctrl && (key.name === 'c' || seq === '\u0003')) {
|
|
17
|
+
return { kind: 'ctrl-c' };
|
|
18
|
+
}
|
|
19
|
+
if (key.name === 'up' || key.name === 'k')
|
|
20
|
+
return { kind: 'up' };
|
|
21
|
+
if (key.name === 'down' || key.name === 'j')
|
|
22
|
+
return { kind: 'down' };
|
|
23
|
+
if (key.name === 'left' || key.name === 'h')
|
|
24
|
+
return { kind: 'left' };
|
|
25
|
+
if (key.name === 'right' || key.name === 'l')
|
|
26
|
+
return { kind: 'right' };
|
|
27
|
+
if (key.name === 'return' || key.name === 'enter')
|
|
28
|
+
return { kind: 'enter' };
|
|
29
|
+
if (key.name === 'escape')
|
|
30
|
+
return { kind: 'escape' };
|
|
31
|
+
if (!key.ctrl && !key.meta) {
|
|
32
|
+
const name = key.name ?? '';
|
|
33
|
+
if (name.length === 1 && isLetterChar(name)) {
|
|
34
|
+
return { kind: 'letter', ch: name.toLowerCase() };
|
|
35
|
+
}
|
|
36
|
+
if (seq.length === 1) {
|
|
37
|
+
const ch0 = seq[0];
|
|
38
|
+
if (ch0 !== undefined && isLetterChar(ch0)) {
|
|
39
|
+
return { kind: 'letter', ch: ch0.toLowerCase() };
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Raw stdin + keypress events. Uninstall restores prior rawMode when possible.
|
|
47
|
+
*/
|
|
48
|
+
export function installKeypress(stdin) {
|
|
49
|
+
const listeners = new Set();
|
|
50
|
+
if (!stdin.isTTY) {
|
|
51
|
+
return {
|
|
52
|
+
onKey(fn) {
|
|
53
|
+
listeners.add(fn);
|
|
54
|
+
},
|
|
55
|
+
uninstall: () => {
|
|
56
|
+
listeners.clear();
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
readline.emitKeypressEvents(stdin);
|
|
61
|
+
stdin.setRawMode(true);
|
|
62
|
+
stdin.resume();
|
|
63
|
+
const handler = (_s, key) => {
|
|
64
|
+
const mk = parseMenuKey(key);
|
|
65
|
+
if (!mk)
|
|
66
|
+
return;
|
|
67
|
+
for (const fn of listeners) {
|
|
68
|
+
fn(mk);
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
stdin.on('keypress', handler);
|
|
72
|
+
return {
|
|
73
|
+
onKey(fn) {
|
|
74
|
+
listeners.add(fn);
|
|
75
|
+
},
|
|
76
|
+
uninstall() {
|
|
77
|
+
stdin.off('keypress', handler);
|
|
78
|
+
try {
|
|
79
|
+
stdin.setRawMode(false);
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
/* ignore */
|
|
83
|
+
}
|
|
84
|
+
listeners.clear();
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
//# sourceMappingURL=keys.js.map
|