tabby-rdp 0.1.0 → 0.2.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 ADDED
@@ -0,0 +1,36 @@
1
+ # Changelog
2
+
3
+ ## 0.2.1
4
+
5
+ The 0.2.0 release, published again: npm never released 0.2.0 (it stayed in a staged state), and a version number
6
+ can't be reused there.
7
+
8
+ ## 0.2.0
9
+
10
+ **New**
11
+ - Windows desktops, and other desktops an SSH host can reach, next to the host's own: added and removed from the
12
+ menus (**Add a desktop behind \<host\>…**, **Remove a desktop**), with a sign-in form and an optional keychain entry.
13
+ - Files both ways through the clipboard: drop files on the desktop (or **Send files to the remote desktop…**) and
14
+ paste them there; files copied on the remote offer **Save to Downloads**.
15
+ - Sound from the remote desktop.
16
+ - Mac shortcuts on macOS (on by default): ⌘ acts as Ctrl on the remote, and a ⌘ tap is the Windows key.
17
+ - Reconnecting: a dropped connection is noticed and reconnected by itself once SSH is back ("Reconnect SSH" when SSH
18
+ is down); ended or failed desktops offer Reconnect / Try again.
19
+
20
+ **Fixed**
21
+ - While a desktop covered a pane, Tabby's shortcuts acted on the terminal underneath: ⌘V pasted into the hidden
22
+ console, ⌘W closed the tab. Only switching tabs and returning to the console stay Tabby's now.
23
+ - Clipboard: IronRDP's component signaled `ready` before setting up the clipboard, so connections got none.
24
+ - A desktop froze instead of noticing that its SSH connection was gone.
25
+ - Retina: GNOME Remote Desktop could reset the scale right after a resize.
26
+ - Windows' self-signed RDP certificate failed TLS in Tabby; such desktops now use TLS 1.2 with RSA key exchange.
27
+
28
+ **Changed**
29
+ - The remote folder is now `~/.local/share/tabby-rdp` (was `tabby-remote-desktop`); existing installs are moved over
30
+ on the next connection.
31
+ - IronRDP is built from a series of focused patches (`ironrdp/patches`).
32
+
33
+ ## 0.1.0
34
+
35
+ First release: GNOME desktops in SSH tabs and in local terminals running `ssh`, one desktop per account, live resize
36
+ and Retina, text and image clipboard, and `desk` (opt-in) with the trd-pty shared session.
package/README.md CHANGED
@@ -1,150 +1,213 @@
1
- # tabby-rdp
1
+ <div align="center">
2
2
 
3
- Remote desktop in Tabby (RDP, powered by IronRDP): shows a remote Linux desktop in a Tabby tab, using RDP through the SSH tab's own connection. Tabby opens nothing on the network: it reaches the remote through an SSH `direct-tcpip` channel to the remote's loopback port 3389.
3
+ <img src="docs/images/logo.svg" width="96" height="96" alt="">
4
4
 
5
- Status: prototype. It works end to end against GNOME Remote Desktop 46 in headless mode (Ubuntu 24.04), and against Windows 11 through an SSH host.
5
+ # tabby-rdp
6
6
 
7
- ## Use
7
+ **Remote desktops in your Tabby SSH tabs.**<br>
8
+ Linux (GNOME) and Windows, through the SSH connection you already have.
8
9
 
9
- Works in **SSH tabs** (Tabby SSH profiles) and in **plain terminals where you typed `ssh …`**. For the latter, the plugin spots the `ssh` process under the shell, reads its command line (`ps`), keeps the connection-relevant options (`-p -i -J -l -F -o …`, not forwards or TTY flags), and opens its *own* non-interactive connections with the system `ssh` (`BatchMode=yes`): `ssh -W 127.0.0.1:3389` for the desktop and `ssh … sh -s` for setup. So that host needs key/agent auth (or a ControlMaster). A password-only login in the terminal can't be reused, and the desktop says so. Accounts are compared as `user@hostname:port` resolved with `ssh -G`, so a profile tab and a plain `ssh` to the same machine share one desktop. Detection checks the active tab every 2 seconds; not on Windows.
10
+ [![npm](https://img.shields.io/npm/v/tabby-rdp?color=3b82f6)](https://www.npmjs.com/package/tabby-rdp)
11
+ [![License: MIT](https://img.shields.io/badge/license-MIT-3b82f6)](LICENSE)
12
+ [![Tabby plugin](https://img.shields.io/badge/Tabby-plugin-6366f1)](https://tabby.sh)
10
13
 
11
- The desktop is a layer over the terminal. Every entry point switches between desktop and console. The RDP session keeps running in the background until you disconnect or close the tab.
14
+ <img src="docs/images/desktop.png" width="860" alt="A GNOME desktop in a Tabby SSH tab">
12
15
 
13
- - **Hotkey:** `⌘⇧G` (`Ctrl+Shift+G` on Windows/Linux). It also works while the desktop has focus, and can be rebound in Settings › Hotkeys.
14
- - **Toolbar:** a **Desktop** button in the SSH tab's floating toolbar, next to Reconnect / SFTP / Ports.
15
- - **Right-click menus:** in the terminal and on the tab header: *Open / Show remote desktop*, *Back to console*, *Disconnect remote desktop*.
16
- - **Tabby's header:** left of the settings gear, shown while the active tab is an SSH tab (or a terminal running `ssh`). A desktop/console switch, plus **Disconnect** when a desktop is open. Nothing is drawn over the remote desktop itself.
16
+ </div>
17
17
 
18
- **Clipboard:** copy on the Mac, paste on the remote desktop, and the other way around (text and images).
18
+ ---
19
19
 
20
- **One desktop per remote account (`user@host:port`).** If another tab already has that desktop open, any entry point switches to that tab and shows its desktop. It doesn't connect a second client, which in GNOME's headless mode would only get an extra, empty monitor.
20
+ tabby-rdp is a plugin for the [Tabby](https://tabby.sh) terminal. Press a key in an SSH tab and the machine's desktop
21
+ appears in its place; press it again and you're back at the shell. Everything travels inside the SSH connection:
22
+ nothing is opened on the network, and nothing has to be installed on the remote machine beforehand.
21
23
 
22
- ### Settings
24
+ - **Linux desktops without a monitor or a login.** The plugin starts a headless GNOME session for your SSH user with
25
+ GNOME Remote Desktop, on the fly, without root. A server in a rack works as well as a workstation.
26
+ - **Windows desktops** behind any SSH host, such as a VM on your build server.
27
+ - **A proper desktop experience:** clipboard and files both ways, sound, live resize to the pane, sharp Retina
28
+ rendering, Mac keyboard shortcuts, and automatic reconnects after sleep.
29
+ - **`desk`:** type `desk` in the console and the same shell, with its history and running programs, moves into a
30
+ terminal on the desktop.
23
31
 
24
- Right-click the header's desktop button, or open **Remote desktop settings** in the terminal or tab right-click menu. The settings are stored in Tabby's config under `remoteDesktop`:
32
+ The remote desktop itself is [IronRDP](https://github.com/Devolutions/IronRDP)'s web client, running inside Tabby.
33
+ Making it work with GNOME took fixes in IronRDP, which we contribute upstream ([below](#built-on-ironrdp)).
25
34
 
26
- | Setting | Values |
27
- |---|---|
28
- | When the pane is resized (`resize`) | **Resize the remote desktop to fit** (`live`, default): the remote monitor changes resolution in place (RDP display control; grd answers with an EGFX ResetGraphics). **Reconnect at the new size** (`reconnect`). **Keep the resolution** (`off`): scaled to fit. |
29
- | Sharpness (`sharpness`) | **Standard** (`standard`, default): one remote pixel per screen point. **Retina** (`retina`): device pixels, with GNOME's scale set to match through Mutter's DisplayConfig (temporarily), so it's sharper at the same size. |
30
- | Bring the console along with `desk` (`desk`) | Off by default. When on, SSH logins on remotes where you open a desktop run in a shareable session, so `desk` works ([below](#desk-bring-the-console-to-the-desktop)). Takes effect on the next desktop connect to each remote. |
31
- | Session backend (`sessionBackend`) | `native` (default) or `tmux`, for `desk`. Config file only. |
35
+ ## Contents
32
36
 
33
- Resizes are debounced (300 ms). A desktop that is hidden catches up when shown again.
37
+ - [Install](#install)
38
+ - [Use](#use)
39
+ - [Features](#features)
40
+ - [Windows and other desktops behind a host](#windows-and-other-desktops-behind-a-host)
41
+ - [desk: the console on the desktop](#desk-the-console-on-the-desktop)
42
+ - [Settings](#settings)
43
+ - [Requirements](#requirements)
44
+ - [Built on IronRDP](#built-on-ironrdp)
45
+ - [Development](#development)
46
+ - [Limitations](#limitations)
47
+ - [License](#license)
34
48
 
35
- **Keyboard focus:** IronRDP only sends keys while its element has focus, and Tabby re-focuses the terminal whenever a pane gets focus. So while the desktop shows, the terminal's input textarea is disabled (nothing can type into the shell underneath), and the desktop takes focus back on pane focus and on click.
49
+ ## Install
36
50
 
37
- ## Desktops behind a host (Windows VMs)
51
+ In Tabby: **Settings › Plugins**, search for **tabby-rdp**, install, and restart Tabby.
38
52
 
39
- An SSH host can also lead to other desktops it can reach, such as a Windows VM whose RDP port it forwards. Add one with **Add a desktop behind <host>…** in that host's menus (or the header button's right-click menu): name, address as seen from the host, type, and optionally the user name. It's saved and opened right away. **Remote desktop settings › Remove a desktop** takes one out again, along with its saved account. In Tabby's config file they look like this:
53
+ Or from a terminal, into Tabby's plugin folder (on macOS `~/Library/Application Support/tabby/plugins`, on Linux
54
+ `~/.config/tabby/plugins`, on Windows `%APPDATA%\tabby\plugins`):
40
55
 
41
- ```yaml
42
- remoteDesktop:
43
- desktops:
44
- - name: Windows VM
45
- via: myhost # the SSH host: alias, hostname, user@hostname or user@hostname:port
46
- host: 127.0.0.1 # the RDP server as seen from that host
47
- port: 13389
48
- kind: windows # or gnome (an RDP server that needs the graphics pipeline)
49
- username: alice # optional; DOMAIN\user works too
56
+ ```sh
57
+ npm install tabby-rdp
50
58
  ```
51
59
 
52
- The host's menus then offer **Open Windows VM** next to its own desktop, and right-clicking the header button lists them too. A pane shows one desktop at a time: opening the other one replaces it. The hotkey, header and toolbar buttons toggle the pane's current desktop, or the one last used for that host.
60
+ ## Use
61
+
62
+ Open an SSH tab to a Linux machine with GNOME, and:
53
63
 
54
- - **Tunnel:** the same SSH connection carries it (`direct-tcpip` to `host:port` from the SSH host). Nothing is opened on the network, and nothing is installed on either machine.
55
- - **Sign-in:** a form in the desktop layer asks for the Windows account. With **Remember in the keychain** it's stored in the macOS keychain (service `tabby-rdp`), through the `keytar` module Tabby already uses for SSH passwords. A wrong password brings the form back and drops the saved one.
56
- - **Graphics:** Windows gets IronRDP's standard bitmap path. The graphics pipeline (EGFX), which GNOME requires, is off for it: IronRDP's web client decodes EGFX without H.264, which Windows would want.
57
- - **Resize and Retina:** Windows follows the pane through display control and honors the scale factor itself, so there is no GNOME scaling step. `desk` only applies to the host's own desktop.
58
- - **TLS:** Windows' self-signed RDP certificate only allows key encipherment, and Electron's TLS library (BoringSSL) enforces that, which rules out every modern key exchange. When a handshake fails for that reason, the proxy repeats the X.224 exchange on a fresh tunnel and uses TLS 1.2 with RSA key exchange for that desktop. The connection is still encrypted, but without forward secrecy. Installing a certificate with the digital-signature usage on the Windows side avoids the fallback.
59
- - **Windows-key shortcuts:** ⌘ is sent as the Windows key. Pressed alone it opens Start, but combinations held with it (⌘R for Win+R) didn't reach Windows in testing.
64
+ - press **⌘⇧G** (Ctrl+Shift+G on Windows and Linux), or
65
+ - click **Desktop** in the SSH tab's toolbar, or the desktop button in Tabby's header, or
66
+ - right-click the terminal or the tab: **Open remote desktop**.
60
67
 
61
- ## `desk`: bring the console to the desktop
68
+ The first connection to a machine takes a few seconds while it prepares GNOME Remote Desktop and a session; later
69
+ ones take well under a second. The same shortcut switches between the desktop and the console, and the desktop keeps
70
+ running in the background until you disconnect or close the tab.
62
71
 
63
- Type `desk` in an SSH console to show that machine's desktop, with a maximized terminal attached to your *exact* console: same shell, history, running programs and scrollback. Typing on either side shows on both. The hotkey or header button takes you back to the console.
72
+ It also works in a plain local terminal where you typed `ssh host` (macOS and Linux), as long as that host accepts
73
+ your key without a password prompt.
64
74
 
65
- `desk` is **off by default**, because it changes how SSH logins start on the remote. Turn it on in **Remote desktop settings**. The next time a desktop opens on a remote, the setup installs `~/.local/share/tabby-remote-desktop/` (`login.sh`, `bin/desk`, `bin/trd-pty`, `tmux.conf`, `backend`) and one guarded line in `~/.bashrc` (and `~/.zshrc`). Interactive SSH logins then run inside a shareable session. The backend comes from the `remoteDesktop.sessionBackend` setting:
75
+ One desktop per account: a second tab to the same `user@host` switches to the tab that has it open, rather than
76
+ connecting again.
66
77
 
67
- - **`native`** (default): [`trd-pty`](docs/native-pty.md), a small per-session PTY owner on a private Unix socket. It passes output through byte for byte and doesn't take Ctrl-B. The PTY size follows the terminal used or focused last.
68
- - **`tmux`**: the earlier setup (`tmux -L tabby`, status bar off, `window-size latest`, `focus-events`, passthrough).
78
+ ## Features
69
79
 
70
- Turning `desk` off removes the `~/.bashrc`/`~/.zshrc` line and those helpers on the next desktop connect (sessions already running keep going). While it's off, typing `desk` where an old hook is still active only shows a notice.
80
+ | | |
81
+ |---|---|
82
+ | **Clipboard** | Copy and paste text and images in both directions. |
83
+ | **Files** | Drop files from Finder onto the desktop, or use **Send files to the remote desktop…**, then paste them in Files or Explorer. Files copied on the remote offer **Save to Downloads**. |
84
+ | **Sound** | The remote desktop's sound plays locally. |
85
+ | **Resize** | The remote resolution follows the pane as you resize the window or split it. Or reconnect at the new size, or keep a fixed resolution. |
86
+ | **Retina** | Optionally renders at device pixels, with the remote's UI scaled to match, for sharp text. |
87
+ | **Keyboard on macOS** | ⌘C, ⌘V, ⌘Z and the rest work as on a Mac; tapping ⌘ alone is the Windows key. Tabby's own shortcuts stay out of the way while a desktop is showing, except switching tabs and returning to the console. |
88
+ | **Reconnecting** | After sleep or a network change, the desktop reconnects by itself once the connection is back. |
89
+ | **Sign-in** | GNOME desktops need none: the plugin manages their credentials. Windows desktops ask for the account once and can remember it in the system keychain. |
71
90
 
72
- If `trd-pty` can't start, a login falls back to tmux, then a plain shell. `TABBY_NO_SESSION=1` skips it for a login. `desk` outside a shared session opens a terminal in the same folder. Sessions that were open before the hook was installed need a reconnect.
91
+ <p align="center"><img src="docs/images/files.png" width="760" alt="A file copied on the remote desktop, offered for saving"></p>
73
92
 
74
- Design, tests, timings and limitations: [docs/native-pty.md](docs/native-pty.md).
93
+ ## Windows and other desktops behind a host
75
94
 
76
- ## How it works
95
+ An SSH host can lead to other desktops it can reach, such as a Windows VM on the same machine. In that host's menu,
96
+ choose **Add a desktop behind \<host\>…** and enter a name, the address as seen from the host, and optionally the
97
+ user name. It opens right away, and from then on appears next to the host's own desktop in its menus. **Remote
98
+ desktop settings › Remove a desktop** removes one, along with its saved password.
77
99
 
100
+ In Tabby's config file, they look like this:
101
+
102
+ ```yaml
103
+ remoteDesktop:
104
+ desktops:
105
+ - name: Windows VM
106
+ via: buildhost # the SSH host: alias, hostname, user@hostname or user@hostname:port
107
+ host: 192.168.122.20 # the RDP server as seen from that host
108
+ port: 3389
109
+ kind: windows # or gnome
110
+ username: alice # optional; DOMAIN\user works too
78
111
  ```
79
- iron-remote-desktop (IronRDP WASM, in the tab)
80
- │ WebSocket, RDCleanPath
81
- ▼
82
- RDCleanPath proxy (src/rdcleanpath.ts, in Tabby's renderer, 127.0.0.1, per-tab token)
83
- │ X.224 request/confirm, then TLS (server cert chain handed back to the client for CredSSP)
84
- ▼
85
- SSH direct-tcpip channel from the SSH tab (src/sshChannelStream.ts) ──► remote 127.0.0.1:3389 (grd)
86
- ```
87
112
 
88
- ## IronRDP
113
+ The connection runs through the same SSH connection; the Windows machine needs Remote Desktop turned on, and nothing
114
+ else.
115
+
116
+ ## desk: the console on the desktop
89
117
 
90
- `vendor/` holds the IronRDP web bundles built from `ironrdp/BASE_COMMIT` plus `ironrdp/gnome-egfx.patch`. Rebuild them with `npm run build:ironrdp`. No IronRDP release works with GNOME yet, so the patch:
118
+ Type `desk` in an SSH console, and the tab switches to that machine's desktop with a terminal attached to the very same
119
+ shell: its scrollback, working directory, jobs and running programs. Type on either side; both show it.
91
120
 
92
- - **Graphics pipeline:** advertises the graphics pipeline (`support_dyn_vc_gfx_protocol: true`) in the web and native clients. grd closes the connection without it.
93
- - **Web client:** registers the EGFX channel (`GraphicsPipelineClient`) when the new `graphicsPipeline(true)` extension asks for it (the plugin does for GNOME, not for Windows). `ActiveStage` already composites its frames.
94
- - **Declined channels:** drops data for a dynamic channel the client declined, instead of ending the session. grd sends `AUDIO_PLAYBACK_DVC` data anyway.
95
- - **Live resize:** the web client follows an EGFX ResetGraphics (grd's answer to a display-control monitor layout). It resizes the canvas and fires the canvas-resized callback, which upstream ignored.
96
- - **Fit to the pane:** the web component sizes itself to its host element, not the whole window, which matters for split panes.
97
- - **Clipboard:** the component fires `ready` only after its clipboard setup. Before, `ready` came first, a client connecting on it had no clipboard callbacks registered yet, and the session never opened the clipboard channel.
121
+ <p align="center">
122
+ <img src="docs/images/console.png" width="49%" alt="A console">
123
+ <img src="docs/images/desk.png" width="49%" alt="The same shell on the desktop, after typing desk">
124
+ </p>
98
125
 
99
- grd's login-screen (system) mode does not work. It hands clients off with a Server Redirection PDU, which IronRDP can't decode yet (Devolutions/IronRDP#139). Use headless per-user mode.
126
+ `desk` is off by default, because it makes interactive SSH logins on that machine start inside a shareable session.
127
+ Turn it on in **Remote desktop settings**. The sessions are handled by a small helper, `trd-pty`, rather than tmux, so
128
+ your terminal's scrollback, colors and shortcuts stay as they are. Turning it off removes it again. Details:
129
+ [docs/desk.md](docs/desk.md).
100
130
 
101
- ## Remote setup (automatic, no root)
131
+ ## Settings
102
132
 
103
- Nothing is copied to the remote ahead of time, and nothing needs root. The desktop comes from GNOME's own RDP server (grd), which needs a password, a TLS certificate and a running headless session before it accepts a client, so on connect the plugin runs a small script over an SSH exec channel (`src/remoteSetup.ts`), as the SSH user. The script:
133
+ Right-click the desktop button in Tabby's header, or open **Remote desktop settings** in a terminal's or tab's menu.
134
+ They are stored in Tabby's config under `remoteDesktop`:
104
135
 
105
- 1. **Stores keys and password:** it creates `~/.local/share/tabby-remote-desktop/` (mode 0700) holding a self-signed TLS key/cert and a random RDP password (0600, generated once).
106
- 2. **Configures grd:** it sets up headless grd (`grdctl --headless rdp …`) with user `tabby` and that password.
107
- 3. **Starts a session if needed:** if no headless shell is running, it starts one: `systemd-run --user --unit=tabby-headless-shell gnome-shell --headless`.
108
- 4. **`desk` (only when that setting is on):** installs the login hook and helpers described [above](#desk-bring-the-console-to-the-desktop); when it's off, removes them.
109
- 5. **Restarts grd only when needed:** it (re)starts `gnome-remote-desktop-headless` only when its configuration changed. grd reads credentials only at startup, and a restart drops live sessions.
136
+ | Setting | Default | |
137
+ |---|---|---|
138
+ | When the pane is resized (`resize`) | Resize the remote desktop to fit (`live`) | Or reconnect at the new size (`reconnect`), or keep the resolution, scaled to fit (`off`). |
139
+ | Sharpness (`sharpness`) | Standard (`standard`) | Retina (`retina`): device pixels, with the remote's scale set to match. |
140
+ | Sound (`sound`) | On | Applies on the next connection. |
141
+ | Mac shortcuts (`macShortcuts`) | On | macOS. Off: ⌘ is the Windows key. |
142
+ | Bring the console along with `desk` (`desk`) | Off | Installs `desk` and a login line on each machine you open a desktop on; applies on the next connection there. |
143
+ | Session backend (`sessionBackend`) | `native` | For `desk`: `native` (trd-pty) or `tmux`. Config file only. |
110
144
 
111
- The remote folder keeps the plugin's earlier name (`tabby-remote-desktop`) so existing setups carry over.
145
+ ## Requirements
112
146
 
113
- The remote needs `gnome-remote-desktop` (≥ 46) and `gnome-shell`. Don't pass `--virtual-monitor` to `gnome-shell`: grd adds a virtual monitor per client, so with a preset monitor the client only sees an empty extended display. For the same reason, **a second client on the same remote user gets a second, empty monitor**, not a mirror.
147
+ - **Tabby 1.0.236 or newer.** Developed and tested on macOS. On Windows, the plugin loads and runs; connecting from a
148
+ Windows Tabby hasn't been tested end to end yet. Linux hasn't been tested.
149
+ - **Linux desktops:** GNOME Shell and GNOME Remote Desktop 46 or newer (Ubuntu 24.04, for example), a systemd user
150
+ session, and SSH access with a key or agent. `desk` also needs `python3` and GNOME Terminal. No root, no display, no
151
+ login screen.
152
+ - **Windows desktops:** Windows 10 or 11 Pro, or Windows Server, with Remote Desktop turned on, reachable from an SSH
153
+ host.
114
154
 
115
- grd has no bind-address setting, so port 3389 is reachable from the LAN (password-protected). The plugin doesn't touch the firewall. For loopback-only access, per machine, as root:
116
- `iptables -I INPUT -p tcp --dport 3389 ! -i lo -j DROP`. Do the same with `ip6tables`.
155
+ Tested with Ubuntu 24.04 (GNOME Remote Desktop 46.3) and Windows 11 Pro.
117
156
 
118
- ## Build and test
157
+ ## Built on IronRDP
119
158
 
120
- The tests drive a sandbox Tabby over the DevTools protocol against real machines: set `RDP_HOST` (and `BASE_PROFILE`, an SSH profile id prefix to copy settings from); their defaults are the author's hosts.
159
+ The desktop is [IronRDP](https://github.com/Devolutions/IronRDP)'s web client: an RDP implementation in Rust, compiled
160
+ to WebAssembly, with a web component. It connects to Windows, but a browser-based client for GNOME Remote Desktop
161
+ needed more: GNOME only speaks RDP's graphics pipeline, which IronRDP's web client didn't offer; it resizes its
162
+ monitor in a way the web client didn't follow; and it depends on details of the sound and dynamic-channel protocols
163
+ that IronRDP got slightly wrong. The web client also had no sound at all.
164
+
165
+ tabby-rdp ships IronRDP with a short series of patches ([ironrdp/](ironrdp)). The fixes go upstream, one by one:
166
+
167
+ | Change | Upstream |
168
+ |---|---|
169
+ | Keep the session when a server sends data on a channel the client declined | [Devolutions/IronRDP#2005](https://github.com/Devolutions/IronRDP/pull/2005) |
170
+ | Fit the web component to its container, not the whole window | [Devolutions/IronRDP#2006](https://github.com/Devolutions/IronRDP/pull/2006) |
171
+ | Set up the clipboard before signaling `ready`, so it works for apps that connect right away | [Devolutions/IronRDP#2018](https://github.com/Devolutions/IronRDP/pull/2018) |
172
+ | Echo the correct size in the sound channel's Training Confirm | Next |
173
+ | The graphics pipeline in the web client, per connection, following its resets | Planned |
174
+ | Sound in the web client | Planned |
175
+
176
+ The aim is to make IronRDP's browser client work well with both Linux and Windows desktops, for everyone who embeds
177
+ it, not just this plugin.
178
+
179
+ ## Development
121
180
 
122
181
  ```sh
123
182
  npm install
124
- npm run build # tsc; no Angular components, so no AOT step
125
- scripts/sandbox.sh # isolated Tabby with this plugin, DevTools on :9334
126
- scripts/install-test-app.sh # "Tabby Test.app" in ~/Applications (Spotlight): opens that copy (settings in ~/tabby-work/tabby-test, or a folder you pass)
127
- node test/e2e.mjs # SSH tabs + a plain terminal running ssh to $RDP_HOST: every entry point, typing, one desktop per account
128
- BACKEND=native node test/desk.mjs # desk acceptance (also BACKEND=tmux)
129
- node test/resize.mjs # live / reconnect / keep resizing, Retina with remote scaling
130
- node test/windows.mjs # a Windows VM behind an SSH host ($RDP_HOST, WIN_HOST/WIN_PORT): menus, sign-in, keychain, typing and clipboard (WinRM checks), resize
131
- node test/clipboard.mjs # Mac ⇄ GNOME clipboard through the desk terminal (saves and restores the Mac clipboard text)
132
- node test/desktops.mjs # "Add a desktop behind…" form, sign-in, keychain, "Remove a desktop" (the host's own grd as the extra)
133
- # trd-pty behavior on a host: scp remote/trd-pty.py test/trd-pty-test.py host:/tmp/ && ssh host python3 /tmp/trd-pty-test.py /tmp/trd-pty.py
183
+ npm run build # TypeScript to dist/
184
+ scripts/sandbox.sh # a separate Tabby with this plugin linked in (macOS), DevTools on port 9334
185
+ npm test # end-to-end suites against a test machine: see test/README.md
186
+ npm run build:ironrdp # rebuild vendor/ from IronRDP and ironrdp/patches: see ironrdp/README.md
134
187
  ```
135
188
 
136
- Build notes:
189
+ - [docs/architecture.md](docs/architecture.md): how the pieces fit, the remote setup, and security notes.
190
+ - [docs/desk.md](docs/desk.md): `desk` and trd-pty.
191
+ - [test/README.md](test/README.md) and [testbed/README.md](testbed/README.md): the tests, and setting up test
192
+ machines for them.
193
+ - [CONTRIBUTING.md](CONTRIBUTING.md).
194
+
195
+ ## Limitations
137
196
 
138
- - **Angular's zone:** anything that watches the DOM or runs on timers stays outside Angular's zone (see `src/header.ts`). Inside it, each callback triggers app-wide change detection. With no tabs open, that looped with the start page and froze Tabby.
139
- - **Background windows:** `scripts/sandbox.sh` disables Chromium's background throttling. Otherwise timers and animation frames stall while the sandbox window is behind others. Tests also run tab actions inside the zone (DevTools evaluation runs outside it), and they close the tabs they open.
197
+ - **One client per account on GNOME.** A second client (another machine, or another Tabby window) gets an extra,
198
+ empty monitor instead of the same screen, which is how GNOME Remote Desktop's headless mode works.
199
+ - **The headless GNOME session is a shell without `gnome-session`:** apps work (Settings, Files, Terminal, Firefox), but
200
+ GNOME's background settings daemons don't run, and X11-only apps don't start.
201
+ - **GNOME's login-screen mode** isn't supported: it hands clients over with a server redirection, which IronRDP can't
202
+ follow yet. The plugin uses per-user headless sessions instead.
203
+ - **GNOME Remote Desktop listens on all interfaces** (password-protected, TLS); it has no setting to listen on
204
+ loopback only. See [docs/architecture.md](docs/architecture.md#security-notes) to restrict it.
205
+ - **Windows-key combinations** such as Win+R don't come through from macOS. Tapping ⌘ for the Windows key does.
206
+ - **Desktops behind a host** can be added and removed from the menus, but not edited there.
140
207
 
141
- - **`author` field:** Tabby silently skips a plugin whose package.json has no `author`.
142
- - **Screenshots:** page screenshots of the sandbox come back white while its window is behind others. The test reads the decoded canvas instead.
208
+ ## License
143
209
 
144
- ## Known gaps
210
+ [MIT](LICENSE). The bundled IronRDP build is MIT or Apache-2.0 ([vendor/IRONRDP-LICENSE](vendor/IRONRDP-LICENSE)).
211
+ Icons in the header are from [Font Awesome Free](https://fontawesome.com) (CC BY 4.0).
145
212
 
146
- - **Other machines:** a client on another machine (or another Tabby window) still gets an empty extra monitor. Within one Tabby window, reuse is handled.
147
- - **Not a full GNOME session:** the headless desktop is `gnome-shell --headless` with a session identity (`XDG_CURRENT_DESKTOP=ubuntu:GNOME`, Ubuntu session mode), without `gnome-session`. So there are no settings daemons, and Xwayland (X11-only apps such as `nvidia-settings`) doesn't start. Settings, Calculator, Firefox, Terminal and DGX Dashboard work.
148
- - **Windows:** ⌘ combinations as Windows-key shortcuts don't get through. Desktops can be added and removed from the menus, not edited (remove and add again, or edit the config file).
149
- - **Clipboard:** text and images (PNG) both ways, through the browser clipboard API in Tabby's window. Files aren't copied.
150
- - **Error handling:** errors show inside the desktop layer. There's no retry except switching to the desktop again, which reconnects.
213
+ tabby-rdp is an independent project, not affiliated with Tabby, Devolutions or the GNOME Project.
package/dist/audio.js ADDED
@@ -0,0 +1,87 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.AudioPlayer = void 0;
4
+ /**
5
+ * Plays a remote desktop's audio (IronRDP's `audioPlayback` extension): blocks of 16-bit PCM, queued
6
+ * back to back on a Web Audio clock. A little lead absorbs network jitter; if playback falls too far
7
+ * behind (a stall, a busy tab), it skips ahead rather than letting the delay grow.
8
+ */
9
+ class AudioPlayer {
10
+ constructor() {
11
+ this.context = null;
12
+ this.gain = null;
13
+ /** Web Audio time at which the next block starts. */
14
+ this.next = 0;
15
+ /** Seconds of audio received and scheduled (for tests and troubleshooting). */
16
+ this.received = 0;
17
+ /** Loudest sample seen, 0..1 (for tests: tells sound from silence). */
18
+ this.peak = 0;
19
+ /** The callback for IronRDP's audioPlayback extension. */
20
+ this.callback = (message) => {
21
+ try {
22
+ if (message?.type === 'wave') {
23
+ this.play(message.sampleRate, message.channels, message.data);
24
+ }
25
+ else if (message?.type === 'volume') {
26
+ this.volume((Number(message.left) + Number(message.right)) / 2);
27
+ }
28
+ else if (message?.type === 'close') {
29
+ this.next = 0;
30
+ }
31
+ }
32
+ catch (e) {
33
+ console.warn('remote desktop audio:', e);
34
+ }
35
+ };
36
+ }
37
+ play(sampleRate, channels, data) {
38
+ const frames = Math.floor(data.byteLength / 2 / channels);
39
+ if (!frames || !channels) {
40
+ return;
41
+ }
42
+ this.context ??= new AudioContext({ latencyHint: 'interactive' });
43
+ if (!this.gain) {
44
+ this.gain = this.context.createGain();
45
+ this.gain.connect(this.context.destination);
46
+ }
47
+ if (this.context.state === 'suspended') {
48
+ this.context.resume().catch(() => null);
49
+ }
50
+ const buffer = this.context.createBuffer(channels, frames, sampleRate);
51
+ const samples = new DataView(data.buffer, data.byteOffset, data.byteLength);
52
+ for (let ch = 0; ch < channels; ch++) {
53
+ const out = buffer.getChannelData(ch);
54
+ for (let i = 0; i < frames; i++) {
55
+ const v = samples.getInt16((i * channels + ch) * 2, true) / 32768;
56
+ out[i] = v;
57
+ if (v > this.peak || -v > this.peak) {
58
+ this.peak = Math.abs(v);
59
+ }
60
+ }
61
+ }
62
+ const now = this.context.currentTime;
63
+ if (this.next < now || this.next > now + AudioPlayer.MAX_AHEAD) {
64
+ this.next = now + AudioPlayer.LEAD;
65
+ }
66
+ const source = this.context.createBufferSource();
67
+ source.buffer = buffer;
68
+ source.connect(this.gain);
69
+ source.start(this.next);
70
+ this.next += buffer.duration;
71
+ this.received += buffer.duration;
72
+ }
73
+ volume(level) {
74
+ if (this.gain && Number.isFinite(level)) {
75
+ this.gain.gain.value = Math.max(0, Math.min(1, level));
76
+ }
77
+ }
78
+ close() {
79
+ this.context?.close().catch(() => null);
80
+ this.context = null;
81
+ this.gain = null;
82
+ }
83
+ }
84
+ exports.AudioPlayer = AudioPlayer;
85
+ AudioPlayer.LEAD = 0.08;
86
+ AudioPlayer.MAX_AHEAD = 0.5;
87
+ //# sourceMappingURL=audio.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"audio.js","sourceRoot":"","sources":["../src/audio.ts"],"names":[],"mappings":"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"}
@@ -19,7 +19,7 @@ KIND=${shq(request.kind)}
19
19
  command -v gnome-terminal >/dev/null 2>&1 || { echo "RD_ERR gnome-terminal is not installed"; exit 0; }
20
20
  W=$(systemctl --user show-environment 2>/dev/null | sed -n 's/^WAYLAND_DISPLAY=//p')
21
21
  export WAYLAND_DISPLAY=${"$"}{W:-wayland-0}
22
- TRD="$HOME/.local/share/tabby-remote-desktop/bin/trd-pty"
22
+ TRD="$HOME/.local/share/tabby-rdp/bin/trd-pty"
23
23
  if [ "$KIND" = native ] && [ -n "$SESS" ] && [ -x "$TRD" ] && [ "$("$TRD" clients "$SESS")" -ge 1 ]; then
24
24
  # The console itself is one attached terminal; a second one means a desktop terminal is already attached.
25
25
  if [ "$("$TRD" clients "$SESS")" -ge 2 ]; then