tabby-rdp 0.5.1 → 0.5.3

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.
@@ -22,8 +22,10 @@ const drives_1 = require("./drives");
22
22
  const osd_1 = require("./osd");
23
23
  const help_1 = require("./help");
24
24
  const updates_1 = require("./updates");
25
+ const releaseSettings_1 = require("./releaseSettings");
25
26
  const targets_1 = require("./targets");
26
27
  const unshowable_1 = require("./unshowable");
28
+ const hostCompat_1 = require("./hostCompat");
27
29
  const REPO = 'https://github.com/meanaverage/tabby-rdp';
28
30
  const k = (s) => `<kbd>${(0, help_1.esc)(s)}</kbd>`;
29
31
  /** The page's tabs (as Tabby's "Profiles & connections" page has Profiles and Advanced), each holding sections by topic. */
@@ -33,7 +35,7 @@ const TABS = [
33
35
  { id: 'osd', title: 'Overlay', topics: ['osd'] },
34
36
  { id: 'desktops', title: 'Desktops', topics: ['desktops', 'certificates'] },
35
37
  { id: 'accounts', title: 'Accounts', topics: ['accounts'] },
36
- { id: 'troubleshooting', title: 'Troubleshooting', topics: ['troubleshooting'] },
38
+ { id: 'updates', title: 'Updates', topics: ['updates'] },
37
39
  ];
38
40
  /** The tab shown last, so that the page comes back to it. */
39
41
  let lastTab = 'start';
@@ -43,13 +45,22 @@ const STYLE = `
43
45
  .trd-settings { display: block; }
44
46
  .trd-settings h3 { margin-bottom: 4px; }
45
47
  .trd-settings .trd-lead { margin-bottom: 18px; opacity: 0.75; }
46
- .trd-settings .trd-links { white-space: nowrap; }
47
- .trd-settings .trd-lead a { margin-left: 10px; }
48
+ .trd-settings .trd-links { display: inline-flex; flex-wrap: wrap; gap: 6px 10px; margin-left: 10px; }
49
+ .trd-settings .trd-lead a { white-space: nowrap; }
48
50
  .trd-settings section .trd-lead a { margin-left: 0; cursor: pointer; text-decoration: underline; }
49
- .trd-settings .trd-update { display: flex; gap: 10px; align-items: center; margin-bottom: 18px; padding: 10px 12px; border-radius: 6px;
51
+ .trd-settings .trd-update { display: flex; flex-wrap: wrap; gap: 10px; align-items: center; margin-bottom: 18px; padding: 10px 12px; border-radius: 6px;
50
52
  border: 1px solid rgba(80, 150, 255, 0.45); background: rgba(80, 150, 255, 0.1); }
51
53
  .trd-settings .trd-update > div { flex: auto; }
52
54
  .trd-settings .trd-update:empty { display: none; }
55
+ .trd-settings .trd-release-summary { display: flex; flex-wrap: wrap; justify-content: space-between; align-items: center; gap: 12px; margin-bottom: 18px; }
56
+ .trd-settings .trd-release-summary h4 { margin: 3px 0; }
57
+ .trd-settings .trd-release-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; margin: 10px 0; }
58
+ .trd-settings .trd-release-actions select { width: auto; max-width: 100%; flex: 1 1 240px; }
59
+ .trd-settings .trd-release-notice { padding: 12px; border: 1px solid rgba(80,150,255,.45); border-radius: 6px; margin-bottom: 18px; }
60
+ .trd-settings .trd-release-error { color: var(--bs-danger, #e88); }
61
+ .trd-settings [data-release-settings] .trd-sub { font-size: 12px; opacity: .75; }
62
+ .trd-settings .trd-release-pause { display: flex; align-items: center; gap: 8px; margin-top: 14px; }
63
+ @media (max-width: 700px) { .trd-settings [data-release-settings] .form-line { flex-wrap: wrap; gap: 8px; } }
53
64
  .trd-settings .nav-tabs { margin-bottom: 18px; }
54
65
  .trd-settings .nav-tabs .nav-link { cursor: pointer; }
55
66
  .trd-settings section { margin-bottom: 28px; scroll-margin-top: 12px; }
@@ -61,7 +72,7 @@ const STYLE = `
61
72
  .trd-settings .trd-card { display: flex; flex-direction: column; gap: 8px; padding: 12px; border-radius: 6px;
62
73
  border: 1px solid rgba(128, 128, 128, 0.25); }
63
74
  .trd-settings .trd-card .trd-card-title { font-weight: 600; display: flex; align-items: center; gap: 6px; }
64
- .trd-settings h4 .trd-info, .trd-settings .trd-lead .trd-info, .trd-settings .form-line .title .trd-info { margin-left: 4px; vertical-align: middle; font-size: 13px; }
75
+ .trd-settings .trd-card-text .trd-info, .trd-settings .trd-lead .trd-info, .trd-settings .description .trd-info { margin-left: 4px; vertical-align: middle; font-size: 13px; }
65
76
  .trd-settings .trd-info { position: relative; display: inline-flex; opacity: 0.55; cursor: help; outline: none; font-weight: normal; }
66
77
  .trd-settings .trd-info:hover, .trd-settings .trd-info:focus { opacity: 1; }
67
78
  .trd-settings .trd-info::after { content: attr(data-tip); position: absolute; left: 0; top: calc(100% + 6px); z-index: 20; width: 260px;
@@ -150,10 +161,10 @@ exports.RemoteDesktopSettingsTab = RemoteDesktopSettingsTab = __decorate([
150
161
  ], RemoteDesktopSettingsTab);
151
162
  /**
152
163
  * The plugin's settings page: getting started, the settings, keys, desktops and certificates it keeps, and
153
- * troubleshooting. Plain DOM like the rest of the plugin; the lists follow config changes made elsewhere.
164
+ * updates. Plain DOM like the rest of the plugin; the lists follow config changes made elsewhere.
154
165
  */
155
166
  let RemoteDesktopSettingsComponent = class RemoteDesktopSettingsComponent {
156
- constructor(element, desktop, help, config, platform, injector, zone, hotkeys) {
167
+ constructor(element, desktop, help, config, platform, injector, zone, hotkeys, compat) {
157
168
  this.element = element;
158
169
  this.desktop = desktop;
159
170
  this.help = help;
@@ -162,6 +173,7 @@ let RemoteDesktopSettingsComponent = class RemoteDesktopSettingsComponent {
162
173
  this.injector = injector;
163
174
  this.zone = zone;
164
175
  this.hotkeys = hotkeys;
176
+ this.compat = compat;
165
177
  this.contentBox = true;
166
178
  this.trdSettings = true;
167
179
  this.subscriptions = [];
@@ -176,8 +188,6 @@ let RemoteDesktopSettingsComponent = class RemoteDesktopSettingsComponent {
176
188
  document.head.appendChild(style);
177
189
  }
178
190
  this.render();
179
- // A known newer version may be stale by now (the page is where it shows): ask npm again.
180
- this.injector.get(updates_1.UpdateCheck).refresh();
181
191
  // The preview is a desktop of the chosen resolution, scaled down to fit the page.
182
192
  const frame = this.root.querySelector('.trd-osd-preview');
183
193
  const scale = new ResizeObserver(() => this.scalePreview());
@@ -198,38 +208,33 @@ let RemoteDesktopSettingsComponent = class RemoteDesktopSettingsComponent {
198
208
  }
199
209
  render() {
200
210
  const mac = process.platform === 'darwin';
201
- const version = (() => {
202
- try {
203
- return require('../package.json').version;
204
- }
205
- catch {
206
- return '';
207
- }
208
- })();
211
+ const version = (0, updates_1.installedVersion)();
209
212
  this.root.innerHTML = `
210
213
  <h3>Remote Desktop</h3>
211
214
  <div class="trd-lead">Linux and Windows desktops in Tabby tabs, through SSH or directly.
212
215
  ${version ? `<span>tabby-rdp ${(0, help_1.esc)(version)}</span>` : ''}
213
- <span class="trd-links"><a href="#" data-link="${REPO}#readme">Guide</a><a href="#" data-link="${REPO}/issues">Report a problem</a><a href="#" data-link="${REPO}/discussions/categories/q-a">Ask a question</a></span></div>
214
- <div class="trd-update" data-update></div>
216
+ <span class="trd-links"><a href="#" data-link="${REPO}#readme">Guide</a><a href="#" data-link="${REPO}/issues">Report a problem</a><a href="#" data-link="${REPO}/discussions/categories/q-a">Ask a question</a><a href="#" data-link="${help_1.TROUBLESHOOTING_URL}">Troubleshooting</a></span></div>
215
217
  <ul class="nav nav-tabs" data-nav></ul>
216
218
 
217
219
  <section data-topic="start">
218
220
  <h4>Getting started</h4>
219
221
  <div class="trd-cards">
220
222
  <div class="trd-card">
221
- <div class="trd-card-title">Over SSH ${this.info('The host is set up on the first connection: GNOME, xrdp, or Windows with OpenSSH. Nothing to install here, no ports to open.')}</div>
222
- <div class="trd-card-text">In an SSH tab, press <span data-hotkey></span> or click <b>Desktop</b>.</div>
223
+ <div class="trd-card-title">Over SSH</div>
224
+ <div class="trd-card-text">In an SSH tab, press <span data-hotkey></span> or click <b>Desktop</b>.
225
+ ${this.info('The host is set up on the first connection: GNOME, xrdp, or Windows with OpenSSH. Nothing to install here, no ports to open.')}</div>
223
226
  <button class="btn btn-secondary btn-sm" data-action="ssh">Open a connection…</button>
224
227
  </div>
225
228
  <div class="trd-card">
226
- <div class="trd-card-title">On your network ${this.info('The desktop opens in a tab of its own. For a quick connection, type user@host in the profile selector.')}</div>
227
- <div class="trd-card-text">A profile that connects straight to an RDP server.</div>
229
+ <div class="trd-card-title">On your network</div>
230
+ <div class="trd-card-text">A profile that connects straight to an RDP server.
231
+ ${this.info('The desktop opens in a tab of its own. For a quick connection, type user@host in the profile selector.')}</div>
228
232
  <button class="btn btn-secondary btn-sm" data-action="profile">New profile…</button>
229
233
  </div>
230
234
  <div class="trd-card">
231
- <div class="trd-card-title">From an .rdp file ${this.info('A file from Remote Desktop Connection, or from your admin. Its address, user name and domain make the profile; the password is asked for.')}</div>
232
- <div class="trd-card-text">The file becomes a profile.</div>
235
+ <div class="trd-card-title">From an .rdp file</div>
236
+ <div class="trd-card-text">The file becomes a profile.
237
+ ${this.info('A file from Remote Desktop Connection, or from your admin. Its address, user name and domain make the profile; the password is asked for.')}</div>
233
238
  <button class="btn btn-secondary btn-sm" data-action="import">Import an .rdp file…</button>
234
239
  </div>
235
240
  </div>
@@ -241,8 +246,7 @@ let RemoteDesktopSettingsComponent = class RemoteDesktopSettingsComponent {
241
246
  <div class="form-line">
242
247
  <div class="header">
243
248
  <div class="title">When the pane is resized</div>
244
- <div class="description">Resizing follows splits and the window; a kept resolution can be scaled or
245
- shown at actual size with scroll bars.</div>
249
+ <div class="description">Fixed resolutions use scaling or scroll bars.</div>
246
250
  </div>
247
251
  <select class="form-control" data-setting="resize">
248
252
  <option value="live">Resize to fit</option>
@@ -254,28 +258,27 @@ let RemoteDesktopSettingsComponent = class RemoteDesktopSettingsComponent {
254
258
  <div class="form-line">
255
259
  <div class="header">
256
260
  <div class="title">Sharpness</div>
257
- <div class="description">Retina renders at your screen's pixels, with the remote's scale set to
258
- match. A desktop can have its own, in its tab's menu.</div>
261
+ <div class="description">Retina uses your screen's pixel density and matching remote scaling.</div>
259
262
  </div>
260
263
  <select class="form-control" data-setting="sharpness">
261
264
  <option value="standard">Standard</option>
262
265
  <option value="retina">Retina</option>
263
266
  </select>
264
267
  </div>
265
- ${this.toggleLine('h264', 'Video decoding (H.264)', 'Decodes what the remote sends as video, hardware-accelerated where available. Applies on the next connection.')}
266
- ${this.toggleLine('connectionStatus', 'Show connection status', 'A small line in the desktop\'s corner: throughput, frames per second, round trip, and how it\'s connected.')}
268
+ ${this.toggleLine('h264', 'Video decoding (H.264)', 'Hardware acceleration where available. Applies on the next connection.')}
269
+ ${this.toggleLine('connectionStatus', 'Show connection status', 'Show throughput, frame rate, latency and connection details.')}
267
270
 
268
271
  <h5>Sound</h5>
269
- ${this.toggleLine('sound', 'Sound', 'Play the remote desktop\'s sound here. Applies on the next connection.')}
270
- ${this.toggleLine('microphone', 'Microphone', 'Send your microphone to a remote desktop that asks for it, as one does while an app there records (a call, say). Meanwhile a red dot shows in the desktop\'s corner, and a red microphone in Tabby\'s header (in full screen, at the top of the window), also while the desktop is hidden. Its menu shows that desktop, or stops sending the microphone there until Send the microphone is turned back on in the desktop\'s menu, or Tabby quits. Turning the setting on applies on the next connection; off stops it at once.')}
272
+ ${this.toggleLine('sound', 'Sound', 'Play remote audio. Applies on the next connection.')}
273
+ ${this.toggleLine('microphone', 'Microphone', `Share your microphone when a remote app requests it. Applies on the next connection; turning it off stops sharing immediately. ${this.info('A red indicator remains visible while sharing, including from background desktops. Its menu can stop sharing with an individual desktop. That desktop stays muted through reconnects until re-enabled or Tabby closes.')}`)}
271
274
 
272
275
  <h5>Clipboard</h5>
273
276
  <div class="form-line">
274
277
  <div class="header">
275
- <div class="title">Clipboard sharing ${this.info('A desktop that gets your clipboard sees what is on it whenever it has the keyboard: what you copy then, and what you copied elsewhere before clicking back into it, passwords included. One that sends its clipboard here can put anything on yours, such as a command for you to paste into a terminal. Off leaves the clipboard out of the connection altogether. Narrowed while a desktop is open, what you copy from then on stays here and the files offered there are taken back; the change applies in full once that desktop reconnects.')}</div>
276
- <div class="description">Which ways text, pictures and files copied on either side go. A desktop can have its
277
- own, in its profile or its edit form. Applies on the next connection; narrowing it also stops the
278
- sending from here at once to open desktops that follow the setting.</div>
278
+ <div class="title">Clipboard sharing</div>
279
+ <div class="description">Share text, images and files. Desktops can override this setting. Applies on reconnect;
280
+ restricting sharing stops local sends immediately.
281
+ ${this.info('Sharing can expose passwords and let the remote replace your local clipboard. Only the focused desktop receives your clipboard. Off disables text, images and clipboard file transfers. Restricting sharing also withdraws previously offered files.')}</div>
279
282
  </div>
280
283
  <select class="form-control" data-setting="clipboard">
281
284
  ${Object.entries(clipboard_1.CLIPBOARD_LABELS).map(([mode, label]) => `<option value="${mode}">${(0, help_1.esc)(label)}</option>`).join('')}
@@ -283,33 +286,30 @@ let RemoteDesktopSettingsComponent = class RemoteDesktopSettingsComponent {
283
286
  </div>
284
287
 
285
288
  <h5>Shared folders</h5>
286
- <div class="trd-lead">Folders from this computer as drives on the remote desktop, under <code>\\\\tsclient</code> in
287
- Explorer. Applies on the next connection.
288
- ${this.info('Every remote desktop you connect to sees these folders, so share only what each of them may have. Read-only keeps the remote from changing anything in a folder. Windows mounts them (xrdp can, with FUSE); GNOME Remote Desktop doesn\'t serve drives.')}</div>
289
+ <div class="trd-lead">Available as remote drives (<code>\\\\tsclient</code> on Windows). Applies on the next connection.
290
+ ${this.info('Shared with every remote desktop. Use read-only to prevent remote changes. Supported by Windows and xrdp with FUSE; unavailable on GNOME Remote Desktop.')}</div>
289
291
  <div class="trd-list" data-list="folders"></div>
290
292
  <div class="trd-add"><button class="btn btn-secondary btn-sm" data-action="folder">Share a folder…</button></div>
291
293
 
292
294
  <h5>Keyboard</h5>
293
- ${this.toggleLine('unicodeKeys', 'Send text as typed', 'Sends the characters your keyboard produces rather than key positions, so dead keys (´ then e) and a layout the remote doesn\'t have come out right. Shortcuts with Ctrl, Alt or ⌘ still go by position. On GNOME, characters the remote\'s own layout lacks still can\'t be typed; Windows takes any. Input methods (Chinese, Japanese, Korean) aren\'t supported yet.')}
294
- ${mac ? this.toggleLine('macShortcuts', 'Mac-style shortcuts', 'Use ⌘ as Ctrl on the desktop, so ⌘C copies and ⌘V pastes. Tap ⌘ on its own for the Windows key. When off, ⌘ is always the Windows key.') : ''}
295
+ ${this.toggleLine('unicodeKeys', 'Send text as typed', `Send characters instead of physical keys. Modifier shortcuts still use key positions. ${this.info('Useful for dead keys or mismatched keyboard layouts. GNOME is limited to characters in its own layout. Chinese, Japanese and Korean input methods are not supported.')}`)}
296
+ ${mac ? this.toggleLine('macShortcuts', 'Mac-style shortcuts', '⌘C copies and ⌘V pastes. Tap ⌘ for the Windows key; when disabled, ⌘ always sends the Windows key.') : ''}
295
297
  <div class="form-line">
296
298
  <div class="header">
297
299
  <div class="title">Shortcuts</div>
298
- <div class="description">Switching between the desktop and the console, view only, screenshots and more: on the
299
- <a data-action="shortcuts">Getting started</a> tab, and in Tabby's Hotkeys.</div>
300
+ <div class="description">Manage in <a data-action="shortcuts">Getting started</a> or Tabby's Hotkeys.</div>
300
301
  </div>
301
302
  <div><span data-hotkey></span></div>
302
303
  </div>
303
304
 
304
305
  <h5>SSH hosts</h5>
305
- ${this.toggleLine('desk', 'desk: bring the console along', `Type <code>desk</code> in an SSH console to show that same shell on the desktop. Installs a small helper and a login line on GNOME hosts the next time a desktop opens there; off removes them.`)}
306
- ${this.toggleLine('discoverVMs', 'Find virtual machines on SSH hosts', 'Lists the host\'s VMs in its tab\'s menu, ready to open or start: libvirt VMs that have a desktop (RDP answering, or Windows and shut off), and on a Windows host, Hyper-V VMs (their consoles, through the host). Read-only: it runs virsh, or PowerShell\'s Get-VM, as you there, at most once a minute.')}
306
+ ${this.toggleLine('desk', 'desk integration', `Type <code>desk</code> to open your current SSH shell on the desktop. Installs a helper on GNOME hosts on the next desktop connection; disabling removes it.`)}
307
+ ${this.toggleLine('discoverVMs', 'Find virtual machines on SSH hosts', 'List libvirt and Hyper-V VMs in the SSH tab menu. Discovery is read-only and runs at most once a minute.')}
307
308
  <div class="form-line">
308
309
  <div class="header">
309
- <div class="title">Shut down VMs it started</div>
310
- <div class="description">A VM started to open its desktop goes back off after this long with no
311
- desktop open, also once Tabby is closed: a small watcher on the SSH host sees to it. VMs that
312
- were already running are left alone.</div>
310
+ <div class="title">Shut down started VMs</div>
311
+ <div class="description">Shut down VMs started by this plugin after the selected time with no desktop open.
312
+ Continues after Tabby closes; previously running VMs are unaffected.</div>
313
313
  </div>
314
314
  <select class="form-control" data-setting="shutDownIdle">
315
315
  <option value="0">Never</option>
@@ -319,16 +319,13 @@ let RemoteDesktopSettingsComponent = class RemoteDesktopSettingsComponent {
319
319
  </select>
320
320
  </div>
321
321
 
322
- <h5>Updates</h5>
323
- ${this.toggleLine('checkUpdates', 'Tell me about new versions', 'Once a day, asks npm (registry.npmjs.org) for the latest tabby-rdp, and says so here and in the menus when there is one. Nothing is sent but the request. Tabby itself shows plugin upgrades only on its Plugins page.')}
324
322
  </section>
325
323
 
326
324
  <section data-topic="osd">
327
325
  <h4>Desktop name overlay</h4>
328
- <div class="trd-lead">Names the desktop for a moment when it connects, when you switch to it, and when you click
329
- into its pane, like a TV naming its input.</div>
326
+ <div class="trd-lead">Show the desktop name when connecting, switching desktops or focusing a pane.</div>
330
327
  <div class="trd-osd-preview"><div class="trd-osd-stage"><div class="trd-osd"></div></div>
331
- <select class="form-control form-control-sm trd-osd-resolution" data-preview-resolution title="The desktop resolution the preview stands for: the overlay's size on screen depends on it">
328
+ <select class="form-control form-control-sm trd-osd-resolution" data-preview-resolution title="Preview resolution">
332
329
  <option value="1280x720">1280 × 720</option>
333
330
  <option value="1920x1080">1920 × 1080</option>
334
331
  <option value="2560x1440">2560 × 1440</option>
@@ -336,10 +333,9 @@ let RemoteDesktopSettingsComponent = class RemoteDesktopSettingsComponent {
336
333
  </select></div>
337
334
  <div class="form-line">
338
335
  <div class="header"><div class="title">Show</div>
339
- <div class="description">"When it helps": in split panes, and for a desktop other than the tab's own
340
- connection, where the tab's title doesn't say which it is.</div></div>
336
+ <div class="description">Automatic shows names in split panes and when the tab title identifies a different connection.</div></div>
341
337
  <select class="form-control" data-osd="show">
342
- <option value="auto">When it helps</option>
338
+ <option value="auto">Automatic</option>
343
339
  <option value="always">Every time</option>
344
340
  <option value="off">Never</option>
345
341
  </select>
@@ -349,7 +345,7 @@ let RemoteDesktopSettingsComponent = class RemoteDesktopSettingsComponent {
349
345
  <select class="form-control" data-osd="font">${Object.entries(osd_1.OSD_FONTS).map(([id, f]) => `<option value="${id}">${(0, help_1.esc)(f.label)}</option>`).join('')}</select>
350
346
  </div>
351
347
  <div class="form-line">
352
- <div class="header"><div class="title">Size</div><div class="description">Smaller in a narrow pane, so a name fits.</div></div>
348
+ <div class="header"><div class="title">Size</div></div>
353
349
  <select class="form-control" data-osd="size">${Object.keys(osd_1.OSD_SIZES).map(id => `<option value="${id}">${id === 'huge' ? 'Extra large' : id[0].toUpperCase() + id.slice(1)}</option>`).join('')}</select>
354
350
  </div>
355
351
  <div class="form-line">
@@ -357,12 +353,11 @@ let RemoteDesktopSettingsComponent = class RemoteDesktopSettingsComponent {
357
353
  <select class="form-control" data-osd="position">${osd_1.OSD_POSITIONS.map(p => `<option value="${p}">${p === 'middle' ? 'Middle' : p.replace('-', ' ').replace(/^./, c => c.toUpperCase())}</option>`).join('')}</select>
358
354
  </div>
359
355
  <div class="form-line">
360
- <div class="header"><div class="title">Color</div><div class="description">White reads on any picture, with the soft
361
- shadow behind it. The line before the second row takes your theme's accent color.</div></div>
356
+ <div class="header"><div class="title">Color</div></div>
362
357
  <div class="trd-color"><input type="color" data-osd="color"><button class="btn btn-link btn-sm" data-action="osd-white">White</button></div>
363
358
  </div>
364
359
  <div class="form-line">
365
- <div class="header"><div class="title">How long</div></div>
360
+ <div class="header"><div class="title">Duration</div></div>
366
361
  <select class="form-control" data-osd="seconds">
367
362
  <option value="1.5">1.5 seconds</option>
368
363
  <option value="2.5">2.5 seconds</option>
@@ -370,12 +365,13 @@ let RemoteDesktopSettingsComponent = class RemoteDesktopSettingsComponent {
370
365
  <option value="6">6 seconds</option>
371
366
  </select>
372
367
  </div>
373
- <button class="btn btn-secondary btn-sm" data-action="osd-try">Show it on open desktops</button>
368
+ <button class="btn btn-secondary btn-sm" data-action="osd-try">Preview on open desktops</button>
374
369
  </section>
375
370
 
376
371
  <section data-topic="keyboard">
377
- <h4>Shortcuts ${this.info('These work while the desktop has the keyboard. Tabby\'s other shortcuts go to the desktop then, except switching tabs and full screen.')}</h4>
378
- <div class="trd-lead">Also listed in Tabby's <a data-action="hotkeys">Hotkeys</a>.</div>
372
+ <h4>Shortcuts</h4>
373
+ <div class="trd-lead">Also listed in Tabby's <a data-action="hotkeys">Hotkeys</a>.
374
+ ${this.info('These work while the desktop has the keyboard. Tabby\'s other shortcuts go to the desktop then, except switching tabs and full screen.')}</div>
379
375
  <div class="trd-hotkeys" data-list="hotkeys"></div>
380
376
  <h4 style="margin-top: 24px">Keys and tips</h4>
381
377
  <table class="trd-keys"><tbody>
@@ -395,13 +391,15 @@ let RemoteDesktopSettingsComponent = class RemoteDesktopSettingsComponent {
395
391
  <div class="trd-lead">Remote desktop profiles, and desktops added from SSH tabs.
396
392
  ${this.info('A desktop added from an SSH tab\'s menu belongs to that host. That is for hosts without a profile, such as ssh run in a local terminal.')}</div>
397
393
  <div class="trd-list" data-list="desktops"></div>
398
- <div class="trd-add"><button class="btn btn-secondary btn-sm" data-action="desktop">Add a desktop…</button></div>
394
+ <div class="trd-add"><button class="btn btn-secondary btn-sm" data-action="desktop">Add desktop…</button></div>
395
+ <h4 style="margin-top: 24px">Open desktops</h4>
396
+ <div class="trd-list" data-list="sessions"></div>
399
397
  </section>
400
398
 
401
399
  <section data-topic="accounts">
402
400
  <h4>Accounts</h4>
403
- <div class="trd-lead">One sign-in for several desktops: when its password changes, change it once here.
404
- ${this.info('A desktop picks its account in its profile\'s settings, or in the form that adds a desktop behind an SSH host. A profile group\'s defaults can pick one for all of its profiles.')}</div>
401
+ <div class="trd-lead">Reusable sign-in details for multiple desktops.
402
+ ${this.info('Choose an account in a desktop\'s settings or a profile group\'s defaults.')}</div>
405
403
  <div class="trd-list" data-list="accounts"></div>
406
404
  <div class="trd-add">
407
405
  <button class="btn btn-secondary btn-sm" data-action="account">Add an account…</button>
@@ -414,14 +412,7 @@ let RemoteDesktopSettingsComponent = class RemoteDesktopSettingsComponent {
414
412
  <div class="trd-list" data-list="certificates"></div>
415
413
  </section>
416
414
 
417
- <section data-topic="troubleshooting">
418
- <h4>Troubleshooting</h4>
419
- <div data-list="troubleshooting">${help_1.TROUBLESHOOTING.map(e => `
420
- <details data-entry="${(0, help_1.esc)(e.id)}"><summary>${(0, help_1.esc)(e.title)}</summary><div>${e.body}</div></details>`).join('')}
421
- </div>
422
- <h4 style="margin-top: 18px">Open desktops</h4>
423
- <div class="trd-list" data-list="sessions"></div>
424
- </section>`;
415
+ <section data-topic="updates"><h4>Updates</h4><div class="trd-update" data-update></div><div data-release-settings></div></section>`;
425
416
  // The sections go into the tabs' bodies; one tab shows at a time.
426
417
  const nav = this.root.querySelector('[data-nav]');
427
418
  for (const tab of TABS) {
@@ -482,7 +473,7 @@ let RemoteDesktopSettingsComponent = class RemoteDesktopSettingsComponent {
482
473
  'osd-white': () => this.changeOsd({ color: '' }),
483
474
  'osd-try': () => {
484
475
  if (!this.desktop.showOsdEverywhere()) {
485
- this.injector.get(tabby_core_1.NotificationsService).info('No desktop is showing right now');
476
+ this.injector.get(tabby_core_1.NotificationsService).info('No visible desktops.');
486
477
  }
487
478
  },
488
479
  };
@@ -491,7 +482,7 @@ let RemoteDesktopSettingsComponent = class RemoteDesktopSettingsComponent {
491
482
  }));
492
483
  this.refresh();
493
484
  }
494
- /** An ⓘ that shows `text` while hovered or focused: detail that the card itself needn't carry. */
485
+ /** An ⓘ placed after a description, showing `text` while hovered or focused. */
495
486
  info(text) {
496
487
  return `<span class="trd-info" tabindex="0" role="note" data-tip="${(0, help_1.esc)(text)}"><i class="fas fa-info-circle"></i></span>`;
497
488
  }
@@ -545,7 +536,7 @@ let RemoteDesktopSettingsComponent = class RemoteDesktopSettingsComponent {
545
536
  el.value = String(settings[key]);
546
537
  }
547
538
  });
548
- // A newer version: say so at the top, with where to get it.
539
+ // Update notices and controls belong to the Updates page.
549
540
  const updates = this.injector.get(updates_1.UpdateCheck);
550
541
  const banner = this.root.querySelector('[data-update]');
551
542
  if (banner) {
@@ -554,6 +545,10 @@ let RemoteDesktopSettingsComponent = class RemoteDesktopSettingsComponent {
554
545
  banner.querySelector('[data-upgrade]')?.addEventListener('click', () => updates.upgrade());
555
546
  banner.querySelector('[data-whatsnew]')?.addEventListener('click', () => updates.whatsNew());
556
547
  }
548
+ (0, releaseSettings_1.renderReleaseSettings)(this.root.querySelector('[data-release-settings]'), updates, {
549
+ notes: version => updates.whatsNew(version), plugins: () => this.help.openSettings('plugins'),
550
+ link: url => this.platform.openExternal(url),
551
+ });
557
552
  const osd = settings.osd;
558
553
  this.root.querySelectorAll('[data-osd]').forEach(el => {
559
554
  const key = el.dataset.osd;
@@ -602,29 +597,34 @@ let RemoteDesktopSettingsComponent = class RemoteDesktopSettingsComponent {
602
597
  const accounts = this.desktop.accounts();
603
598
  const accountOf = (id) => id ? accounts.find(a => a.id === id) : undefined;
604
599
  // Remote desktop profiles first (Tabby's editor), then the desktops added from SSH tabs (the plugin's form).
605
- const profiles = (this.config.store.profiles ?? []).filter((p) => p?.type === targets_1.RDP_PROFILE_TYPE && p.options?.host);
606
- const groups = this.config.store.groups ?? [];
600
+ const profiles = (this.config.store.profiles ?? []).filter((p) => p?.type === targets_1.RDP_PROFILE_TYPE);
601
+ const gatewaySignIn = (id) => {
602
+ const account = accountOf(id);
603
+ return account ? `, as ${(0, help_1.esc)(account.name)}` : id === accounts_1.ASK_GATEWAY_ACCOUNT ? ', asks for a gateway account'
604
+ : id ? ', saved gateway account missing (asks when connecting)' : '';
605
+ };
607
606
  // Every field as text whatever the config holds (see configText): one that can't be shown would leave this list,
608
607
  // and the lists after it on the page, unbuilt.
609
- const profileRows = profiles.map(p => {
610
- const o = p.options;
608
+ const profileRows = profiles.flatMap(p => {
609
+ const o = this.injector.get(tabby_core_1.ProfilesService).getConfigProxyForProfile(p).options;
610
+ if (!o?.host) {
611
+ return [];
612
+ }
611
613
  const address = `${(0, desktops_1.configText)(o.host)}:${(0, desktops_1.configText)(o.port || 3389)}`;
612
614
  const via = o.via ? (0, desktops_1.configText)((this.config.store.profiles ?? []).find((x) => x.id === o.via)?.name) || 'an SSH profile' : null;
613
- const inherited = o.account === undefined ? groups.find(g => g.id === p.group)?.defaults?.[targets_1.RDP_PROFILE_TYPE]?.options?.account : undefined;
614
- const account = accountOf(o.account || inherited);
615
- // Its own clipboard, or its group's or the type's default: what it connects with, as Tabby resolves the profile.
616
- const clipboard = (0, clipboard_1.ownClipboard)(this.injector.get(tabby_core_1.ProfilesService).getConfigProxyForProfile(p).options?.clipboard);
615
+ const account = accountOf(o.account);
616
+ const clipboard = (0, clipboard_1.ownClipboard)(o.clipboard);
617
617
  const [username, kind, gateway] = [(0, desktops_1.configText)(o.username), (0, desktops_1.configText)(o.kind), (0, desktops_1.configText)(o.gateway)];
618
618
  const details = [
619
619
  account ? `Signs in as ${(0, help_1.esc)(account.name)}` : username ? `Signs in as ${(0, help_1.esc)(username)}` : '',
620
620
  kind && kind !== 'windows' ? (0, help_1.esc)(kind) : '',
621
- gateway ? `Through the gateway ${(0, help_1.esc)(gateway)}${accountOf(o.gatewayAccount) ? `, as ${(0, help_1.esc)(accountOf(o.gatewayAccount).name)}` : ''}` : '',
621
+ gateway ? `Through the gateway ${(0, help_1.esc)(gateway)}${gatewaySignIn(o.gatewayAccount)}` : '',
622
622
  clipboard ? `Clipboard: ${(0, help_1.esc)(clipboard_1.CLIPBOARD_LABELS[clipboard].toLowerCase())}` : '',
623
623
  ].filter(Boolean).join('<br>');
624
- return this.row((0, help_1.esc)((0, desktops_1.configText)(p.name) || address), `${(0, help_1.esc)(address)}${via ? ` via ${(0, help_1.esc)(via)}` : ''}${details ? `<br>${details}` : ''}`, [
625
- { label: 'Edit…', run: () => { this.editProfile(p.id); } },
626
- { label: 'Remove…', danger: true, run: () => { this.removeProfile(p.id); } },
627
- ]);
624
+ return [this.row((0, help_1.esc)((0, desktops_1.configText)(p.name) || address), `${(0, help_1.esc)(address)}${via ? ` via ${(0, help_1.esc)(via)}` : ''}${details ? `<br>${details}` : ''}`, [
625
+ { label: 'Edit…', run: () => { this.editProfile(p.id); } },
626
+ { label: 'Remove…', danger: true, run: () => { this.removeProfile(p.id); } },
627
+ ])];
628
628
  });
629
629
  const desktops = this.desktop.configuredDesktops();
630
630
  // An item that is no entry describes no desktop, and isn't listed (see isDesktopEntry).
@@ -639,7 +639,7 @@ let RemoteDesktopSettingsComponent = class RemoteDesktopSettingsComponent {
639
639
  const details = [
640
640
  account ? `Signs in as ${(0, help_1.esc)(account.name)}` : username ? `Signs in as ${(0, help_1.esc)(username)}` : '',
641
641
  kind && kind !== 'windows' ? (0, help_1.esc)(kind) : '',
642
- gateway && !hyperv ? `Through the gateway ${(0, help_1.esc)(gateway)}${accountOf(d.gatewayAccount) ? `, as ${(0, help_1.esc)(accountOf(d.gatewayAccount).name)}` : ''}` : '',
642
+ gateway && !hyperv ? `Through the gateway ${(0, help_1.esc)(gateway)}${gatewaySignIn(d.gatewayAccount)}` : '',
643
643
  vm ? `Starts VM ${(0, help_1.esc)(vm)} when off` : mac ? `Wakes ${(0, help_1.esc)(mac)} when off` : '',
644
644
  (0, clipboard_1.ownClipboard)(d.clipboard) ? `Clipboard: ${(0, help_1.esc)(clipboard_1.CLIPBOARD_LABELS[(0, clipboard_1.ownClipboard)(d.clipboard)].toLowerCase())}` : '',
645
645
  ].filter(Boolean).join('<br>');
@@ -648,9 +648,7 @@ let RemoteDesktopSettingsComponent = class RemoteDesktopSettingsComponent {
648
648
  { label: 'Remove…', danger: true, run: () => { this.desktop.confirmRemoveDesktop(i); } },
649
649
  ])];
650
650
  });
651
- list.replaceChildren(...profileRows, ...desktopRows, this.empty(profileRows.length + desktopRows.length
652
- ? 'Profiles are also in Profiles &amp; connections; a desktop behind a host also in that host\'s SSH tab: right-click › Settings.'
653
- : 'None yet. <b>Add a desktop…</b> makes a profile: an RDP server on your network, or one behind an SSH profile\'s host.'));
651
+ list.replaceChildren(...profileRows, ...desktopRows, ...(profileRows.length + desktopRows.length ? [] : [this.empty('No saved desktops.')]));
654
652
  }
655
653
  renderFolders() {
656
654
  const list = this.root.querySelector('[data-list=folders]');
@@ -672,7 +670,7 @@ let RemoteDesktopSettingsComponent = class RemoteDesktopSettingsComponent {
672
670
  }));
673
671
  row.insertBefore(check, row.querySelector('button'));
674
672
  return row;
675
- }), ...folders.length ? [] : [this.empty('None shared. The clipboard carries files too, the ways it goes: drop them on the desktop, or copy them there and save them here.')]);
673
+ }), ...folders.length ? [] : [this.empty('No shared folders.')]);
676
674
  }
677
675
  /** Asks for a folder and shares it under its own name (numbered when another share has it). */
678
676
  async shareFolder() {
@@ -682,7 +680,7 @@ let RemoteDesktopSettingsComponent = class RemoteDesktopSettingsComponent {
682
680
  }
683
681
  const folders = this.desktop.sharedFolders();
684
682
  if (folders.some(f => f.path === picked)) {
685
- this.injector.get(tabby_core_1.NotificationsService).info('That folder is shared already');
683
+ this.injector.get(tabby_core_1.NotificationsService).info('This folder is already shared.');
686
684
  return;
687
685
  }
688
686
  this.desktop.setSharedFolders([...folders, { path: picked, name: (0, drives_1.driveName)((0, path_1.basename)(picked) || picked, folders), readOnly: false }]);
@@ -703,7 +701,7 @@ let RemoteDesktopSettingsComponent = class RemoteDesktopSettingsComponent {
703
701
  const used = uses.length
704
702
  ? `Used by ${uses.map(u => u.profileId ? `<a data-profile="${(0, help_1.esc)(u.profileId)}">${(0, help_1.esc)(u.name)}</a>`
705
703
  : u.desktopIndex !== undefined ? `<a data-desktop="${u.desktopIndex}">${(0, help_1.esc)(u.name)}</a>` : (0, help_1.esc)(u.name)).join(', ')}`
706
- : 'Not used by a desktop yet';
704
+ : 'Not used';
707
705
  const row = this.row((0, help_1.esc)(a.name), `<span>${(0, help_1.esc)((0, accounts_1.signInName)(a))}</span><br><span>${used}</span><span data-password></span>`, [
708
706
  { label: 'Edit…', run: () => this.editAccount(a) },
709
707
  { label: 'Remove…', danger: true, run: () => { this.desktop.confirmRemoveAccount(a.id); } },
@@ -714,11 +712,11 @@ let RemoteDesktopSettingsComponent = class RemoteDesktopSettingsComponent {
714
712
  checks = checks.then(() => row.isConnected ? this.desktop.accountHasPassword(a.id) : 'unknown').then(has => {
715
713
  const note = row.querySelector('[data-password]');
716
714
  if (note && has === 'no') {
717
- note.innerHTML = '<br>No password saved: asked for on the next connection';
715
+ note.innerHTML = '<br>No password saved';
718
716
  }
719
717
  }, () => null);
720
718
  return row;
721
- }), ...accounts.length ? [] : [this.empty('None yet. Without one, each desktop asks for its account and can remember it for itself.')]);
719
+ }), ...accounts.length ? [] : [this.empty('No saved accounts.')]);
722
720
  }
723
721
  /** The form under the list, for a new account or `account`. */
724
722
  editAccount(account) {
@@ -745,7 +743,7 @@ let RemoteDesktopSettingsComponent = class RemoteDesktopSettingsComponent {
745
743
  field('name').value = account?.name ?? '';
746
744
  field('username').value = account?.username ?? '';
747
745
  field('domain').value = account?.domain ?? '';
748
- field('password').placeholder = account ? 'Leave empty to keep the saved one' : 'Optional; asked on the first connection';
746
+ field('password').placeholder = account ? 'Leave empty to keep the saved password' : 'Optional; requested when connecting';
749
747
  form.querySelector('button[type=submit]').textContent = account ? 'Save' : 'Add';
750
748
  const close = () => {
751
749
  form.remove();
@@ -762,7 +760,7 @@ let RemoteDesktopSettingsComponent = class RemoteDesktopSettingsComponent {
762
760
  const username = field('username').value.trim();
763
761
  const name = field('name').value.trim();
764
762
  if (!username) {
765
- error.textContent = 'Give it a user name.';
763
+ error.textContent = 'Enter a user name.';
766
764
  field('username').focus();
767
765
  return;
768
766
  }
@@ -999,7 +997,7 @@ let RemoteDesktopSettingsComponent = class RemoteDesktopSettingsComponent {
999
997
  ]);
1000
998
  }), this.empty(entries.length
1001
999
  ? 'A forgotten certificate is asked about again on the next connection, unless a certificate authority vouches for it; one behind an SSH host is remembered again without asking.'
1002
- : 'None yet. A desktop connected to directly, and a gateway, are asked about the first time unless a certificate authority vouches for them; a desktop behind an SSH host is remembered on first use. Each is checked every time after. GNOME desktops only accept the one the plugin made.'));
1000
+ : 'No saved certificates.'));
1003
1001
  }
1004
1002
  renderSessions() {
1005
1003
  const list = this.root.querySelector('[data-list=sessions]');
@@ -1009,23 +1007,19 @@ let RemoteDesktopSettingsComponent = class RemoteDesktopSettingsComponent {
1009
1007
  const sessions = this.desktop.sessionSummaries();
1010
1008
  list.replaceChildren(...sessions.map(s => this.row((0, help_1.esc)(s.name), `${s.where ? `via ${(0, help_1.esc)(s.where)} · ` : ''}${(0, help_1.esc)(s.state)}`, [
1011
1009
  { label: 'Copy log', run: () => this.copyLog(s.name, s.log) },
1012
- ])), ...sessions.length ? [] : [this.empty('No desktop is open. Each open desktop\'s connection log can be copied from here, for a bug report.')]);
1010
+ ])), ...sessions.length ? [] : [this.empty('No desktops are open.')]);
1013
1011
  }
1014
1012
  copyLog(name, log) {
1015
- const version = (() => {
1016
- try {
1017
- return require('../package.json').version;
1018
- }
1019
- catch {
1020
- return '?';
1021
- }
1022
- })();
1023
- this.platform.setClipboard({ text: [`tabby-rdp ${version} on ${process.platform}: ${name}`, ...log].join('\n') });
1013
+ const version = (0, updates_1.installedVersion)();
1014
+ this.platform.setClipboard({ text: [`tabby-rdp ${version} on ${process.platform}: ${name}`, ...this.compat.diagnosticLines(), ...log].join('\n') });
1024
1015
  }
1025
1016
  showTab(id) {
1026
1017
  lastTab = TABS.some(t => t.id === id) ? id : TABS[0].id;
1027
1018
  this.root.querySelectorAll('[data-tab]').forEach(el => el.classList.toggle('active', el.dataset.tab === lastTab));
1028
1019
  this.root.querySelectorAll('[data-body]').forEach(el => { el.hidden = el.dataset.body !== lastTab; });
1020
+ if (lastTab === 'updates') {
1021
+ this.injector.get(updates_1.UpdateCheck).refresh();
1022
+ }
1029
1023
  }
1030
1024
  /** Shows the tab with a section (and opens a troubleshooting entry), with a short highlight. */
1031
1025
  reveal(topic, entry) {
@@ -1170,5 +1164,6 @@ exports.RemoteDesktopSettingsComponent = RemoteDesktopSettingsComponent = __deco
1170
1164
  tabby_core_1.PlatformService,
1171
1165
  core_1.Injector,
1172
1166
  core_1.NgZone,
1173
- tabby_core_1.HotkeysService])
1167
+ tabby_core_1.HotkeysService,
1168
+ hostCompat_1.HostCompatibility])
1174
1169
  ], RemoteDesktopSettingsComponent);