tabby-rdp 0.5.0 → 0.5.2

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.
Files changed (87) hide show
  1. package/CHANGELOG.md +516 -0
  2. package/README.md +216 -58
  3. package/TROUBLESHOOTING.md +77 -0
  4. package/dist/accountForm.js +0 -1
  5. package/dist/accountRevisions.js +46 -0
  6. package/dist/accounts.js +2 -2
  7. package/dist/audio.js +0 -1
  8. package/dist/authorities.js +331 -0
  9. package/dist/byteQueue.js +102 -0
  10. package/dist/clipboard.js +106 -0
  11. package/dist/connectionStatus.js +0 -1
  12. package/dist/desk.js +16 -47
  13. package/dist/deskScript.js +196 -6
  14. package/dist/desktop.service.js +1499 -269
  15. package/dist/desktopForm.js +53 -18
  16. package/dist/desktops.js +232 -24
  17. package/dist/diskStorage.js +212 -0
  18. package/dist/drives.js +271 -30
  19. package/dist/fileTransfer.js +960 -132
  20. package/dist/gateway.js +216 -57
  21. package/dist/header.js +76 -5
  22. package/dist/help.js +16 -47
  23. package/dist/hostCompat.js +86 -0
  24. package/dist/hyperv.js +65 -15
  25. package/dist/index.js +7 -4
  26. package/dist/keyboard.js +0 -1
  27. package/dist/microphone.js +57 -13
  28. package/dist/ntlm.js +0 -1
  29. package/dist/osd.js +56 -15
  30. package/dist/packageChangeDialog.js +186 -0
  31. package/dist/probe.js +15 -3
  32. package/dist/rdcleanpath.js +319 -44
  33. package/dist/rdpFile.js +22 -7
  34. package/dist/rdpProfile.js +39 -21
  35. package/dist/releaseSettings.js +72 -0
  36. package/dist/releases.js +408 -0
  37. package/dist/remoteSetup.js +103 -23
  38. package/dist/sessionLog.js +53 -0
  39. package/dist/settingsPage.js +148 -122
  40. package/dist/signin.js +478 -48
  41. package/dist/ssh.js +99 -18
  42. package/dist/sshChannelStream.js +40 -6
  43. package/dist/targets.js +194 -40
  44. package/dist/ui.js +88 -20
  45. package/dist/unshowable.js +41 -0
  46. package/dist/updates.js +79 -56
  47. package/dist/vms.js +17 -3
  48. package/dist/wake.js +15 -8
  49. package/docs/desk.md +157 -0
  50. package/package.json +14 -4
  51. package/remote/trd-pty.py +74 -13
  52. package/vendor/SHA256SUMS +26 -0
  53. package/vendor/ironrdp_web_bg.wasm +0 -0
  54. package/dist/accountForm.js.map +0 -1
  55. package/dist/accounts.js.map +0 -1
  56. package/dist/audio.js.map +0 -1
  57. package/dist/connectionStatus.js.map +0 -1
  58. package/dist/desk.js.map +0 -1
  59. package/dist/deskScript.js.map +0 -1
  60. package/dist/desktop.service.js.map +0 -1
  61. package/dist/desktopForm.js.map +0 -1
  62. package/dist/desktops.js.map +0 -1
  63. package/dist/drives.js.map +0 -1
  64. package/dist/fileTransfer.js.map +0 -1
  65. package/dist/gateway.js.map +0 -1
  66. package/dist/header.js.map +0 -1
  67. package/dist/help.js.map +0 -1
  68. package/dist/hyperv.js.map +0 -1
  69. package/dist/index.js.map +0 -1
  70. package/dist/keyboard.js.map +0 -1
  71. package/dist/microphone.js.map +0 -1
  72. package/dist/ntlm.js.map +0 -1
  73. package/dist/osd.js.map +0 -1
  74. package/dist/probe.js.map +0 -1
  75. package/dist/rdcleanpath.js.map +0 -1
  76. package/dist/rdpFile.js.map +0 -1
  77. package/dist/rdpProfile.js.map +0 -1
  78. package/dist/remoteSetup.js.map +0 -1
  79. package/dist/settingsPage.js.map +0 -1
  80. package/dist/signin.js.map +0 -1
  81. package/dist/ssh.js.map +0 -1
  82. package/dist/sshChannelStream.js.map +0 -1
  83. package/dist/targets.js.map +0 -1
  84. package/dist/ui.js.map +0 -1
  85. package/dist/updates.js.map +0 -1
  86. package/dist/vms.js.map +0 -1
  87. package/dist/wake.js.map +0 -1
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 = Number(v.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 in exports.OSD_FONTS ? v.font : exports.OSD_DEFAULTS.font,
25
- size: v.size in exports.OSD_SIZES ? v.size : exports.OSD_DEFAULTS.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: typeof v.color === 'string' && CSS.supports('color', v.color) ? v.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
- /** Fills `el` (a .trd-osd) with `name` and an optional second line, as the settings say. */
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 font = exports.OSD_FONTS[settings.font];
63
- const { box, glow, align } = place(settings.position);
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[settings.size], width / Math.max(4, name.length) * 1.1));
67
- el.innerHTML = `<div class="trd-osd-glow" style="${glow}"></div>
68
- <div class="trd-osd-box" style="${box}; align-items: ${align}; font-family: ${font.family.replace(/"/g, '&quot;')};
69
- font-weight: ${font.weight}; font-stretch: ${font.stretch}; font-size: ${size}px${settings.color ? `; color: ${settings.color}` : ''}">
70
- <div class="trd-osd-name"></div><div class="trd-osd-sub"></div></div>`;
71
- el.querySelector('.trd-osd-name').textContent = name;
72
- const subEl = el.querySelector('.trd-osd-sub');
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
@@ -0,0 +1,186 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PackageChangeDialog = void 0;
4
+ const STYLE = `
5
+ .trd-package-dialog { width: min(460px, calc(100vw - 40px)); padding: 24px; border-radius: 8px; outline: none;
6
+ border: 1px solid rgba(128,128,128,.35); background: var(--bs-body-bg, #1b1b1b); color: var(--bs-body-color, #ddd);
7
+ box-shadow: 0 16px 60px rgba(0,0,0,.45); }
8
+ .trd-package-dialog::backdrop { background: rgba(0,0,0,.55); }
9
+ .trd-package-dialog h4 { font-size: 18px; margin: 0 0 16px; }
10
+ .trd-package-dialog p { white-space: pre-line; overflow-wrap: anywhere; }
11
+ .trd-package-dialog [data-detail] { font-size: 13px; opacity: .8; }
12
+ .trd-package-dialog p:empty { display: none; }
13
+ .trd-package-dialog progress { display: block; width: 100%; height: 8px; margin: 18px 0 8px; accent-color: #589cf0; }
14
+ .trd-package-dialog [hidden] { display: none !important; }
15
+ .trd-package-dialog [data-elapsed] { font-size: 12px; opacity: .7; }
16
+ .trd-package-dialog [data-phase=error] { color: var(--bs-danger, #f08080); }
17
+ .trd-package-dialog [data-phase=success] { color: var(--bs-success, #79c99b); }
18
+ .trd-package-dialog .trd-package-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 22px; }
19
+ `;
20
+ /** One modal remains open from confirmation through the installer's verified result. */
21
+ class PackageChangeDialog {
22
+ constructor() {
23
+ this.dialog = null;
24
+ this.previousFocus = null;
25
+ this.started = 0;
26
+ }
27
+ update(state) {
28
+ if (state.phase === 'cancelled') {
29
+ this.close();
30
+ return;
31
+ }
32
+ this.open();
33
+ this.field('[data-title]').textContent = state.action === 'install'
34
+ ? `Install tabby-rdp ${state.version}` : 'Uninstall tabby-rdp';
35
+ const result = this.field('[data-result]');
36
+ result.dataset.phase = state.phase;
37
+ result.setAttribute('role', state.phase === 'error' ? 'alert' : 'status');
38
+ const complete = state.phase === 'success' || state.phase === 'error';
39
+ this.field('progress').hidden = complete;
40
+ this.field('[data-elapsed]').hidden = complete;
41
+ this.field('[data-cancel]').hidden = true;
42
+ this.field('[data-confirm]').hidden = true;
43
+ this.field('[data-close]').hidden = !complete;
44
+ this.field('[data-detail]').textContent = '';
45
+ let message = state.message ?? '';
46
+ if (state.phase === 'preparing') {
47
+ message = state.action === 'install' ? 'Checking compatibility…' : 'Checking installation…';
48
+ }
49
+ if (state.phase === 'installing') {
50
+ message = `Installing tabby-rdp ${state.version}…`;
51
+ }
52
+ if (state.phase === 'uninstalling') {
53
+ message = 'Uninstalling tabby-rdp…';
54
+ }
55
+ if (state.phase === 'verifying') {
56
+ message = state.action === 'install' ? 'Verifying installation…' : 'Verifying removal…';
57
+ }
58
+ if (state.phase === 'success') {
59
+ this.field('[data-title]').textContent = state.action === 'install' ? 'Installation complete' : 'Uninstall complete';
60
+ if (!state.message) {
61
+ message = state.action === 'install' ? `tabby-rdp ${state.version} is installed.` : 'tabby-rdp is uninstalled.';
62
+ this.field('[data-detail]').textContent = 'Close remote desktops, then restart Tabby.';
63
+ }
64
+ }
65
+ if (state.phase === 'error') {
66
+ this.field('[data-title]').textContent = state.action === 'install' ? 'Installation failed' : 'Uninstall failed';
67
+ }
68
+ this.field('[data-message]').textContent = message;
69
+ if (complete) {
70
+ this.stopTimer();
71
+ this.field('[data-close]').focus();
72
+ }
73
+ else {
74
+ this.dialog.focus();
75
+ }
76
+ }
77
+ confirm(message, detail, action) {
78
+ this.open();
79
+ this.stopTimer();
80
+ this.field('[data-title]').textContent = message;
81
+ this.field('[data-message]').textContent = detail;
82
+ this.field('[data-detail]').textContent = '';
83
+ this.field('progress').hidden = true;
84
+ this.field('[data-elapsed]').hidden = true;
85
+ this.field('[data-close]').hidden = true;
86
+ const cancel = this.field('[data-cancel]');
87
+ const accept = this.field('[data-confirm]');
88
+ cancel.hidden = false;
89
+ accept.hidden = false;
90
+ accept.textContent = action;
91
+ this.field('[data-result]').dataset.phase = 'confirming';
92
+ cancel.focus();
93
+ return new Promise(resolve => { this.confirmation = resolve; });
94
+ }
95
+ field(selector) { return this.dialog.querySelector(selector); }
96
+ open() {
97
+ if (this.dialog) {
98
+ if (!this.dialog.open) {
99
+ this.dialog.showModal();
100
+ }
101
+ if (!this.timer) {
102
+ this.startTimer();
103
+ }
104
+ return;
105
+ }
106
+ if (!document.getElementById('trd-package-dialog-style')) {
107
+ const style = document.createElement('style');
108
+ style.id = 'trd-package-dialog-style';
109
+ style.textContent = STYLE;
110
+ document.head.appendChild(style);
111
+ }
112
+ this.previousFocus = document.activeElement instanceof HTMLElement ? document.activeElement : null;
113
+ const dialog = document.createElement('dialog');
114
+ this.dialog = dialog;
115
+ dialog.className = 'trd-package-dialog';
116
+ dialog.setAttribute('aria-labelledby', 'trd-package-dialog-title');
117
+ dialog.setAttribute('tabindex', '-1');
118
+ dialog.setAttribute('closedby', 'none');
119
+ dialog.innerHTML = `<h4 id="trd-package-dialog-title" data-title></h4>
120
+ <div data-result role="status" aria-live="polite"><p data-message></p><p data-detail></p></div>
121
+ <progress aria-label="Package change progress"></progress><div data-elapsed></div>
122
+ <div class="trd-package-actions"><button class="btn btn-secondary" data-cancel hidden>Cancel</button>
123
+ <button class="btn btn-primary" data-confirm hidden></button><button class="btn btn-primary" data-close hidden>Close</button></div>`;
124
+ this.field('[data-confirm]').addEventListener('click', () => this.decide(true));
125
+ this.field('[data-cancel]').addEventListener('click', () => this.decide(false));
126
+ this.field('[data-close]').addEventListener('click', () => this.close());
127
+ dialog.addEventListener('cancel', event => {
128
+ event.preventDefault();
129
+ this.dismiss();
130
+ });
131
+ // Keep package-dialog keystrokes out of Tabby's terminal and shortcut handlers.
132
+ dialog.addEventListener('keydown', event => {
133
+ event.stopPropagation();
134
+ if (event.key === 'Escape') {
135
+ event.preventDefault();
136
+ this.dismiss();
137
+ }
138
+ });
139
+ dialog.addEventListener('keyup', event => event.stopPropagation());
140
+ document.body.appendChild(dialog);
141
+ dialog.showModal();
142
+ this.startTimer();
143
+ }
144
+ decide(accepted) {
145
+ const resolve = this.confirmation;
146
+ if (!resolve) {
147
+ return;
148
+ }
149
+ this.confirmation = undefined;
150
+ this.field('[data-confirm]').hidden = true;
151
+ this.field('[data-cancel]').hidden = true;
152
+ resolve(accepted);
153
+ if (!accepted) {
154
+ this.close();
155
+ }
156
+ }
157
+ dismiss() {
158
+ if (this.confirmation) {
159
+ this.decide(false);
160
+ }
161
+ else if (!this.field('[data-close]').hidden) {
162
+ this.close();
163
+ }
164
+ }
165
+ startTimer() {
166
+ this.started = Date.now();
167
+ const tick = () => {
168
+ const seconds = Math.floor((Date.now() - this.started) / 1000);
169
+ this.field('[data-elapsed]').textContent = `Elapsed ${Math.floor(seconds / 60)}:${String(seconds % 60).padStart(2, '0')}`;
170
+ };
171
+ tick();
172
+ this.timer = setInterval(tick, 1000);
173
+ }
174
+ stopTimer() { clearInterval(this.timer); this.timer = undefined; }
175
+ close() {
176
+ this.stopTimer();
177
+ this.dialog?.close();
178
+ this.dialog?.remove();
179
+ this.dialog = null;
180
+ if (this.previousFocus?.isConnected) {
181
+ this.previousFocus.focus();
182
+ }
183
+ this.previousFocus = null;
184
+ }
185
+ }
186
+ exports.PackageChangeDialog = PackageChangeDialog;
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
- /** Collects the probes (ids starting with `prefix`) that arrive in `pane` while `during` runs, and briefly after. */
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(prefix)) {
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