tabby-rdp 0.5.0 → 0.5.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/CHANGELOG.md +490 -0
- package/README.md +177 -48
- package/dist/accountForm.js +0 -1
- package/dist/accounts.js +0 -1
- package/dist/audio.js +0 -1
- package/dist/authorities.js +331 -0
- package/dist/byteQueue.js +102 -0
- package/dist/clipboard.js +106 -0
- package/dist/connectionStatus.js +0 -1
- package/dist/desk.js +16 -47
- package/dist/deskScript.js +196 -6
- package/dist/desktop.service.js +1442 -244
- package/dist/desktopForm.js +51 -17
- package/dist/desktops.js +232 -24
- package/dist/drives.js +271 -30
- package/dist/fileTransfer.js +958 -83
- package/dist/gateway.js +216 -57
- package/dist/header.js +76 -5
- package/dist/help.js +34 -13
- package/dist/hyperv.js +65 -15
- package/dist/index.js +2 -2
- package/dist/keyboard.js +0 -1
- package/dist/microphone.js +57 -13
- package/dist/ntlm.js +0 -1
- package/dist/osd.js +56 -15
- package/dist/probe.js +15 -3
- package/dist/rdcleanpath.js +319 -44
- package/dist/rdpFile.js +22 -7
- package/dist/rdpProfile.js +39 -21
- package/dist/remoteSetup.js +103 -23
- package/dist/sessionLog.js +53 -0
- package/dist/settingsPage.js +65 -29
- package/dist/signin.js +407 -40
- package/dist/ssh.js +99 -18
- package/dist/sshChannelStream.js +40 -6
- package/dist/targets.js +185 -39
- package/dist/ui.js +88 -20
- package/dist/unshowable.js +41 -0
- package/dist/updates.js +37 -2
- package/dist/vms.js +17 -3
- package/dist/wake.js +15 -8
- package/package.json +4 -3
- package/remote/trd-pty.py +74 -13
- package/vendor/SHA256SUMS +26 -0
- package/vendor/ironrdp_web_bg.wasm +0 -0
- package/dist/accountForm.js.map +0 -1
- package/dist/accounts.js.map +0 -1
- package/dist/audio.js.map +0 -1
- package/dist/connectionStatus.js.map +0 -1
- package/dist/desk.js.map +0 -1
- package/dist/deskScript.js.map +0 -1
- package/dist/desktop.service.js.map +0 -1
- package/dist/desktopForm.js.map +0 -1
- package/dist/desktops.js.map +0 -1
- package/dist/drives.js.map +0 -1
- package/dist/fileTransfer.js.map +0 -1
- package/dist/gateway.js.map +0 -1
- package/dist/header.js.map +0 -1
- package/dist/help.js.map +0 -1
- package/dist/hyperv.js.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/keyboard.js.map +0 -1
- package/dist/microphone.js.map +0 -1
- package/dist/ntlm.js.map +0 -1
- package/dist/osd.js.map +0 -1
- package/dist/probe.js.map +0 -1
- package/dist/rdcleanpath.js.map +0 -1
- package/dist/rdpFile.js.map +0 -1
- package/dist/rdpProfile.js.map +0 -1
- package/dist/remoteSetup.js.map +0 -1
- package/dist/settingsPage.js.map +0 -1
- package/dist/signin.js.map +0 -1
- package/dist/ssh.js.map +0 -1
- package/dist/sshChannelStream.js.map +0 -1
- package/dist/targets.js.map +0 -1
- package/dist/ui.js.map +0 -1
- package/dist/updates.js.map +0 -1
- package/dist/vms.js.map +0 -1
- package/dist/wake.js.map +0 -1
package/dist/help.js
CHANGED
|
@@ -36,12 +36,26 @@ exports.SETTINGS_TAB_ID = 'remote-desktop';
|
|
|
36
36
|
exports.TROUBLESHOOTING = [
|
|
37
37
|
{
|
|
38
38
|
id: 'certificate',
|
|
39
|
-
title: '
|
|
39
|
+
title: 'A certificate this computer can\'t verify, or one that has changed',
|
|
40
40
|
match: /certificate/i,
|
|
41
|
-
body: `
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
41
|
+
body: `A desktop you connect to directly (a remote desktop profile, an imported file) and an RD Gateway connect
|
|
42
|
+
without asking when their certificate is valid for their name by this computer's certificate authorities, as
|
|
43
|
+
an organisation's own desktops and gateways often are. Any other is shown the first time, with why it can't
|
|
44
|
+
be verified (most often: it is the server's own, self-signed) and its fingerprint, before anything of your
|
|
45
|
+
sign-in is sent: check the fingerprint with whoever runs it, some other way than this connection (it is in the
|
|
46
|
+
desktop's log too, which <b>Copy log</b> under Open desktops below copies), then choose <b>Trust the
|
|
47
|
+
certificate</b>. The question also says whom the certificate says it was issued to and by, and when it is
|
|
48
|
+
valid: its own word, which nothing has checked, unless its chain leads to an authority this computer trusts
|
|
49
|
+
(another machine's certificate then shows that machine's name) and the TLS library didn't refuse it for a
|
|
50
|
+
reason of its own (a limit set on the authority, say). An issuer you wouldn't expect there (one
|
|
51
|
+
that inspects your network's connections, say) is worth asking about before you trust it, but the one you
|
|
52
|
+
would expect proves nothing: anyone can make a certificate that names it. Only the fingerprint, checked some
|
|
53
|
+
other way, does. A desktop behind an SSH host remembers its certificate on first use without asking, since
|
|
54
|
+
the way there runs inside SSH. When a remembered certificate differs later, the plugin stops before signing
|
|
55
|
+
in, so nothing of your sign-in has gone to that server; only a certificate an authority vouched for is
|
|
56
|
+
replaced without a question, by another it vouches for too. Reinstalling the machine or renewing its
|
|
57
|
+
certificate changes it: then choose <b>Trust the new certificate</b>. If neither happened, something else
|
|
58
|
+
is answering at that address. A GNOME desktop only ever accepts the certificate the plugin made for it; a
|
|
45
59
|
different one there means another program listens on its port. Remembered certificates are listed above.`,
|
|
46
60
|
},
|
|
47
61
|
{
|
|
@@ -94,15 +108,21 @@ exports.TROUBLESHOOTING = [
|
|
|
94
108
|
body: `Sound and microphone changes apply on the next connection. xrdp needs <code>pipewire-module-xrdp</code> or
|
|
95
109
|
<code>pulseaudio-module-xrdp</code>. The microphone needs the system's permission here (macOS: System Settings ›
|
|
96
110
|
Privacy & Security › Microphone, then restart Tabby). On GNOME, apps record from "Remoteaudio Source"; on
|
|
97
|
-
Windows, the machine must allow audio recording redirection
|
|
111
|
+
Windows, the machine must allow audio recording redirection. A desktop whose microphone was stopped (<b>Stop</b>
|
|
112
|
+
in the microphone's menu in Tabby's header, or <b>Send the microphone</b> unchecked in the desktop's menu) gets
|
|
113
|
+
none, its reconnects included, until <b>Send the microphone</b> is checked again in its menu or that Tabby window is closed.`,
|
|
98
114
|
},
|
|
99
115
|
{
|
|
100
116
|
id: 'nested-ssh',
|
|
101
117
|
title: 'I typed ssh to another machine in a terminal',
|
|
102
118
|
body: `In an SSH tab (or a split pane) where you typed <code>ssh</code> on to another machine, <b>Desktop</b> opens that
|
|
103
|
-
machine's desktop, going through the first one, and <code>desk</code> works there too.
|
|
104
|
-
|
|
105
|
-
|
|
119
|
+
machine's desktop, going through the first one, and <code>desk</code> works there too. Desktop asks which desktop
|
|
120
|
+
you mean, since which <code>ssh</code> runs there is the first machine's to say; pick the other machine's
|
|
121
|
+
"from now on" to have it open at once the next times. Desktops you set up behind a host open in a tab connected
|
|
122
|
+
to that host, not through another one.
|
|
123
|
+
The first machine has to log in to the other by itself for this: with a key there, or agent forwarding, as
|
|
124
|
+
<code>ssh -o BatchMode=yes</code> would. Tabby's <b>Reconnect</b> belongs to the tab: it reconnects the first
|
|
125
|
+
machine and ends the ssh typed in it.
|
|
106
126
|
To have both desktops side by side, open the first machine's desktop in one pane, and ssh on in the other.`,
|
|
107
127
|
},
|
|
108
128
|
{
|
|
@@ -127,13 +147,15 @@ const esc = (s) => String(s ?? '').replace(/[&<>"']/g, c => `&#${c.charCodeAt(0)
|
|
|
127
147
|
exports.esc = esc;
|
|
128
148
|
function hotkeyLabel(binding) {
|
|
129
149
|
const first = Array.isArray(binding) ? binding[0] : null;
|
|
130
|
-
|
|
150
|
+
// Text only: the config may hold anything, and this runs for the settings page and the first-connect tip.
|
|
151
|
+
const strokes = (Array.isArray(first) ? first : [first]).filter((s) => typeof s === 'string');
|
|
131
152
|
if (!strokes.length) {
|
|
132
153
|
return null;
|
|
133
154
|
}
|
|
134
155
|
const mac = process.platform === 'darwin';
|
|
135
|
-
|
|
136
|
-
|
|
156
|
+
// A Map, not an object literal: a part named 'constructor' or 'toString' would find what objects inherit.
|
|
157
|
+
const MAC = new Map([['Shift', '⇧'], ['Alt', '⌥'], ['Ctrl', '⌃'], ['Meta', '⌘'], ['Cmd', '⌘']]);
|
|
158
|
+
return strokes.map(s => s.split('-').map(k => mac ? MAC.get(k) ?? k : k).join(mac ? '' : '+')).join(', ');
|
|
137
159
|
}
|
|
138
160
|
const TIP_STYLE = `
|
|
139
161
|
.trd-tip { position: absolute; left: 50%; bottom: 16px; transform: translateX(-50%); z-index: 2; max-width: min(560px, 90%);
|
|
@@ -260,4 +282,3 @@ exports.RemoteDesktopHelp = RemoteDesktopHelp = __decorate([
|
|
|
260
282
|
(0, core_1.Injectable)({ providedIn: 'root' }),
|
|
261
283
|
__metadata("design:paramtypes", [tabby_core_1.AppService, tabby_core_1.ConfigService, core_1.NgZone])
|
|
262
284
|
], RemoteDesktopHelp);
|
|
263
|
-
//# sourceMappingURL=help.js.map
|
package/dist/hyperv.js
CHANGED
|
@@ -1,16 +1,23 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.HYPERV_PORT = void 0;
|
|
3
|
+
exports.MAX_VMS = exports.HYPERV_PORT = void 0;
|
|
4
4
|
exports.vmId = vmId;
|
|
5
5
|
exports.scanHyperV = scanHyperV;
|
|
6
6
|
exports.hyperVState = hyperVState;
|
|
7
7
|
exports.startHyperV = startHyperV;
|
|
8
8
|
/** VMConnect's port on a Hyper-V host. */
|
|
9
9
|
exports.HYPERV_PORT = 2179;
|
|
10
|
+
/**
|
|
11
|
+
* How many VMs a host's scan lists at most (see vms.ts too). Each is a menu item, and an answer of the host's making
|
|
12
|
+
* could otherwise make thousands of them. A host can have more (a Hyper-V server can run 1,024): the first ones are
|
|
13
|
+
* listed, and the menu says that only so many are (see RemoteDesktopService.vmsCut).
|
|
14
|
+
*/
|
|
15
|
+
exports.MAX_VMS = 256;
|
|
10
16
|
const GUID = /^[0-9a-f]{8}(-[0-9a-f]{4}){3}-[0-9a-f]{12}$/i;
|
|
11
17
|
/** A VM id as Hyper-V prints it, or '' if this isn't one. */
|
|
12
18
|
function vmId(value) {
|
|
13
|
-
|
|
19
|
+
// Text only: String() of an object from the config runs its own conversions, which can throw.
|
|
20
|
+
const id = (typeof value === 'string' ? value : '').trim().replace(/^\{|\}$/g, '');
|
|
14
21
|
return GUID.test(id) ? id.toLowerCase() : '';
|
|
15
22
|
}
|
|
16
23
|
/**
|
|
@@ -27,19 +34,63 @@ const SCAN = String.raw `
|
|
|
27
34
|
$ErrorActionPreference = 'SilentlyContinue'
|
|
28
35
|
if (Get-Command Get-VM) { Get-VM | ForEach-Object { 'TRD_HV ' + $_.Id + '|' + $_.State + '|' + $_.Name } }
|
|
29
36
|
`;
|
|
30
|
-
/**
|
|
31
|
-
|
|
37
|
+
/**
|
|
38
|
+
* A line of the scan's answer longer than this is none of its own: its id and state take some 50 characters, and
|
|
39
|
+
* Hyper-V keeps a VM's name to 100.
|
|
40
|
+
*/
|
|
41
|
+
const MAX_LINE = 1024;
|
|
42
|
+
/**
|
|
43
|
+
* A line of the scan's answer, `TRD_HV <id>|<state>|<name>` (the name may hold bars), as a VM; null for any other line.
|
|
44
|
+
* Read by where its bars are, not with a pattern whose parts can take the same characters: a long line of the host's
|
|
45
|
+
* making could make that cost the square of its length.
|
|
46
|
+
*/
|
|
47
|
+
function scannedVM(raw) {
|
|
48
|
+
const line = raw.length > MAX_LINE ? '' : raw.trim();
|
|
49
|
+
const first = line.indexOf('|');
|
|
50
|
+
const second = first < 0 ? -1 : line.indexOf('|', first + 1);
|
|
51
|
+
if (!line.startsWith('TRD_HV ') || second < 0) {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
const id = vmId(line.slice('TRD_HV '.length, first));
|
|
55
|
+
const name = line.slice(second + 1);
|
|
56
|
+
// A name on one line, as PowerShell prints it.
|
|
57
|
+
if (!id || !name || /[\r\u2028\u2029]/.test(name)) {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
return { id, name, state: line.slice(first + 1, second) === 'Running' ? 'running' : 'off' };
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* The host's Hyper-V VMs, running or not (one that is off is started when opened), MAX_VMS at most; `listed.cut` says
|
|
64
|
+
* whether the host has more. Read-only.
|
|
65
|
+
*/
|
|
66
|
+
async function scanHyperV(target, listed = {}) {
|
|
32
67
|
const out = await powershell(target, SCAN);
|
|
33
68
|
const found = [];
|
|
69
|
+
listed.cut = false;
|
|
70
|
+
// Ids seen so far: looked up, not searched for, so that the time taken grows with the lines, not their square.
|
|
71
|
+
const ids = new Set();
|
|
34
72
|
for (const line of out.split('\n')) {
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
found.
|
|
73
|
+
const vm = scannedVM(line);
|
|
74
|
+
if (vm && !ids.has(vm.id)) {
|
|
75
|
+
// One more than are listed: the list is cut short.
|
|
76
|
+
if (found.length === exports.MAX_VMS) {
|
|
77
|
+
listed.cut = true;
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
80
|
+
ids.add(vm.id);
|
|
81
|
+
found.push(vm);
|
|
39
82
|
}
|
|
40
83
|
}
|
|
41
84
|
return found;
|
|
42
85
|
}
|
|
86
|
+
/**
|
|
87
|
+
* What follows `prefix` on each line of the host's answer that starts with it, trimmed, and MAX_LINE characters of it
|
|
88
|
+
* at most (the host's reason for an error is shown as it is). Line by line: a pattern run over the whole answer can
|
|
89
|
+
* match across its lines, which a long answer of the host's making can make cost the square of its length.
|
|
90
|
+
*/
|
|
91
|
+
function linesAfter(out, prefix) {
|
|
92
|
+
return out.split('\n').map(line => line.trim()).filter(line => line.startsWith(prefix)).map(line => line.slice(prefix.length).trim().slice(0, MAX_LINE));
|
|
93
|
+
}
|
|
43
94
|
// TRD_HV_STATE <state>|<enhanced session mode state>, or TRD_HV_ERR <why>. The enhanced state is
|
|
44
95
|
// Msvm_ComputerSystem's: 2 when the guest takes enhanced sessions right now (the host allows them, and the guest's
|
|
45
96
|
// Remote Desktop Services answer on its bus), anything else when only the basic console is there.
|
|
@@ -53,9 +104,9 @@ try {
|
|
|
53
104
|
/** How a VM is now: running or not, and whether it takes an enhanced session. Throws with the host's reason when it can't say. */
|
|
54
105
|
async function hyperVState(target, id) {
|
|
55
106
|
const out = await powershell(target, STATE.replace('@@ID@@', vmId(id)));
|
|
56
|
-
const state =
|
|
107
|
+
const state = linesAfter(out, 'TRD_HV_STATE ').map(line => /^([^|]*)\|(\d*)$/.exec(line)).find(Boolean);
|
|
57
108
|
if (!state) {
|
|
58
|
-
throw new Error(
|
|
109
|
+
throw new Error(linesAfter(out, 'TRD_HV_ERR ')[0] || 'the host didn\'t answer about the VM (is PowerShell\'s Hyper-V module there, and may this account use it?)');
|
|
59
110
|
}
|
|
60
111
|
return { running: state[1] === 'Running', enhanced: state[2] === '2' };
|
|
61
112
|
}
|
|
@@ -70,10 +121,9 @@ try {
|
|
|
70
121
|
/** Starts (or resumes) the VM. Resolves with what was done; throws with the host's reason when it couldn't. */
|
|
71
122
|
async function startHyperV(target, id) {
|
|
72
123
|
const out = await powershell(target, START.replace('@@ID@@', vmId(id)));
|
|
73
|
-
const ok =
|
|
74
|
-
if (!ok) {
|
|
75
|
-
throw new Error(
|
|
124
|
+
const ok = linesAfter(out, 'TRD_HV_OK ');
|
|
125
|
+
if (!ok.length) {
|
|
126
|
+
throw new Error(linesAfter(out, 'TRD_HV_ERR ')[0] || 'the host didn\'t answer');
|
|
76
127
|
}
|
|
77
|
-
return ok[
|
|
128
|
+
return ok[0];
|
|
78
129
|
}
|
|
79
|
-
//# sourceMappingURL=hyperv.js.map
|
package/dist/index.js
CHANGED
|
@@ -49,7 +49,8 @@ let RemoteDesktopModule = class RemoteDesktopModule {
|
|
|
49
49
|
'remote-desktop-disconnect': pane => desktop.disconnect(pane),
|
|
50
50
|
};
|
|
51
51
|
hotkeys.hotkey$.subscribe((id) => {
|
|
52
|
-
|
|
52
|
+
// Tabby fires any key bound under `hotkeys` in the config ('constructor' too): the table's own entries only.
|
|
53
|
+
const action = Object.prototype.hasOwnProperty.call(actions, id) ? actions[id] : undefined;
|
|
53
54
|
const pane = action && (0, targets_1.desktopPaneOf)(app.activeTab);
|
|
54
55
|
if (!action || !pane) {
|
|
55
56
|
return;
|
|
@@ -85,4 +86,3 @@ RemoteDesktopModule = __decorate([
|
|
|
85
86
|
__metadata("design:paramtypes", [tabby_core_1.AppService, tabby_core_1.HotkeysService, desktop_service_1.RemoteDesktopService, header_1.HeaderControls, targets_1.RemoteTargets, keyboard_1.DesktopKeyboard, core_1.Injector])
|
|
86
87
|
], RemoteDesktopModule);
|
|
87
88
|
exports.default = RemoteDesktopModule;
|
|
88
|
-
//# sourceMappingURL=index.js.map
|
package/dist/keyboard.js
CHANGED
package/dist/microphone.js
CHANGED
|
@@ -2,14 +2,16 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.Resampler = exports.Microphone = void 0;
|
|
4
4
|
/**
|
|
5
|
-
* Redirects the local microphone to a remote desktop (IronRDP's `audioInput` extension). The server asks for it
|
|
6
|
-
*
|
|
7
|
-
* the microphone is captured only in between. The browser's capture runs at the audio device's
|
|
8
|
-
* and converted to interleaved 16-bit PCM here, in blocks of about 20 ms.
|
|
5
|
+
* Redirects the local microphone to a remote desktop (IronRDP's `audioInput` extension). The server asks for it when it
|
|
6
|
+
* likes (by its own account, while an application there records): IronRDP says `open` (with the format the server
|
|
7
|
+
* chose) and `close`, and the microphone is captured only in between. The browser's capture runs at the audio device's
|
|
8
|
+
* rate; it is resampled and converted to interleaved 16-bit PCM here, in blocks of about 20 ms.
|
|
9
9
|
*
|
|
10
10
|
* It keeps capturing while the desktop layer is hidden: the remote application is still recording (a call, say), and
|
|
11
|
-
* the console being in front doesn't mean the user stopped talking to it.
|
|
12
|
-
*
|
|
11
|
+
* the console being in front doesn't mean the user stopped talking to it. What the remote does with it is out of
|
|
12
|
+
* sight then, so it shows elsewhere (see RemoteDesktopService.microphoneChanged): a red microphone in Tabby's header
|
|
13
|
+
* for as long as it lasts, and a note when a capture starts on a desktop that isn't showing. turnOff() stops it here,
|
|
14
|
+
* and turnOn() takes the remote's requests again.
|
|
13
15
|
*/
|
|
14
16
|
class Microphone {
|
|
15
17
|
/**
|
|
@@ -37,6 +39,8 @@ class Microphone {
|
|
|
37
39
|
this.starting = false;
|
|
38
40
|
this.resampler = null;
|
|
39
41
|
this.reportedError = false;
|
|
42
|
+
/** Turned off here (see turnOff): the remote's requests are refused for the rest of the connection. */
|
|
43
|
+
this.off = false;
|
|
40
44
|
/** The callback for IronRDP's audioInput extension. */
|
|
41
45
|
this.callback = (message) => {
|
|
42
46
|
try {
|
|
@@ -49,6 +53,10 @@ class Microphone {
|
|
|
49
53
|
}
|
|
50
54
|
this.format = { sampleRate, channels };
|
|
51
55
|
this.log(`microphone: the remote opened it (${sampleRate} Hz, ${channels === 1 ? 'mono' : 'stereo'})`);
|
|
56
|
+
if (this.off) {
|
|
57
|
+
this.log('microphone: not sent: turned off here');
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
52
60
|
this.start().catch(e => this.failed(e));
|
|
53
61
|
}
|
|
54
62
|
else if (message?.type === 'close') {
|
|
@@ -74,8 +82,8 @@ class Microphone {
|
|
|
74
82
|
finally {
|
|
75
83
|
this.starting = false;
|
|
76
84
|
}
|
|
77
|
-
// Closed and opened again while starting: that start was dropped, so start over.
|
|
78
|
-
if (!this.stream && this.format) {
|
|
85
|
+
// Closed and opened again while starting: that start was dropped, so start over (unless turned off meanwhile).
|
|
86
|
+
if (!this.stream && this.format && !this.off) {
|
|
79
87
|
await this.start();
|
|
80
88
|
}
|
|
81
89
|
}
|
|
@@ -100,28 +108,42 @@ class Microphone {
|
|
|
100
108
|
let capture;
|
|
101
109
|
try {
|
|
102
110
|
await context.audioWorklet.addModule(workletURL());
|
|
111
|
+
if (generation !== this.generation) {
|
|
112
|
+
return; // closed meanwhile: stop() released the stream and closed the context, which takes no new nodes
|
|
113
|
+
}
|
|
103
114
|
const node = new AudioWorkletNode(context, 'trd-microphone', { numberOfInputs: 1, numberOfOutputs: 1, outputChannelCount: [1] });
|
|
104
|
-
node.port.onmessage = event =>
|
|
115
|
+
node.port.onmessage = event => {
|
|
116
|
+
// A block the worklet posted before the capture stopped can still arrive after it: dropped, not sent.
|
|
117
|
+
if (generation === this.generation) {
|
|
118
|
+
this.captured(event.data, context.sampleRate);
|
|
119
|
+
}
|
|
120
|
+
};
|
|
105
121
|
capture = node;
|
|
106
122
|
}
|
|
107
123
|
catch (e) {
|
|
124
|
+
if (generation !== this.generation) {
|
|
125
|
+
return; // closed meanwhile, which may be why the module was refused: as above, and not an error
|
|
126
|
+
}
|
|
108
127
|
// No AudioWorklet (or its module was refused): the deprecated ScriptProcessor does the same on the main thread.
|
|
109
128
|
this.log(`microphone: AudioWorklet unavailable (${e?.message ?? e}), using ScriptProcessor`);
|
|
110
129
|
const node = context.createScriptProcessor(1024, 2, 1);
|
|
111
130
|
node.onaudioprocess = event => {
|
|
131
|
+
if (generation !== this.generation) {
|
|
132
|
+
return; // stopped: as above
|
|
133
|
+
}
|
|
112
134
|
const input = event.inputBuffer;
|
|
113
135
|
this.captured(Array.from({ length: input.numberOfChannels }, (_, i) => input.getChannelData(i).slice()), context.sampleRate);
|
|
114
136
|
};
|
|
115
137
|
capture = node;
|
|
116
138
|
}
|
|
117
|
-
if (generation !== this.generation) {
|
|
118
|
-
return; // closed meanwhile: stop() released the stream and the context
|
|
119
|
-
}
|
|
120
139
|
source.connect(capture);
|
|
121
140
|
capture.connect(mute);
|
|
122
141
|
this.nodes = [source, capture, mute];
|
|
123
142
|
if (context.state === 'suspended') {
|
|
124
143
|
await context.resume().catch(() => null);
|
|
144
|
+
if (generation !== this.generation) {
|
|
145
|
+
return; // closed or turned off while resuming: stop() released everything, and it isn't capturing
|
|
146
|
+
}
|
|
125
147
|
}
|
|
126
148
|
const track = stream.getAudioTracks()[0];
|
|
127
149
|
this.log(`microphone: capturing from ${track?.label || 'the default input'} at ${context.sampleRate} Hz`);
|
|
@@ -188,6 +210,29 @@ class Microphone {
|
|
|
188
210
|
this.changed();
|
|
189
211
|
}
|
|
190
212
|
}
|
|
213
|
+
/**
|
|
214
|
+
* Turned off here (the setting, or Stop for this desktop): stops capturing at once, also a capture still starting,
|
|
215
|
+
* and refuses the remote's later requests on this connection. Whether the next connection gets the microphone is
|
|
216
|
+
* the service's to say (see RemoteDesktopService.stopMicrophone).
|
|
217
|
+
*/
|
|
218
|
+
turnOff() {
|
|
219
|
+
if (!this.off) {
|
|
220
|
+
this.off = true;
|
|
221
|
+
this.log('microphone: turned off here');
|
|
222
|
+
}
|
|
223
|
+
this.stop();
|
|
224
|
+
}
|
|
225
|
+
/** Turned on again here (the desktop's menu): the remote's requests are taken again, one open now included. */
|
|
226
|
+
turnOn() {
|
|
227
|
+
if (!this.off) {
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
this.off = false;
|
|
231
|
+
this.log('microphone: turned on again here');
|
|
232
|
+
if (this.format) {
|
|
233
|
+
this.start().catch(e => this.failed(e));
|
|
234
|
+
}
|
|
235
|
+
}
|
|
191
236
|
/** The connection ended. */
|
|
192
237
|
close() {
|
|
193
238
|
this.format = null;
|
|
@@ -292,4 +337,3 @@ function workletURL() {
|
|
|
292
337
|
workletBlobURL ??= URL.createObjectURL(new Blob([WORKLET], { type: 'application/javascript' }));
|
|
293
338
|
return workletBlobURL;
|
|
294
339
|
}
|
|
295
|
-
//# sourceMappingURL=microphone.js.map
|
package/dist/ntlm.js
CHANGED
package/dist/osd.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.OSD_STYLE = exports.OSD_DEFAULTS = exports.OSD_POSITIONS = exports.OSD_SIZES = exports.OSD_FONTS = void 0;
|
|
4
|
+
exports.osdColor = osdColor;
|
|
5
|
+
exports.configNumber = configNumber;
|
|
4
6
|
exports.osdSettings = osdSettings;
|
|
5
7
|
exports.renderOsd = renderOsd;
|
|
6
8
|
/** Font stacks: macOS's, then Windows' (Bahnschrift) and common Linux ones, then any sans-serif. */
|
|
@@ -15,16 +17,35 @@ exports.OSD_FONTS = {
|
|
|
15
17
|
exports.OSD_SIZES = { small: 28, medium: 44, large: 60, huge: 80 };
|
|
16
18
|
exports.OSD_POSITIONS = ['top-left', 'top-center', 'top-right', 'middle', 'bottom-left', 'bottom-center', 'bottom-right'];
|
|
17
19
|
exports.OSD_DEFAULTS = { show: 'auto', font: 'condensed', size: 'medium', position: 'top-right', color: '', seconds: 2.5 };
|
|
20
|
+
/** Whether `key` is one of the table's own entries: 'constructor' or '__proto__' from the config are no font or size. */
|
|
21
|
+
function isOwn(table, key) {
|
|
22
|
+
return typeof key === 'string' && Object.prototype.hasOwnProperty.call(table, key);
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* A color from the config, or '' for the default. Only plain colors: a name, #hex, or a color function of numbers.
|
|
26
|
+
* CSS.supports alone isn't enough: a value with var() passes it whatever follows (`var(--x, red)"><img …>`).
|
|
27
|
+
*/
|
|
28
|
+
function osdColor(value) {
|
|
29
|
+
const plain = typeof value === 'string' && /^(#[0-9a-f]{3,8}|[a-z]+|(rgba?|hsla?|hwb|lab|lch|oklab|oklch|color)\([\w\s.,%/+-]*\))$/i.test(value);
|
|
30
|
+
return plain && CSS.supports('color', value) ? value : '';
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* A number from the config, or NaN: only from a number or a string. Number() of an object calls its own conversions,
|
|
34
|
+
* and a config can hold an object whose `valueOf` and `toString` aren't functions, which makes it throw.
|
|
35
|
+
*/
|
|
36
|
+
function configNumber(value) {
|
|
37
|
+
return typeof value === 'number' || typeof value === 'string' ? Number(value) : NaN;
|
|
38
|
+
}
|
|
18
39
|
/** The config value, with anything missing or unknown replaced by the default. */
|
|
19
40
|
function osdSettings(value) {
|
|
20
41
|
const v = value && typeof value === 'object' ? value : {};
|
|
21
|
-
const seconds =
|
|
42
|
+
const seconds = configNumber(v.seconds);
|
|
22
43
|
return {
|
|
23
44
|
show: ['auto', 'always', 'off'].includes(v.show) ? v.show : exports.OSD_DEFAULTS.show,
|
|
24
|
-
font: v.font
|
|
25
|
-
size: v.size
|
|
45
|
+
font: isOwn(exports.OSD_FONTS, v.font) ? v.font : exports.OSD_DEFAULTS.font,
|
|
46
|
+
size: isOwn(exports.OSD_SIZES, v.size) ? v.size : exports.OSD_DEFAULTS.size,
|
|
26
47
|
position: exports.OSD_POSITIONS.includes(v.position) ? v.position : exports.OSD_DEFAULTS.position,
|
|
27
|
-
color:
|
|
48
|
+
color: osdColor(v.color),
|
|
28
49
|
seconds: Number.isFinite(seconds) && seconds >= 0.5 && seconds <= 30 ? seconds : exports.OSD_DEFAULTS.seconds,
|
|
29
50
|
};
|
|
30
51
|
}
|
|
@@ -57,23 +78,43 @@ function place(position) {
|
|
|
57
78
|
].join('; ');
|
|
58
79
|
return { box, glow, align: h === 'left' ? 'flex-start' : h === 'right' ? 'flex-end' : 'center' };
|
|
59
80
|
}
|
|
60
|
-
/**
|
|
81
|
+
/**
|
|
82
|
+
* Fills `el` (a .trd-osd) with `name` and an optional second line, as the settings say. The settings are checked
|
|
83
|
+
* again, whoever passes them, and the overlay is built node by node with its styles set through the CSSOM: nothing
|
|
84
|
+
* from the config is ever parsed as markup, which in Tabby's window (Node integration) would run as code.
|
|
85
|
+
*/
|
|
61
86
|
function renderOsd(el, name, sub, settings) {
|
|
62
|
-
const
|
|
63
|
-
const
|
|
87
|
+
const checked = osdSettings(settings);
|
|
88
|
+
const font = exports.OSD_FONTS[checked.font];
|
|
89
|
+
const { box, glow, align } = place(checked.position);
|
|
64
90
|
// Smaller in a narrow pane: a name should fit in about two thirds of it.
|
|
65
91
|
const width = el.offsetWidth || 800; // layout width: the settings preview is a scaled-down desktop
|
|
66
|
-
const size = Math.max(18, Math.min(exports.OSD_SIZES[
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
const
|
|
92
|
+
const size = Math.max(18, Math.min(exports.OSD_SIZES[checked.size], width / Math.max(4, name.length) * 1.1));
|
|
93
|
+
const part = (className) => {
|
|
94
|
+
const div = document.createElement('div');
|
|
95
|
+
div.className = className;
|
|
96
|
+
return div;
|
|
97
|
+
};
|
|
98
|
+
const glowEl = part('trd-osd-glow');
|
|
99
|
+
glowEl.style.cssText = glow;
|
|
100
|
+
const boxEl = part('trd-osd-box');
|
|
101
|
+
boxEl.style.cssText = box;
|
|
102
|
+
boxEl.style.setProperty('align-items', align);
|
|
103
|
+
boxEl.style.setProperty('font-family', font.family);
|
|
104
|
+
boxEl.style.setProperty('font-weight', String(font.weight));
|
|
105
|
+
boxEl.style.setProperty('font-stretch', font.stretch);
|
|
106
|
+
boxEl.style.setProperty('font-size', `${size}px`);
|
|
107
|
+
if (checked.color) {
|
|
108
|
+
boxEl.style.setProperty('color', checked.color);
|
|
109
|
+
}
|
|
110
|
+
const nameEl = part('trd-osd-name');
|
|
111
|
+
nameEl.textContent = name;
|
|
112
|
+
const subEl = part('trd-osd-sub');
|
|
73
113
|
if (sub) {
|
|
74
114
|
const bar = document.createElement('span');
|
|
75
115
|
bar.className = 'trd-osd-bar';
|
|
76
116
|
subEl.append(bar, document.createTextNode(sub));
|
|
77
117
|
}
|
|
118
|
+
boxEl.append(nameEl, subEl);
|
|
119
|
+
el.replaceChildren(glowEl, boxEl);
|
|
78
120
|
}
|
|
79
|
-
//# sourceMappingURL=osd.js.map
|
package/dist/probe.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.MAX_PROBES = void 0;
|
|
3
4
|
exports.probeArrived = probeArrived;
|
|
4
5
|
exports.probesIn = probesIn;
|
|
5
6
|
/**
|
|
@@ -8,15 +9,27 @@ exports.probesIn = probesIn;
|
|
|
8
9
|
* in, so nothing shows). Exact where a split's panes share a host, and even their size.
|
|
9
10
|
*/
|
|
10
11
|
const listeners = new Set();
|
|
12
|
+
/**
|
|
13
|
+
* How many probes probesIn keeps at most. A pane gets one for each `ssh` in the foreground of its terminal there (two
|
|
14
|
+
* with a jump host's); the host, which is given the prefix, could print any number more while its command runs (up to
|
|
15
|
+
* a minute), each one kept until then.
|
|
16
|
+
*/
|
|
17
|
+
exports.MAX_PROBES = 64;
|
|
18
|
+
/** What follows a probe's prefix and its dash: the number the script gives each `ssh` it finds, from 1. */
|
|
19
|
+
const PROBE_NUMBER = /^[1-9][0-9]{0,2}$/;
|
|
11
20
|
/** Called by desk.ts for each probe in a pane's output. */
|
|
12
21
|
function probeArrived(id, pane) {
|
|
13
22
|
listeners.forEach(f => f(id, pane));
|
|
14
23
|
}
|
|
15
|
-
/**
|
|
24
|
+
/**
|
|
25
|
+
* Collects the probes that arrive in `pane` while `during` runs, and briefly after: ids of the form `<prefix>-<n>`, n
|
|
26
|
+
* from 1 to 999, the first MAX_PROBES of them.
|
|
27
|
+
*/
|
|
16
28
|
async function probesIn(pane, prefix, during, settleMs = 400) {
|
|
17
29
|
const ids = new Set();
|
|
30
|
+
const start = `${prefix}-`;
|
|
18
31
|
const listener = (id, where) => {
|
|
19
|
-
if (where === pane && id.startsWith(
|
|
32
|
+
if (where === pane && ids.size < exports.MAX_PROBES && id.startsWith(start) && PROBE_NUMBER.test(id.slice(start.length))) {
|
|
20
33
|
ids.add(id);
|
|
21
34
|
}
|
|
22
35
|
};
|
|
@@ -30,4 +43,3 @@ async function probesIn(pane, prefix, during, settleMs = 400) {
|
|
|
30
43
|
listeners.delete(listener);
|
|
31
44
|
}
|
|
32
45
|
}
|
|
33
|
-
//# sourceMappingURL=probe.js.map
|