xp-command 1.0.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/LICENSE +21 -0
- package/README.md +298 -0
- package/bin/index.js +240 -0
- package/package.json +44 -0
- package/src/api.js +138 -0
- package/src/clipboard.js +19 -0
- package/src/config.js +69 -0
- package/src/config.yml +137 -0
- package/src/console.js +12 -0
- package/src/error.js +12 -0
- package/src/history.js +53 -0
- package/src/sleep.js +6 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Boris Diakur
|
|
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,298 @@
|
|
|
1
|
+
# 🛩 xp-command
|
|
2
|
+
|
|
3
|
+
**Quick cockpit commands for X-Plane 12** - set your radios, altimeter, autopilot, and more from the terminal while flying.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 🎯 What does this tool do?
|
|
8
|
+
|
|
9
|
+
During instrument flight in X-Plane 12, constantly clicking tiny cockpit buttons to adjust altimeter, transponder, or radio frequencies can be tricky without the appropriate hardware. This tool lets you type quick commands like `q1013` or `x7000` to instantly set instruments without touching the mouse.
|
|
10
|
+
|
|
11
|
+
**Example commands:**
|
|
12
|
+
- `q1013` → Set altimeter to 1013 hPa
|
|
13
|
+
- `x7000` → Set transponder to 7000
|
|
14
|
+
- `a250` → Set autopilot altitude to FL250
|
|
15
|
+
- `c1118900` → Set COM1 to 118.900 MHz
|
|
16
|
+
|
|
17
|
+
The tool remembers your command history and automatically creates aircraft-specific profiles which can then be adjusted to your specific needs.
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## 🚀 Installation
|
|
22
|
+
|
|
23
|
+
### Step 1: Install Node.js
|
|
24
|
+
|
|
25
|
+
You need Node.js version 22 or higher installed on your system:
|
|
26
|
+
|
|
27
|
+
1. **Download Node.js**: Go to https://nodejs.org/en/download/ and download the **LTS version** (Long Term Support)
|
|
28
|
+
2. **Install**: Run the downloaded installer
|
|
29
|
+
3. **Verify**: Open Terminal and type:
|
|
30
|
+
```bash
|
|
31
|
+
node --version
|
|
32
|
+
```
|
|
33
|
+
You should see something like `v22.x.x`
|
|
34
|
+
|
|
35
|
+
### Step 2: Install xp-command globally
|
|
36
|
+
|
|
37
|
+
Open Terminal and run:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
npm install -g xp-command
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
This installs xp-command globally so you can run it from anywhere.
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## 🛠 X-Plane setup
|
|
48
|
+
|
|
49
|
+
You shouldn't need to perform any extra setup steps in X-Plane. There is no plugin that needs to be installed. If you still want to check if the Web API (which allows xp-command to communicate with X-Plane) is configured correctly, here is how you can do it:
|
|
50
|
+
|
|
51
|
+
1. In X-Plane 12, go to **Settings → Data Output**
|
|
52
|
+
2. Find **"Enable Web Server API"** and make sure it's enabled
|
|
53
|
+
3. Note the **port number** (default is `8086`)
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
## 🎮 Usage
|
|
58
|
+
|
|
59
|
+
### Starting the tool
|
|
60
|
+
|
|
61
|
+
1. **Load into an aircraft** in X-Plane 12
|
|
62
|
+
2. **Open Terminal** on your machine
|
|
63
|
+
3. **Run**:
|
|
64
|
+
```bash
|
|
65
|
+
xp-command
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
You'll see a prompt:
|
|
69
|
+
```
|
|
70
|
+
🛩 █
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Now you can enter commands!
|
|
74
|
+
|
|
75
|
+
### Using a custom port
|
|
76
|
+
|
|
77
|
+
If you changed the Web API port from 8086 to another port, you'll need to start xp-command accordingly:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
xp-command --port 8090
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### Exiting
|
|
84
|
+
|
|
85
|
+
Type `exit` or press `Ctrl+C`
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## 📋 Pre-configured commands
|
|
90
|
+
|
|
91
|
+
All pre-configured commands work well with most aircraft I currently fly, but you may want to adjust them for **your** aircraft (see _Aircraft-specific profiles_ section below).
|
|
92
|
+
|
|
93
|
+
### Barometric Pressure
|
|
94
|
+
|
|
95
|
+
| Command | Action | Example |
|
|
96
|
+
|---------|-----------------------------------------|--------------|
|
|
97
|
+
| `q` | Get QNH in hPa/mb (copied to clipboard) | `q` → `1013` |
|
|
98
|
+
| `l` | Get QNH in inHg (copied to clipboard) | `l` → `2992` |
|
|
99
|
+
| `q####` | Set QNH in hPa/mb | `q1013` |
|
|
100
|
+
| `l####` | Set QNH in inHg | `l2992` |
|
|
101
|
+
|
|
102
|
+
### Autopilot
|
|
103
|
+
|
|
104
|
+
| Command | Action | Example |
|
|
105
|
+
|----------|-----------------------------|---------------------------|
|
|
106
|
+
| `h###` | Set heading | `h090` → 090° |
|
|
107
|
+
| `a###` | Set altitude (flight level) | `a250` → FL250 (25000 ft) |
|
|
108
|
+
| `a#####` | Set altitude (exact feet) | `a03500` → 3500 ft |
|
|
109
|
+
| `s###` | Set speed in KIAS | `s180` → 180 knots |
|
|
110
|
+
| `s##` | Set speed in Mach | `s78` → Mach 0.78 |
|
|
111
|
+
| `v####` | Set vertical speed | `v1500` → 1500 ft/min |
|
|
112
|
+
|
|
113
|
+
### Radios
|
|
114
|
+
|
|
115
|
+
| Command | Action | Format | Example |
|
|
116
|
+
|------------|------------------|------------|-----------------------|
|
|
117
|
+
| `c1#####` | Set COM1 active | No decimal | `c1118900` → 118.900 |
|
|
118
|
+
| `cs1#####` | Set COM1 standby | No decimal | `cs1121750` → 121.750 |
|
|
119
|
+
| `c2#####` | Set COM2 active | No decimal | `c2119200` → 119.200 |
|
|
120
|
+
| `cs2#####` | Set COM2 standby | No decimal | `cs2122800` → 122.800 |
|
|
121
|
+
| `n1#####` | Set NAV1 active | No decimal | `n1110500` → 110.500 |
|
|
122
|
+
| `ns1#####` | Set NAV1 standby | No decimal | `ns1116600` → 116.600 |
|
|
123
|
+
| `n2#####` | Set NAV2 active | No decimal | `n2113900` → 113.900 |
|
|
124
|
+
| `ns2#####` | Set NAV2 standby | No decimal | `ns2115700` → 115.700 |
|
|
125
|
+
|
|
126
|
+
**Radio frequency format:** Remove the decimal point. For `118.900`, type `118900`. For COM1 and COM2 you can omit the last digit – the value will be padded with 0.
|
|
127
|
+
|
|
128
|
+
### Transponder
|
|
129
|
+
|
|
130
|
+
| Command | Action | Example |
|
|
131
|
+
|---------|-----------------|---------|
|
|
132
|
+
| `x####` | Set squawk code | `x7000` |
|
|
133
|
+
|
|
134
|
+
---
|
|
135
|
+
|
|
136
|
+
## ⚙️ Aircraft-specific profiles
|
|
137
|
+
|
|
138
|
+
The first time you run xp-command in a new aircraft, it automatically creates a configuration file at:
|
|
139
|
+
|
|
140
|
+
```
|
|
141
|
+
~/.xp-command/<Aircraft Name>.yml
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### Customizing commands
|
|
145
|
+
|
|
146
|
+
You can edit these YAML files to add aircraft-specific commands or modify existing ones. Note that for changes to take effect you'll need to restart xp-command.
|
|
147
|
+
|
|
148
|
+
**Example**: Add a command to set heading with 10-degree increments:
|
|
149
|
+
|
|
150
|
+
```yaml
|
|
151
|
+
- pattern: "^hdg(\\d{2})$"
|
|
152
|
+
type: set
|
|
153
|
+
dataref: sim/cockpit/autopilot/heading_mag
|
|
154
|
+
transform:
|
|
155
|
+
- mult10
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
**Key components:**
|
|
159
|
+
- `pattern`: Regular expression matching your command (`hdg18`)
|
|
160
|
+
- `type`: Either `get` (read value) or `set` (write value)
|
|
161
|
+
- `dataref`: X-Plane dataref path (find these in X-Plane's DataRef Editor)
|
|
162
|
+
- `transform`: Optional value conversions (multiply, divide, round, etc.)
|
|
163
|
+
|
|
164
|
+
**Finding datarefs**: Use X-Plane's built-in **DataRef Editor** plugin or check [X-Plane datarefs documentation](https://developer.x-plane.com/datarefs/).
|
|
165
|
+
|
|
166
|
+
#### Array Datarefs
|
|
167
|
+
|
|
168
|
+
Many X-Plane datarefs are arrays (e.g., for multiple engines, generators, radios). You can access specific array elements using `[index]` notation:
|
|
169
|
+
|
|
170
|
+
```yaml
|
|
171
|
+
# Check if right generator is on (index 1 = right, 0 = left)
|
|
172
|
+
- pattern: "^gr$"
|
|
173
|
+
type: get
|
|
174
|
+
dataref: sim/cockpit/electrical/generator_on[1]
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
## 🔄 Resetting Aircraft Profiles
|
|
178
|
+
|
|
179
|
+
If you've edited an aircraft configuration and xp-command crashes or stops working, you can reset to default settings by deleting the config files.
|
|
180
|
+
|
|
181
|
+
### Profile Location
|
|
182
|
+
|
|
183
|
+
Aircraft profiles are stored in:
|
|
184
|
+
|
|
185
|
+
**macOS/Linux:** `~/.xp-command/`
|
|
186
|
+
**Windows:** `%USERPROFILE%\.xp-command\`
|
|
187
|
+
|
|
188
|
+
Each aircraft has its own `.yml` file named after the aircraft (e.g., `Cessna 172SP.yml`).
|
|
189
|
+
|
|
190
|
+
### Reset Single Aircraft
|
|
191
|
+
|
|
192
|
+
To reset one specific aircraft, delete its `.yml` file. Next time you load that aircraft and run a command, xp-command will create a fresh default configuration.
|
|
193
|
+
|
|
194
|
+
### Reset All Aircraft
|
|
195
|
+
|
|
196
|
+
To start completely fresh, delete the entire `.xp-command` folder. This removes all custom aircraft configurations.
|
|
197
|
+
The tool will automatically recreate default configurations when you next fly each aircraft and run a command.
|
|
198
|
+
|
|
199
|
+
---
|
|
200
|
+
|
|
201
|
+
## 🔧 Troubleshooting
|
|
202
|
+
|
|
203
|
+
### "No connection - in aircraft?"
|
|
204
|
+
|
|
205
|
+
**Causes:**
|
|
206
|
+
- You're in the X-Plane menu (not loaded into cockpit)
|
|
207
|
+
- Web API is disabled in X-Plane settings
|
|
208
|
+
- X-Plane is not running
|
|
209
|
+
- Wrong port number
|
|
210
|
+
|
|
211
|
+
**Fix:**
|
|
212
|
+
1. Load into aircraft cockpit
|
|
213
|
+
2. Check **Settings → Data Output → Enable Web Server API** is checked
|
|
214
|
+
3. Verify port matches (default 8086)
|
|
215
|
+
|
|
216
|
+
### Command not recognized (red text)
|
|
217
|
+
|
|
218
|
+
**Causes:**
|
|
219
|
+
- Typo in command
|
|
220
|
+
- Command not defined for this aircraft
|
|
221
|
+
- Wrong number of digits
|
|
222
|
+
|
|
223
|
+
**Fix:**
|
|
224
|
+
- Check command format in this README
|
|
225
|
+
- Edit `~/.xp-command/<Aircraft>.yml` to add custom commands
|
|
226
|
+
|
|
227
|
+
### Values not changing in cockpit
|
|
228
|
+
|
|
229
|
+
**Causes:**
|
|
230
|
+
- Wrong dataref for your specific aircraft
|
|
231
|
+
- Aircraft systems override (e.g., autopilot off)
|
|
232
|
+
|
|
233
|
+
**Fix:**
|
|
234
|
+
- Check DataRef Editor to verify correct dataref path
|
|
235
|
+
- Ensure aircraft systems are in correct mode (e.g., autopilot engaged)
|
|
236
|
+
|
|
237
|
+
### xp-command crashes after parsing errors
|
|
238
|
+
|
|
239
|
+
**Cause:** Invalid YAML syntax when editing config files
|
|
240
|
+
**Fix:** Create backups of problematic aircraft's `.yml` files and delete them or the entire profiles folder
|
|
241
|
+
|
|
242
|
+
---
|
|
243
|
+
|
|
244
|
+
## 🗑️ Uninstalling
|
|
245
|
+
|
|
246
|
+
### Uninstall npm package
|
|
247
|
+
|
|
248
|
+
Open Terminal and run:
|
|
249
|
+
|
|
250
|
+
```bash
|
|
251
|
+
npm uninstall -g xp-command
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
### Clean Up Data Files
|
|
255
|
+
|
|
256
|
+
**Aircraft profiles:**
|
|
257
|
+
- macOS/Linux: Delete `~/.xp-command/` folder
|
|
258
|
+
- Windows: Delete `%USERPROFILE%\.xp-command\` folder
|
|
259
|
+
|
|
260
|
+
**Command history:**
|
|
261
|
+
- macOS: Delete `~/Library/Preferences/xp-command-nodejs/` folder
|
|
262
|
+
- Linux: Delete `~/.config/xp-command-nodejs/` folder
|
|
263
|
+
- Windows: Delete `%APPDATA%\xp-command-nodejs\` folder
|
|
264
|
+
|
|
265
|
+
**Complete removal:** Delete both the tool and all data files to fully uninstall xp-command from your system.
|
|
266
|
+
|
|
267
|
+
---
|
|
268
|
+
|
|
269
|
+
## 🛣 How it works (technical overview)
|
|
270
|
+
|
|
271
|
+
1. **Web API connection**: Uses [X-Plane local REST Web API](https://developer.x-plane.com/article/x-plane-web-api/)
|
|
272
|
+
2. **Dataref system**: Reads/writes X-Plane datarefs (internal simulator variables)
|
|
273
|
+
3. **Pattern matching**: Regular expressions match your commands to datarefs
|
|
274
|
+
4. **Transform pipeline**: Converts your input format to X-Plane's expected values
|
|
275
|
+
5. **Aircraft profiles**: YAML configs loaded based on current aircraft name
|
|
276
|
+
6. **Persistent history**: Stored using the [`conf`](https://github.com/sindresorhus/conf) package
|
|
277
|
+
|
|
278
|
+
---
|
|
279
|
+
|
|
280
|
+
## 📝 License
|
|
281
|
+
|
|
282
|
+
MIT License - See repository for details
|
|
283
|
+
|
|
284
|
+
---
|
|
285
|
+
|
|
286
|
+
## 🙏 Credits
|
|
287
|
+
|
|
288
|
+
This project was inspired by http://www.xpluginsdk.org/command_line.htm which unfortunately stopped working for me after upgrading to X-Plane 12.
|
|
289
|
+
|
|
290
|
+
---
|
|
291
|
+
|
|
292
|
+
## 🐛 Issues and contributions
|
|
293
|
+
|
|
294
|
+
Found a bug or want to add features? Submit issues or pull requests on the [GitHub repository](https://github.com/borisdiakur/xp-command).
|
|
295
|
+
|
|
296
|
+
---
|
|
297
|
+
|
|
298
|
+
**👋🏻 Happy flying!**
|
package/bin/index.js
ADDED
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
createPrompt,
|
|
5
|
+
isDownKey,
|
|
6
|
+
isEnterKey,
|
|
7
|
+
isUpKey,
|
|
8
|
+
useKeypress,
|
|
9
|
+
useState,
|
|
10
|
+
} from "@inquirer/core";
|
|
11
|
+
import chalk from "chalk";
|
|
12
|
+
import { program } from "commander";
|
|
13
|
+
import ora from "ora";
|
|
14
|
+
|
|
15
|
+
import { getDatarefValue, initAPI, setDatarefValue } from "../src/api.js";
|
|
16
|
+
import { copyToClipboard } from "../src/clipboard.js";
|
|
17
|
+
import { getConfig } from "../src/config.js";
|
|
18
|
+
import { clearLine, hideCursor, showCursor } from "../src/console.js";
|
|
19
|
+
import { isEconnRefused } from "../src/error.js";
|
|
20
|
+
import history from "../src/history.js";
|
|
21
|
+
import { sleep } from "../src/sleep.js";
|
|
22
|
+
|
|
23
|
+
const PREFIX = "🛩 ";
|
|
24
|
+
|
|
25
|
+
program
|
|
26
|
+
.version("1.0.0")
|
|
27
|
+
.description("xp-command")
|
|
28
|
+
.option("-p, --port <number>", "server port number");
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* @param {string} command
|
|
32
|
+
*/
|
|
33
|
+
const processCommand = async (command) => {
|
|
34
|
+
const spinner = ora(`${PREFIX} ${chalk.cyan(command)}`).start();
|
|
35
|
+
hideCursor();
|
|
36
|
+
|
|
37
|
+
/** @type {import('../src/config.js').CommandConfig} */
|
|
38
|
+
let config;
|
|
39
|
+
try {
|
|
40
|
+
config = await getConfig();
|
|
41
|
+
} catch (error) {
|
|
42
|
+
if (isEconnRefused(error)) {
|
|
43
|
+
spinner.fail(chalk.red(`${PREFIX} No connection - in aircraft?`));
|
|
44
|
+
hideCursor();
|
|
45
|
+
await sleep(1500);
|
|
46
|
+
showCursor();
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* @param {number|Array<number>|string} value
|
|
53
|
+
* @param {import('../src/config.js').Transform} transform
|
|
54
|
+
*/
|
|
55
|
+
const getTransformedValue = (value, transform) => {
|
|
56
|
+
if (Array.isArray(value)) return value.slice();
|
|
57
|
+
|
|
58
|
+
if (transform.startsWith("mult")) {
|
|
59
|
+
const factor = parseFloat(transform.slice(4));
|
|
60
|
+
if (isNaN(factor)) return value;
|
|
61
|
+
return Number(value) * factor;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (transform.toLowerCase().startsWith("tofixed")) {
|
|
65
|
+
const digits = parseInt(transform.slice(7));
|
|
66
|
+
if (isNaN(digits)) return value;
|
|
67
|
+
return Number(Number(value).toFixed(digits));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (transform === "round") {
|
|
71
|
+
return Math.round(Number(value));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return value;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
/** @type {Array<[RegExp, (regExpResult: Array<string> | null) => Promise<void>]>} */
|
|
78
|
+
const matches = config.commands.map((c) => {
|
|
79
|
+
switch (c.type) {
|
|
80
|
+
case "get":
|
|
81
|
+
return [
|
|
82
|
+
new RegExp(c.pattern),
|
|
83
|
+
async () => {
|
|
84
|
+
let value = await getDatarefValue(c.dataref);
|
|
85
|
+
c.transform?.forEach((t) => {
|
|
86
|
+
value = getTransformedValue(value, t);
|
|
87
|
+
});
|
|
88
|
+
await copyToClipboard(JSON.stringify(value));
|
|
89
|
+
spinner.succeed(chalk.green(`${PREFIX} ${value}`));
|
|
90
|
+
hideCursor();
|
|
91
|
+
await sleep(1500);
|
|
92
|
+
clearLine();
|
|
93
|
+
},
|
|
94
|
+
];
|
|
95
|
+
case "set":
|
|
96
|
+
return [
|
|
97
|
+
new RegExp(c.pattern),
|
|
98
|
+
async (regExpResult) => {
|
|
99
|
+
let value = regExpResult[1];
|
|
100
|
+
c.transform?.forEach((t) => {
|
|
101
|
+
value = String(getTransformedValue(value, t));
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
await setDatarefValue(c.dataref, Number(value));
|
|
105
|
+
spinner.succeed(chalk.green(`${PREFIX} ${command}`));
|
|
106
|
+
hideCursor();
|
|
107
|
+
await sleep(1500);
|
|
108
|
+
clearLine();
|
|
109
|
+
},
|
|
110
|
+
];
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
for (const [regexp, cb] of matches) {
|
|
115
|
+
const regexpResult = regexp.exec(command);
|
|
116
|
+
if (regexpResult !== null) {
|
|
117
|
+
try {
|
|
118
|
+
await cb(regexpResult);
|
|
119
|
+
|
|
120
|
+
spinner.succeed();
|
|
121
|
+
hideCursor();
|
|
122
|
+
|
|
123
|
+
showCursor();
|
|
124
|
+
} catch (error) {
|
|
125
|
+
spinner.fail();
|
|
126
|
+
hideCursor();
|
|
127
|
+
|
|
128
|
+
if (error instanceof Error) {
|
|
129
|
+
if (isEconnRefused(error)) {
|
|
130
|
+
clearLine();
|
|
131
|
+
spinner.fail(chalk.red(`${PREFIX} No connection - in aircraft?`));
|
|
132
|
+
await sleep(1500);
|
|
133
|
+
} else if (error.name === "APIError") {
|
|
134
|
+
clearLine();
|
|
135
|
+
spinner.fail(chalk.red(`${PREFIX} ${error.message}`));
|
|
136
|
+
await sleep(1500);
|
|
137
|
+
} else {
|
|
138
|
+
throw error;
|
|
139
|
+
}
|
|
140
|
+
} else {
|
|
141
|
+
throw error;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
await sleep(500);
|
|
145
|
+
showCursor();
|
|
146
|
+
}
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
spinner.fail(chalk.red(`${PREFIX} ${command}`));
|
|
152
|
+
hideCursor();
|
|
153
|
+
|
|
154
|
+
await sleep(1500);
|
|
155
|
+
showCursor();
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
const sayHello = () => {
|
|
159
|
+
console.log(chalk.magenta(" 👋 Hello!"));
|
|
160
|
+
};
|
|
161
|
+
const sayBye = () => {
|
|
162
|
+
console.log(chalk.magenta(" 👋 Bye!"));
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
const askForCommand = async () => {
|
|
166
|
+
clearLine();
|
|
167
|
+
|
|
168
|
+
const prompt = createPrompt((config, done) => {
|
|
169
|
+
const [value, setValue] = useState();
|
|
170
|
+
const [, setStatus] = useState("idle");
|
|
171
|
+
|
|
172
|
+
useKeypress((key, readline) => {
|
|
173
|
+
if (isEnterKey(key)) {
|
|
174
|
+
setStatus("done");
|
|
175
|
+
history.addCommand(value);
|
|
176
|
+
done(value);
|
|
177
|
+
} else if (isUpKey(key)) {
|
|
178
|
+
const v = history.up();
|
|
179
|
+
setValue(v);
|
|
180
|
+
readline.line = v;
|
|
181
|
+
} else if (isDownKey(key)) {
|
|
182
|
+
const v = history.down();
|
|
183
|
+
setValue(v);
|
|
184
|
+
readline.line = v;
|
|
185
|
+
} else {
|
|
186
|
+
setValue(readline.line);
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
return `${config[0].theme.prefix} ${config[0].message ?? ""} ${value ?? ""}`;
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
const command = await prompt(
|
|
194
|
+
[
|
|
195
|
+
{
|
|
196
|
+
type: "input",
|
|
197
|
+
name: "command",
|
|
198
|
+
message: "",
|
|
199
|
+
theme: {
|
|
200
|
+
prefix: " 🛩",
|
|
201
|
+
},
|
|
202
|
+
},
|
|
203
|
+
],
|
|
204
|
+
{ clearPromptOnDone: true },
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
if (command.toLowerCase() === "exit") {
|
|
208
|
+
sayBye();
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
await processCommand(command);
|
|
213
|
+
|
|
214
|
+
await askForCommand();
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
program.action(async (/** @type {{ port: number | undefined }} */ options) => {
|
|
218
|
+
initAPI({ port: options.port ?? 8086 });
|
|
219
|
+
|
|
220
|
+
hideCursor();
|
|
221
|
+
clearLine();
|
|
222
|
+
sayHello();
|
|
223
|
+
await sleep(1000);
|
|
224
|
+
showCursor();
|
|
225
|
+
|
|
226
|
+
await askForCommand();
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
program.parse(process.argv);
|
|
230
|
+
|
|
231
|
+
process.on("uncaughtException", (error) => {
|
|
232
|
+
if (error instanceof Error) {
|
|
233
|
+
if (error.name === "ExitPromptError") {
|
|
234
|
+
clearLine();
|
|
235
|
+
sayBye();
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
throw error;
|
|
240
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "xp-command",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Quick cockpit commands for X-Plane 12 - set your radios, altimeter, autopilot, and more from the terminal while flying.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"xp-command": "./bin/index.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"start": "node bin/index.js",
|
|
11
|
+
"start:clean": "npm run clean && npm start",
|
|
12
|
+
"clean": "rm -rf ~/.xp-command/"
|
|
13
|
+
},
|
|
14
|
+
"devDependencies": {
|
|
15
|
+
"@types/js-yaml": "^4.0.9",
|
|
16
|
+
"@types/node": "^24.9.2",
|
|
17
|
+
"eslint": "^9.39.0",
|
|
18
|
+
"eslint-config-prettier": "^10.1.8",
|
|
19
|
+
"eslint-plugin-prettier": "^5.5.4",
|
|
20
|
+
"eslint-plugin-simple-import-sort": "^12.1.1",
|
|
21
|
+
"globals": "^16.5.0",
|
|
22
|
+
"prettier": "^3.6.2",
|
|
23
|
+
"typescript": "^5.9.3",
|
|
24
|
+
"typescript-eslint": "^8.46.2"
|
|
25
|
+
},
|
|
26
|
+
"keywords": [
|
|
27
|
+
"X-Plane"
|
|
28
|
+
],
|
|
29
|
+
"author": "Boris Diakur (https://borisdiakur.de)",
|
|
30
|
+
"repository": "borisdiakur/xp-command",
|
|
31
|
+
"license": "MIT",
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"@inquirer/core": "^10.3.0",
|
|
34
|
+
"chalk": "^5.6.2",
|
|
35
|
+
"commander": "^14.0.2",
|
|
36
|
+
"conf": "^15.0.2",
|
|
37
|
+
"inquirer": "^12.10.0",
|
|
38
|
+
"js-yaml": "^4.1.0",
|
|
39
|
+
"ora": "^9.0.0"
|
|
40
|
+
},
|
|
41
|
+
"engines": {
|
|
42
|
+
"node": ">=22"
|
|
43
|
+
}
|
|
44
|
+
}
|
package/src/api.js
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/** @type {number} */
|
|
2
|
+
let port;
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @typedef {{
|
|
6
|
+
* error_code: string
|
|
7
|
+
* error_message: string
|
|
8
|
+
* }} APIError
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @typedef {{
|
|
13
|
+
* port: number
|
|
14
|
+
* }} APIOptions
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
class CustomError extends Error {
|
|
18
|
+
constructor(/** @type {string} */ message) {
|
|
19
|
+
super(message);
|
|
20
|
+
this.name = "APIError";
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @param {string} datarefString
|
|
26
|
+
* @return {[string, number|null]}
|
|
27
|
+
*/
|
|
28
|
+
const parseDataref = (datarefString) => {
|
|
29
|
+
// Regex pattern: captures base name and optional array index
|
|
30
|
+
const pattern = /^(.+?)(?:\[(\d+)\])?$/;
|
|
31
|
+
const match = datarefString.match(pattern);
|
|
32
|
+
|
|
33
|
+
if (!match) {
|
|
34
|
+
throw new CustomError("invalid_dataref");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return [match[1], match[2] ? parseInt(match[2]) : null];
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* @param {string} datarefName
|
|
42
|
+
* @return { Promise<number | null> }
|
|
43
|
+
*/
|
|
44
|
+
const getDatarefId = async (datarefName) => {
|
|
45
|
+
const url = new URL(`http://localhost:${port}/api/v2/datarefs`);
|
|
46
|
+
url.searchParams.set("filter[name]", datarefName);
|
|
47
|
+
url.searchParams.set("fields", "id");
|
|
48
|
+
|
|
49
|
+
const response = await fetch(url);
|
|
50
|
+
const json = /** @type { {data:[{id: number }]} | APIError } */ (
|
|
51
|
+
await response.json()
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
if ("error_code" in json) {
|
|
55
|
+
throw new CustomError(json.error_code);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return json.data[0]?.id ?? null;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* @param {APIOptions} options
|
|
63
|
+
*/
|
|
64
|
+
export const initAPI = (options) => {
|
|
65
|
+
port = options.port;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* @param {string} datarefNameWithOptionalIndex
|
|
70
|
+
* @return {Promise<number|Array<number>|string>}
|
|
71
|
+
*/
|
|
72
|
+
export const getDatarefValue = async (datarefNameWithOptionalIndex) => {
|
|
73
|
+
const [datarefName, index] = parseDataref(datarefNameWithOptionalIndex);
|
|
74
|
+
|
|
75
|
+
const datarefId = await getDatarefId(datarefName);
|
|
76
|
+
|
|
77
|
+
const url = new URL(
|
|
78
|
+
`http://localhost:${port}/api/v2/datarefs/${datarefId}/value`,
|
|
79
|
+
);
|
|
80
|
+
if (typeof index === "number") {
|
|
81
|
+
url.searchParams.set("index", String(index));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const response = await fetch(url);
|
|
85
|
+
const json =
|
|
86
|
+
/** @type { {data: number|Array<number>|string } | APIError } */ (
|
|
87
|
+
await response.json()
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
if ("error_code" in json) {
|
|
91
|
+
throw new CustomError(json.error_code);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (typeof json.data === "string") {
|
|
95
|
+
const decoded = Buffer.from(json.data, "base64")
|
|
96
|
+
.toString("utf-8")
|
|
97
|
+
.replaceAll("\x00", "")
|
|
98
|
+
.trim();
|
|
99
|
+
return decoded;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (Array.isArray(json.data) && json.data.length === 1) {
|
|
103
|
+
return JSON.stringify(json.data[0]);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return JSON.stringify(json.data);
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* @param {string} datarefNameWithOptionalIndex
|
|
111
|
+
* @param {number|Array<number>} [value]
|
|
112
|
+
*/
|
|
113
|
+
export const setDatarefValue = async (datarefNameWithOptionalIndex, value) => {
|
|
114
|
+
const [datarefName, index] = parseDataref(datarefNameWithOptionalIndex);
|
|
115
|
+
|
|
116
|
+
const datarefId = await getDatarefId(datarefName);
|
|
117
|
+
|
|
118
|
+
const url = new URL(
|
|
119
|
+
`http://localhost:${port}/api/v2/datarefs/${datarefId}/value`,
|
|
120
|
+
);
|
|
121
|
+
if (typeof index === "number") {
|
|
122
|
+
url.searchParams.set("index", String(index));
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const response = await fetch(url, {
|
|
126
|
+
method: "PATCH",
|
|
127
|
+
body: JSON.stringify({ data: value }),
|
|
128
|
+
});
|
|
129
|
+
const json = /** @type { {data: number|Array<number> } | APIError } */ (
|
|
130
|
+
await response.json()
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
if (json && "error_code" in json) {
|
|
134
|
+
throw new CustomError(json.error_code);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return json;
|
|
138
|
+
};
|
package/src/clipboard.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { exec } from "child_process";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @param {string|number} text
|
|
5
|
+
*/
|
|
6
|
+
export const copyToClipboard = async (text) => {
|
|
7
|
+
const command =
|
|
8
|
+
process.platform === "win32"
|
|
9
|
+
? `echo ${text} | clip`
|
|
10
|
+
: process.platform === "darwin"
|
|
11
|
+
? `echo "${text}" | pbcopy`
|
|
12
|
+
: `echo "${text}" | xclip -selection clipboard`;
|
|
13
|
+
|
|
14
|
+
await new Promise((resolve) => {
|
|
15
|
+
exec(command, () => {
|
|
16
|
+
resolve();
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
};
|
package/src/config.js
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { copyFile, mkdir, readFile } from "node:fs/promises";
|
|
2
|
+
|
|
3
|
+
import yaml from "js-yaml";
|
|
4
|
+
import { homedir } from "os";
|
|
5
|
+
import { dirname } from "path";
|
|
6
|
+
import { join } from "path";
|
|
7
|
+
import { fileURLToPath } from "url";
|
|
8
|
+
|
|
9
|
+
import { getDatarefValue } from "./api.js";
|
|
10
|
+
|
|
11
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
12
|
+
const __dirname = dirname(__filename);
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @typedef {Object} CommandConfig
|
|
16
|
+
* @property {Command[]} commands - Array of command definitions
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @typedef {Object} Command
|
|
21
|
+
* @property {RegExp} pattern - Regular expression pattern to match commands
|
|
22
|
+
* @property {'get' | 'set'} type - Operation type
|
|
23
|
+
* @property {string} dataref - X-Plane dataref path
|
|
24
|
+
* @property {Transform[]} transform - Array of transformation operations to apply
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* @typedef {'mult100' | 'round' | 'div100' | 'toFixed2'} Transform
|
|
29
|
+
* Transform operations:
|
|
30
|
+
* - mult100: Multiply by 100
|
|
31
|
+
* - round: Round to nearest integer
|
|
32
|
+
* - div100: Divide by 100
|
|
33
|
+
* - toFixed2: Format to 2 decimal places
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
const osHomedir = homedir();
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* @return {Promise<CommandConfig>}
|
|
40
|
+
*/
|
|
41
|
+
export const getConfig = async () => {
|
|
42
|
+
await mkdir(join(osHomedir, ".xp-command"), {
|
|
43
|
+
recursive: true,
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
/** @type {string} */
|
|
47
|
+
let aircraft;
|
|
48
|
+
aircraft = /** @type {string} */ (
|
|
49
|
+
await getDatarefValue("sim/aircraft/view/acf_ui_name")
|
|
50
|
+
);
|
|
51
|
+
const aircraftConfigPath = join(osHomedir, ".xp-command", `${aircraft}.yml`);
|
|
52
|
+
|
|
53
|
+
let config;
|
|
54
|
+
try {
|
|
55
|
+
const file = await readFile(aircraftConfigPath, "utf8");
|
|
56
|
+
config = yaml.load(file);
|
|
57
|
+
} catch (error) {
|
|
58
|
+
if (error.code === "ENOENT") {
|
|
59
|
+
await copyFile(join(__dirname, "config.yml"), aircraftConfigPath);
|
|
60
|
+
} else {
|
|
61
|
+
throw error;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const file = await readFile(aircraftConfigPath, "utf8");
|
|
65
|
+
config = yaml.load(file);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return /** @type {CommandConfig} */ (config);
|
|
69
|
+
};
|
package/src/config.yml
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
commands:
|
|
2
|
+
# get barometric pressure at sealevel in inHg (dddd format)
|
|
3
|
+
- pattern: "^l$"
|
|
4
|
+
type: get
|
|
5
|
+
dataref: sim/weather/barometer_sealevel_inhg
|
|
6
|
+
transform:
|
|
7
|
+
- mult100
|
|
8
|
+
- round
|
|
9
|
+
|
|
10
|
+
# get barometric pressure at sealevel in hPa/mb (dddd format)
|
|
11
|
+
- pattern: "^q$"
|
|
12
|
+
type: get
|
|
13
|
+
dataref: sim/weather/barometer_sealevel_inhg
|
|
14
|
+
transform:
|
|
15
|
+
- mult33.8637526
|
|
16
|
+
- round
|
|
17
|
+
|
|
18
|
+
# set barometric pressure at sealevel in inHg (dddd format)
|
|
19
|
+
- pattern: "^l(\\d{4})$"
|
|
20
|
+
type: set
|
|
21
|
+
dataref: sim/cockpit/misc/barometer_setting
|
|
22
|
+
transform:
|
|
23
|
+
- mult0.01
|
|
24
|
+
- toFixed2
|
|
25
|
+
|
|
26
|
+
# set barometric pressure at sealevel in hPa/mb (dddd format)
|
|
27
|
+
- pattern: "^q(\\d{4})$"
|
|
28
|
+
type: set
|
|
29
|
+
dataref: sim/cockpit/misc/barometer_setting
|
|
30
|
+
transform:
|
|
31
|
+
- mult0.02953009998
|
|
32
|
+
- toFixed2
|
|
33
|
+
|
|
34
|
+
# set heading
|
|
35
|
+
- pattern: "^h(\\d{3})$"
|
|
36
|
+
type: set
|
|
37
|
+
dataref: sim/cockpit/autopilot/heading_mag
|
|
38
|
+
|
|
39
|
+
# set transponder code
|
|
40
|
+
- pattern: "^x(\\d{4})$"
|
|
41
|
+
type: set
|
|
42
|
+
dataref: sim/cockpit/radios/transponder_code
|
|
43
|
+
|
|
44
|
+
# set autopilot airspeed in KIAS
|
|
45
|
+
- pattern: "^s(\\d{3})$"
|
|
46
|
+
type: set
|
|
47
|
+
dataref: sim/cockpit/autopilot/airspeed
|
|
48
|
+
|
|
49
|
+
# set autopilot airspeed in MACH
|
|
50
|
+
- pattern: "^s(\\d{2})$"
|
|
51
|
+
type: set
|
|
52
|
+
dataref: sim/cockpit/autopilot/airspeed
|
|
53
|
+
transform:
|
|
54
|
+
- mult0.01
|
|
55
|
+
- toFixed2
|
|
56
|
+
|
|
57
|
+
# set autopilot vertical speed
|
|
58
|
+
- pattern: "^v(\\d\\d?\\d?\\d?)$"
|
|
59
|
+
type: set
|
|
60
|
+
dataref: sim/cockpit/autopilot/vertical_velocity
|
|
61
|
+
|
|
62
|
+
# set autopilot target altitude
|
|
63
|
+
- pattern: "^a((0|\\d\\d\\d\\d\\d?))$"
|
|
64
|
+
type: set
|
|
65
|
+
dataref: sim/cockpit/autopilot/altitude
|
|
66
|
+
|
|
67
|
+
# set autopilot target flight level
|
|
68
|
+
- pattern: "^a(\\d\\d\\d?)$"
|
|
69
|
+
type: set
|
|
70
|
+
dataref: sim/cockpit/autopilot/altitude
|
|
71
|
+
transform:
|
|
72
|
+
- mult10
|
|
73
|
+
- round
|
|
74
|
+
|
|
75
|
+
# set com1 frequency
|
|
76
|
+
- pattern: "^c1(\\d{6})$"
|
|
77
|
+
type: set
|
|
78
|
+
dataref: sim/cockpit2/radios/actuators/com1_frequency_hz_833
|
|
79
|
+
- pattern: "^c1(\\d{5})$"
|
|
80
|
+
type: set
|
|
81
|
+
dataref: sim/cockpit2/radios/actuators/com1_frequency_hz_833
|
|
82
|
+
transform:
|
|
83
|
+
- mult10
|
|
84
|
+
- round
|
|
85
|
+
|
|
86
|
+
# set com1 standby frequency
|
|
87
|
+
- pattern: "^cs1(\\d{6})$"
|
|
88
|
+
type: set
|
|
89
|
+
dataref: sim/cockpit2/radios/actuators/com1_standby_frequency_hz_833
|
|
90
|
+
- pattern: "^cs1(\\d{5})$"
|
|
91
|
+
type: set
|
|
92
|
+
dataref: sim/cockpit2/radios/actuators/com1_standby_frequency_hz_833
|
|
93
|
+
transform:
|
|
94
|
+
- mult10
|
|
95
|
+
- round
|
|
96
|
+
|
|
97
|
+
# set com2 frequency
|
|
98
|
+
- pattern: "^c2(\\d{6})$"
|
|
99
|
+
type: set
|
|
100
|
+
dataref: sim/cockpit2/radios/actuators/com2_frequency_hz_833
|
|
101
|
+
- pattern: "^c2(\\d{5})$"
|
|
102
|
+
type: set
|
|
103
|
+
dataref: sim/cockpit2/radios/actuators/com2_frequency_hz_833
|
|
104
|
+
transform:
|
|
105
|
+
- mult10
|
|
106
|
+
- round
|
|
107
|
+
|
|
108
|
+
# set com2 standby frequency
|
|
109
|
+
- pattern: "^cs2(\\d{6})$"
|
|
110
|
+
type: set
|
|
111
|
+
dataref: sim/cockpit2/radios/actuators/com2_standby_frequency_hz_833
|
|
112
|
+
- pattern: "^cs2(\\d{5})$"
|
|
113
|
+
type: set
|
|
114
|
+
dataref: sim/cockpit2/radios/actuators/com2_standby_frequency_hz_833
|
|
115
|
+
transform:
|
|
116
|
+
- mult10
|
|
117
|
+
- round
|
|
118
|
+
|
|
119
|
+
# set nav1 frequency
|
|
120
|
+
- pattern: "^n1(\\d{5})$"
|
|
121
|
+
type: set
|
|
122
|
+
dataref: sim/cockpit/radios/nav1_freq_hz
|
|
123
|
+
|
|
124
|
+
# set nav1 standby frequency
|
|
125
|
+
- pattern: "^ns1(\\d{5})$"
|
|
126
|
+
type: set
|
|
127
|
+
dataref: sim/cockpit/radios/nav1_stdby_freq_hz
|
|
128
|
+
|
|
129
|
+
# set nav2 frequency
|
|
130
|
+
- pattern: "^n2(\\d{5})$"
|
|
131
|
+
type: set
|
|
132
|
+
dataref: sim/cockpit/radios/nav2_freq_hz
|
|
133
|
+
|
|
134
|
+
# set nav2 standby frequency
|
|
135
|
+
- pattern: "^ns2(\\d{5})$"
|
|
136
|
+
type: set
|
|
137
|
+
dataref: sim/cockpit/radios/nav2_stdby_freq_hz
|
package/src/console.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export const clearLine = (dy = -1) => {
|
|
2
|
+
process.stdout.moveCursor(0, dy);
|
|
3
|
+
process.stdout.clearLine(0);
|
|
4
|
+
process.stdout.cursorTo(0);
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
export const hideCursor = () => {
|
|
8
|
+
process.stdout.write("\x1B[?25l");
|
|
9
|
+
};
|
|
10
|
+
export const showCursor = () => {
|
|
11
|
+
process.stdout.write("\x1B[?25h");
|
|
12
|
+
};
|
package/src/error.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @param {unknown} error
|
|
3
|
+
*/
|
|
4
|
+
export const isEconnRefused = (error) => {
|
|
5
|
+
return Boolean(
|
|
6
|
+
error instanceof Error &&
|
|
7
|
+
error.cause &&
|
|
8
|
+
typeof error.cause === "object" &&
|
|
9
|
+
"code" in error.cause &&
|
|
10
|
+
error.cause.code === "ECONNREFUSED",
|
|
11
|
+
);
|
|
12
|
+
};
|
package/src/history.js
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import Conf from "conf";
|
|
2
|
+
|
|
3
|
+
class CliHistory {
|
|
4
|
+
/** @type {Conf} */
|
|
5
|
+
config;
|
|
6
|
+
|
|
7
|
+
/** @type {number} */
|
|
8
|
+
index;
|
|
9
|
+
|
|
10
|
+
constructor() {
|
|
11
|
+
this.config = new Conf({
|
|
12
|
+
projectName: "xp-command",
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
this.index = this.getHistory().length - 1;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @param {string} command
|
|
20
|
+
* @returns {void}
|
|
21
|
+
*/
|
|
22
|
+
addCommand(command) {
|
|
23
|
+
const history = this.getHistory();
|
|
24
|
+
|
|
25
|
+
const filtered = history.filter((cmd) => cmd !== command);
|
|
26
|
+
filtered.push(command);
|
|
27
|
+
const trimmed = filtered.slice(-100);
|
|
28
|
+
|
|
29
|
+
this.config.set("history", trimmed);
|
|
30
|
+
this.index = trimmed.length - 1;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
getHistory() {
|
|
34
|
+
return /** @type {Array<string>} */ (this.config.get("history") ?? []);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
clear() {
|
|
38
|
+
this.config.set("history", []);
|
|
39
|
+
this.index = 0;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
up() {
|
|
43
|
+
this.index = Math.max(0, this.index - 1);
|
|
44
|
+
return this.getHistory()[this.index + 1] ?? "";
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
down() {
|
|
48
|
+
this.index = Math.min(this.getHistory().length - 1, this.index + 1);
|
|
49
|
+
return this.getHistory()[this.index + 1] ?? "";
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export default new CliHistory();
|