Internal
Public Access
Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
988637eb39 | ||
|
|
67ea7ea6b3 | ||
|
|
0f821feeed | ||
|
|
34c16cbfa8 |
No files matched your search
+1
-1
@@ -1,6 +1,6 @@
|
||||
cmake_minimum_required(VERSION 3.21)
|
||||
|
||||
project(OrbitHub VERSION 2026.9.16.6 LANGUAGES CXX)
|
||||
project(OrbitHub VERSION 2026.9.28 LANGUAGES CXX)
|
||||
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
|
||||
@@ -15,16 +15,23 @@ OrbitHub is in active development.
|
||||
|
||||
- Milestones completed: M0-M9
|
||||
- Current milestone: Milestone 10 (v1.0 Stabilization)
|
||||
- Latest checkpoint tag: `v2026.9.15`
|
||||
- VNC (M6) covers standard VNC Authentication and no-auth servers; see
|
||||
[docs/PROGRESS.md](docs/PROGRESS.md) for known gaps (Apple Screen
|
||||
Sharing auth, compression encodings, resize, cursor sync, clipboard)
|
||||
- Latest published release: `v2026.9.16`; a `v2026.9.16.6` release is
|
||||
drafted with several post-`.16` fixes and is pending a macOS installer
|
||||
before publishing
|
||||
- VNC (M6) is fully built out: Raw/CopyRect/Hextile/ZRLE/Tight encodings,
|
||||
bidirectional clipboard sync, remote cursor shape sync, and both
|
||||
scale-to-fit and actual-size display modes. Standard VNC Authentication
|
||||
and no-auth servers (TigerVNC, x11vnc, TightVNC, etc.) work end to end.
|
||||
macOS's built-in Screen Sharing uses two undocumented, reverse-engineered
|
||||
authentication schemes that are implemented but not yet confirmed working
|
||||
against a real macOS server — see
|
||||
[docs/PROGRESS.md](docs/PROGRESS.md) for details.
|
||||
|
||||
Progress and milestone details:
|
||||
- [docs/PROGRESS.md](docs/PROGRESS.md)
|
||||
|
||||
Latest release (installers for Windows, Linux, and macOS):
|
||||
- [v2026.9.15](https://git.darksingularity.org/DarkSingularity/orbithub/releases/tag/v2026.9.15)
|
||||
Latest published release (installers for Windows, Linux, and macOS):
|
||||
- [v2026.9.16](https://git.darksingularity.org/DarkSingularity/orbithub/releases/tag/v2026.9.16)
|
||||
|
||||
User Guide:
|
||||
- [docs/USER_GUIDE.md](docs/USER_GUIDE.md) (also available as a PDF attached to each release, and in-app via `Help -> User Guide`)
|
||||
@@ -50,6 +57,8 @@ An embedded RDP session in a tab.
|
||||
- SQLite-backed profile storage
|
||||
- Create, edit, delete profiles
|
||||
- Protocol-aware profile validation (SSH/RDP/VNC)
|
||||
- Username is optional on any protocol's profile — if left blank, OrbitHub
|
||||
asks for one inline at connect time instead of requiring it up front
|
||||
- Profile search and sorting
|
||||
- Tags support
|
||||
- Folder/subfolder support
|
||||
@@ -58,13 +67,20 @@ An embedded RDP session in a tab.
|
||||
- New Folder
|
||||
- New Connection
|
||||
- Drag-and-drop profile moves between folders with persistence
|
||||
- Import/Export of the whole profile list as JSON, and one-way import from
|
||||
mRemoteNG connection XML files (passwords are never imported)
|
||||
|
||||
### Session Experience
|
||||
|
||||
- Multi-tab session window
|
||||
- Auto-connect on tab open
|
||||
- Disconnect on tab close
|
||||
- Session state indicators on tabs
|
||||
- Session state indicators on tabs, colored distinctly per state
|
||||
(connecting/connected/disconnected/failed)
|
||||
- A tab awaiting a username/password prompt is clearly marked — its title
|
||||
gets a "(Needs input)" suffix and its tab color changes — even when it
|
||||
isn't the tab currently in view, and the inline prompt itself uses a
|
||||
solid highlighted banner rather than blending into the rest of the tab
|
||||
- Timestamped event log with filtering and export
|
||||
|
||||
### SSH
|
||||
@@ -82,14 +98,36 @@ An embedded RDP session in a tab.
|
||||
- Domain-aware authentication support
|
||||
- RDP security/performance profile options
|
||||
|
||||
### VNC
|
||||
|
||||
- Embedded in-window VNC rendering surface (no external launcher)
|
||||
- Raw, CopyRect, Hextile, ZRLE, and Tight rectangle encodings
|
||||
- Bidirectional clipboard sync
|
||||
- Remote cursor shape sync
|
||||
- Scale-to-fit and actual-size (scrollable) display modes, toggled
|
||||
per-tab and remembered across sessions
|
||||
- Standard VNC Authentication and no-auth servers (TigerVNC, x11vnc,
|
||||
TightVNC, etc.)
|
||||
- No dynamic remote-desktop resizing (VNC has no real equivalent of RDP's
|
||||
MS-RDPEDISP)
|
||||
- macOS Screen Sharing's two undocumented Apple auth schemes are
|
||||
implemented but not yet confirmed working against a real macOS server —
|
||||
see [docs/PROGRESS.md](docs/PROGRESS.md)
|
||||
|
||||
### App UX
|
||||
|
||||
- App icon and themed About dialog
|
||||
- In-app User Guide (`Help -> User Guide`), matching
|
||||
[docs/USER_GUIDE.md](docs/USER_GUIDE.md)
|
||||
- `File` menu:
|
||||
- New Profile
|
||||
- New Folder
|
||||
- Import Profiles...
|
||||
- Export Profiles...
|
||||
- Import from mRemoteNG...
|
||||
- Quit
|
||||
- `Help` menu:
|
||||
- User Guide
|
||||
- About OrbitHub
|
||||
|
||||
## Build and Run
|
||||
@@ -140,6 +178,9 @@ Core dependencies:
|
||||
- Qt 6 (Widgets, SQL)
|
||||
- CMake 3.21+
|
||||
- C++17 toolchain
|
||||
- OpenSSL (RDP/VNC transport security and Apple VNC auth)
|
||||
- zlib (VNC's ZRLE and Tight encodings)
|
||||
- libjpeg-turbo (VNC's Tight encoding's JPEG sub-mode)
|
||||
|
||||
Protocol/runtime dependencies:
|
||||
- SSH client (`ssh`) available on `PATH` for SSH sessions
|
||||
@@ -176,12 +217,19 @@ See in-app `Help -> About OrbitHub` for license links and third-party inventory.
|
||||
|
||||
- `src/` - application source code
|
||||
- `docs/` - build guide, spec, and progress tracking
|
||||
- `packaging/` - per-platform installer/package build scripts
|
||||
- `tests/` - CTest unit/integration tests and fixtures
|
||||
- `tools/` - standalone build-time tools (e.g. the User Guide PDF generator)
|
||||
- `third_party/` - vendored third-party dependencies
|
||||
- `build/` - local build output (generated)
|
||||
- `dist/` - packaged installer/deb/flatpak/dmg output (generated)
|
||||
|
||||
## Notes
|
||||
|
||||
- Passwords are requested at connect time and are not stored in the profile database.
|
||||
- A profile's username is optional for every protocol; if left blank,
|
||||
OrbitHub asks for one inline the first time you connect that profile.
|
||||
- VNC support covers standard VNC Authentication and no-auth servers (e.g. TigerVNC, x11vnc,
|
||||
TightVNC); it doesn't yet reach macOS's built-in Screen Sharing server, which uses a different
|
||||
authentication scheme (see docs/PROGRESS.md, Milestone 6).
|
||||
TightVNC). macOS's built-in Screen Sharing server uses two undocumented, reverse-engineered
|
||||
authentication schemes that are implemented but not yet confirmed working against a real
|
||||
macOS server (see docs/PROGRESS.md, Milestone 6).
|
||||
@@ -226,6 +226,27 @@ Delivered:
|
||||
dark mode -- its perceived luminance was well below the four existing
|
||||
state colors -- and was replaced with `#ab47bc`, tuned to roughly
|
||||
match their visibility
|
||||
- Clipboard sync (RDP/VNC) randomly stopping, reported on Windows and
|
||||
Linux: (1) `SessionTab` de-duplicated local copies against the last text
|
||||
it had synced, with no expiry, so once one send was lost (channel not
|
||||
ready, remote clipboard changed to something it never saw) re-copying
|
||||
the same text did nothing indefinitely -- it now suppresses only the
|
||||
echo of its own clipboard write, for one second; (2) every open tab
|
||||
listened to the system clipboard, so a background session re-announcing
|
||||
its stale clipboard overwrote what was just copied and one tab's update
|
||||
was forwarded to every other connected session -- only the visible tab
|
||||
syncs now; (3) RDP answered Monitor Ready with capabilities but never
|
||||
the initial Format List MS-RDPECLIP requires, and the server can send
|
||||
Monitor Ready again mid-session (e.g. rdpclip.exe restarting), leaving
|
||||
sync dead until reconnect -- it now always sends one; announce failures
|
||||
and not-yet-ready-channel copies are logged to the session event log
|
||||
(with a line per copy sent/received) instead of silently dropped. Not
|
||||
reproduced locally; needs confirmation from real use
|
||||
- Follow-up to the clipboard fix above: restricting sync to the visible
|
||||
tab left a tab that had been in the background with a stale remote
|
||||
clipboard when switched to (with two RDP sessions only the one last
|
||||
copied in worked). A tab now pushes the current local clipboard to its
|
||||
remote whenever it becomes visible
|
||||
- Robustness fix: an unrecognized `FramebufferUpdate` rectangle encoding
|
||||
used to abort the connection generically; `kAnnouncedEncodings` is now
|
||||
the single source of truth for what `SetEncodings` announces and what
|
||||
@@ -403,6 +424,23 @@ Git:
|
||||
- Release: [v2026.9.14.2](https://git.darksingularity.org/DarkSingularity/orbithub/releases/tag/v2026.9.14.2) — same-day patch fixing RDP display corruption (missing/misplaced taskbar) after resizing the session window (the client never resized its own display buffer for channel-driven RDP resizes)
|
||||
- Release: [v2026.9.15](https://git.darksingularity.org/DarkSingularity/orbithub/releases/tag/v2026.9.15) — adds Import/Export for profile lists (File menu)
|
||||
|
||||
- Quick Connect (issue #23): a `File > Quick Connect...` action and a
|
||||
matching button on the Profiles tab open a connection without saving a
|
||||
profile first. Reuses `ProfileDialog` via a new `setQuickConnectMode()`
|
||||
toggle (Name/Tags/Folder hidden, Name validation skipped) rather than a
|
||||
second dialog class, so all protocol-specific field logic
|
||||
(`refreshAuthFields()`) stays in one place. The tab title is derived
|
||||
from host/username/protocol (e.g. `user@host`) purely for display --
|
||||
never written to the database. A Quick Connect tab's right-click menu
|
||||
gets a "Save as Profile..." action (absent on normal profile-backed
|
||||
tabs) that opens the same dialog pre-filled to persist it afterward.
|
||||
Detecting a Quick Connect tab needed no new state: `ProfileDialog::
|
||||
profile()` already always returns `id = -1` for an unsaved profile,
|
||||
and Quick Connect is the only path today that hands such a profile to
|
||||
`SessionWindow::addSessionTab()`. GUI-only change with no automated
|
||||
test coverage (consistent with `ProfileDialog` never having had any);
|
||||
verified manually by running the built binary
|
||||
|
||||
## Milestone 10 - v1.0 Stabilization
|
||||
|
||||
Status: Planned
|
||||
|
||||
+113
-15
@@ -1,16 +1,19 @@
|
||||
## Introduction
|
||||
|
||||
OrbitHub is a native desktop application for organizing connection profiles
|
||||
and launching SSH and RDP sessions from one place, in a single tabbed
|
||||
and launching SSH, RDP, and VNC sessions from one place, in a single tabbed
|
||||
window. It runs on Windows, Linux, and macOS.
|
||||
|
||||
This guide covers everyday use: creating and organizing profiles,
|
||||
connecting over SSH and RDP, managing active sessions, and what to do when
|
||||
something goes wrong. It does not cover installation or building from
|
||||
connecting over SSH, RDP, and VNC, managing active sessions, and what to do
|
||||
when something goes wrong. It does not cover installation or building from
|
||||
source — see the project's `README.md` and `docs/BUILDING.md` for that.
|
||||
|
||||
VNC support is planned but not yet implemented; profiles can be tagged for
|
||||
it, but connecting will show an "unsupported protocol" message for now.
|
||||
VNC support covers standard VNC Authentication and no-auth servers
|
||||
(TigerVNC, x11vnc, TightVNC, and similar). macOS's built-in Screen Sharing
|
||||
server uses two undocumented, reverse-engineered authentication schemes
|
||||
that are implemented but not yet confirmed working end to end against a
|
||||
real macOS server — see [Connecting via VNC](#connecting-via-vnc).
|
||||
|
||||
## Getting Started
|
||||
|
||||
@@ -43,15 +46,19 @@ A profile stores everything needed to reach one remote host. Open **New**
|
||||
|
||||
- **Name** — a label for the profile; shown on its tab and in the list.
|
||||
- **Host** — hostname or IP address.
|
||||
- **Port** — defaults to `22` for SSH, `3389` for RDP.
|
||||
- **Username** — the account to log in as.
|
||||
- **Port** — defaults to `22` for SSH, `3389` for RDP, `5900` for VNC.
|
||||
- **Username** — the account to log in as. Optional for every protocol —
|
||||
leave it blank and OrbitHub asks for one inline the first time you
|
||||
connect that profile, the same way it already asks for a password. (VNC
|
||||
servers mostly ignore username entirely; macOS Screen Sharing is the
|
||||
exception and requires one — see
|
||||
[Connecting via VNC](#connecting-via-vnc).)
|
||||
- **Domain** — Windows domain for RDP logins (leave blank for local/
|
||||
workgroup accounts or SSH profiles).
|
||||
workgroup accounts, SSH, or VNC profiles).
|
||||
- **Tags** — free-form, comma-separated labels for filtering and grouping
|
||||
(e.g. `prod, linux, db`).
|
||||
- **Folder** — where the profile lives in the Folders view.
|
||||
- **Protocol** — `SSH`, `RDP`, or `VNC` (VNC is accepted but not yet
|
||||
connectable — see Introduction).
|
||||
- **Protocol** — `SSH`, `RDP`, or `VNC`.
|
||||
|
||||
Fields below Protocol change based on what you pick:
|
||||
|
||||
@@ -63,6 +70,9 @@ policy means.
|
||||
**RDP**: **RDP Security** and **RDP Performance** — see
|
||||
[Connecting via RDP](#connecting-via-rdp).
|
||||
|
||||
**VNC** has no protocol-specific fields beyond Host/Port/Username above —
|
||||
see [Connecting via VNC](#connecting-via-vnc).
|
||||
|
||||
Passwords are never saved in the profile — OrbitHub asks for them each
|
||||
time you connect (unless you've set up private-key SSH auth, which needs
|
||||
no password to be entered per-connection if the key itself has none).
|
||||
@@ -85,6 +95,24 @@ Combine both with the **Search** box (matches name, host, folder path, or
|
||||
tags) and the **Sort** control (Name / Protocol / Host) to find what you
|
||||
need quickly even with a large profile list.
|
||||
|
||||
## Importing and Exporting Profiles
|
||||
|
||||
The **File** menu has three options for moving profiles in or out of
|
||||
OrbitHub:
|
||||
|
||||
- **Export Profiles...** — saves your entire profile list, including
|
||||
folder structure and tags, to a JSON file. Passwords are never included,
|
||||
since OrbitHub never stores them in the first place.
|
||||
- **Import Profiles...** — reads that same JSON format back in, recreating
|
||||
folders and profiles. Existing profiles aren't touched; entries missing
|
||||
a name or host are skipped and reported in the summary.
|
||||
- **Import from mRemoteNG...** — reads an mRemoteNG connections `.xml`
|
||||
file and creates equivalent OrbitHub profiles. Passwords are never
|
||||
imported — you'll be prompted the first time you connect each imported
|
||||
profile, same as a new one. Connections using a protocol OrbitHub
|
||||
doesn't support are skipped and listed in the import summary. This is a
|
||||
one-way conversion; there's no export back to mRemoteNG's format.
|
||||
|
||||
## Connecting via SSH
|
||||
|
||||
Double-clicking an SSH profile opens a new tab with an embedded, fully
|
||||
@@ -93,6 +121,8 @@ emulator. A **theme** selector lets you switch between `Dark`, `Light`,
|
||||
and `Solarized Dark`; your choice is remembered for future sessions.
|
||||
|
||||
**Authentication.** Set in the profile itself:
|
||||
- **Username** — if left blank in the profile, OrbitHub asks for one
|
||||
inline the first time you connect, right alongside the password prompt.
|
||||
- **Password** — OrbitHub prompts for a password each time you connect.
|
||||
It is never stored.
|
||||
- **Private Key** — point at a key file (via the profile's Browse button);
|
||||
@@ -105,7 +135,7 @@ silently impersonating a host you've connected to before:
|
||||
| Policy | Behavior |
|
||||
|---|---|
|
||||
| `Ask` | Prompts you to confirm trust the first time a host is seen, and on any later change. Recommended default. |
|
||||
| `Accept-new` | Silently trusts a host the first time it's seen, but still stops and warns if a previously-trusted host's key later changes. |
|
||||
| `Accept New` | Silently trusts a host the first time it's seen, but still stops and warns if a previously-trusted host's key later changes. |
|
||||
| `Strict` | Never trusts an unknown host automatically — the connection fails until you've manually confirmed the host key some other way. |
|
||||
| `Ignore` | Skips host-key checking entirely. Only use this for throwaway/test environments — it removes protection against on-path attacks. |
|
||||
|
||||
@@ -123,7 +153,8 @@ window renegotiates the remote resolution to match. Clipboard content
|
||||
syncs between your machine and the remote session automatically.
|
||||
|
||||
**Authentication** uses the profile's Username/Domain fields; OrbitHub
|
||||
prompts for the password at connect time.
|
||||
prompts for the password at connect time, and for a username too if the
|
||||
profile's own Username field was left blank.
|
||||
|
||||
**RDP Security** controls which transport-security layer is used to
|
||||
negotiate the connection:
|
||||
@@ -151,6 +182,51 @@ expected — you rotated the server's certificate yourself — reconnecting
|
||||
after clearing the old entry from FreeRDP's certificate store will trust
|
||||
the new one.
|
||||
|
||||
## Connecting via VNC
|
||||
|
||||
VNC sessions render in an embedded display surface inside the tab, like
|
||||
RDP — no external VNC client window opens.
|
||||
|
||||
**Authentication.** VNC servers don't have a fixed username/password
|
||||
convention the way SSH and RDP do, so OrbitHub always prompts for a
|
||||
password at connect time (leave it blank if the server doesn't require
|
||||
one — no-auth servers exist and are supported). Most VNC servers ignore
|
||||
username entirely; the one common exception is **macOS's built-in Screen
|
||||
Sharing**, which does require one — see the Apple Screen Sharing note
|
||||
below if you're connecting to a Mac.
|
||||
|
||||
**Display mode.** Right-click a VNC session's tab to choose:
|
||||
- **Scale to Fit** (default) — the whole remote screen is scaled to fit
|
||||
the tab, like RDP.
|
||||
- **Actual Size (Scrollbars)** — renders at the remote's native pixel
|
||||
size, with scrollbars for panning. Useful when scaling would make small
|
||||
text illegible.
|
||||
|
||||
Your choice is remembered across sessions. Unlike RDP, VNC has no way to
|
||||
renegotiate the remote screen's resolution to match your window size —
|
||||
resizing the OrbitHub window changes how much of a scaled-down remote
|
||||
screen you can see, not the remote resolution itself.
|
||||
|
||||
**Clipboard and cursor.** Clipboard content syncs between your machine and
|
||||
the remote session in both directions, and the remote's cursor shape (not
|
||||
just position) is mirrored locally when the server supports it.
|
||||
|
||||
**Encodings.** OrbitHub negotiates whichever of Raw, CopyRect, Hextile,
|
||||
ZRLE, or Tight the server prefers — this is automatic and requires no
|
||||
configuration; more efficient encodings (Tight, ZRLE) simply mean better
|
||||
performance over slower links.
|
||||
|
||||
**Apple Screen Sharing (macOS).** macOS's built-in Screen Sharing server
|
||||
doesn't use a standard, documented VNC authentication method — it uses two
|
||||
undocumented schemes that OrbitHub implements based on reverse-engineering
|
||||
work, but neither is yet confirmed to authenticate successfully against a
|
||||
real macOS server. If you're connecting to a Mac's Screen Sharing and see
|
||||
an authentication failure, this is the likely cause rather than a wrong
|
||||
username or password; check
|
||||
[docs/PROGRESS.md](https://git.darksingularity.org/DarkSingularity/orbithub/blob/main/docs/PROGRESS.md)
|
||||
(Milestone 6) for the current status, or a VNC server you install yourself
|
||||
on the Mac (e.g. TigerVNC) as a workaround in the meantime.
|
||||
|
||||
## Managing Sessions
|
||||
|
||||
Every open connection lives in its own tab in the same window, alongside
|
||||
@@ -158,6 +234,14 @@ the Profiles tab. A tab's title and a colored state indicator show
|
||||
whether it's connecting, connected, disconnected, or failed. Closing a
|
||||
tab disconnects that session; opening a profile again starts a fresh one.
|
||||
|
||||
**When OrbitHub needs a username or password from you**, the tab shows a
|
||||
highlighted prompt bar with a Connect/Cancel pair — this happens for a
|
||||
blank-username SSH/RDP profile, any RDP or SSH password prompt, and any
|
||||
VNC connection (VNC always asks for a password, even if blank). If that
|
||||
prompt appears on a tab you're not currently viewing, the tab's title
|
||||
gets a `(Needs input)` suffix and its color changes so it's easy to spot
|
||||
among several open sessions.
|
||||
|
||||
Each session tab includes a collapsible **event log** beneath the
|
||||
connection surface — a timestamped record of connection state changes,
|
||||
warnings, and errors for that session. Controls above the log let you:
|
||||
@@ -176,8 +260,9 @@ settings screen — they're saved automatically as you use the app:
|
||||
|
||||
- Profile list: search text, view mode (List/Folders), Protocol/Tag
|
||||
filters, and sort order.
|
||||
- Session tabs: terminal theme choice, and whether the events panel is
|
||||
shown or hidden for new tabs.
|
||||
- Session tabs: terminal theme choice, whether the events panel is shown
|
||||
or hidden for new tabs, and VNC display mode (Scale to Fit / Actual
|
||||
Size).
|
||||
|
||||
Profile data itself (names, hosts, tags, folder structure, and so on) is
|
||||
stored in a local SQLite database — see the README for its exact path on
|
||||
@@ -209,10 +294,23 @@ certificate is replaced.
|
||||
|
||||
**SSH: connection hangs at "Ask" waiting for host-key trust** — check
|
||||
that policy's behavior under
|
||||
[Connecting via SSH](#connecting-via-ssh); switching to `Accept-new` avoids
|
||||
[Connecting via SSH](#connecting-via-ssh); switching to `Accept New` avoids
|
||||
the prompt for genuinely new hosts while still protecting against a later
|
||||
key change.
|
||||
|
||||
**VNC: "Authentication or authorization failure"** — for most VNC
|
||||
servers this means a wrong password. If you're connecting to **macOS
|
||||
Screen Sharing** specifically, this is the expected result right now — see
|
||||
the Apple Screen Sharing note under
|
||||
[Connecting via VNC](#connecting-via-vnc).
|
||||
|
||||
**VNC: connection closes immediately with no prompt** — some servers
|
||||
(including macOS Screen Sharing in some configurations) close the
|
||||
connection outright rather than negotiating; confirm the server is
|
||||
actually running and reachable on the profile's port (default `5900`),
|
||||
and that no allowlist on the server side is blocking your account or
|
||||
machine.
|
||||
|
||||
If none of this covers what you're seeing, a session's exported event log
|
||||
(see Managing Sessions) is the most useful thing to include when asking
|
||||
for help or filing an issue.
|
||||
|
||||
+40
-2
@@ -65,7 +65,10 @@ ProfileDialog::ProfileDialog(QWidget* parent)
|
||||
m_rdpSecurityModeInput(new QComboBox(this)),
|
||||
m_rdpPerformanceProfileInput(new QComboBox(this)),
|
||||
m_protocolHint(new QLabel(this)),
|
||||
m_folderHint(new QLabel(this))
|
||||
m_folderHint(new QLabel(this)),
|
||||
m_nameLabel(nullptr),
|
||||
m_tagsLabel(nullptr),
|
||||
m_quickConnectMode(false)
|
||||
{
|
||||
resize(560, 360);
|
||||
|
||||
@@ -145,6 +148,9 @@ ProfileDialog::ProfileDialog(QWidget* parent)
|
||||
form->addRow(QStringLiteral("RDP Security"), m_rdpSecurityModeInput);
|
||||
form->addRow(QStringLiteral("RDP Performance"), m_rdpPerformanceProfileInput);
|
||||
|
||||
m_nameLabel = form->labelForField(m_nameInput);
|
||||
m_tagsLabel = form->labelForField(m_tagsInput);
|
||||
|
||||
auto* note = new QLabel(
|
||||
QStringLiteral("Passwords are requested at connect time and are not stored."),
|
||||
this);
|
||||
@@ -176,6 +182,24 @@ void ProfileDialog::setDefaultFolderPath(const QString& folderPath)
|
||||
refreshAuthFields();
|
||||
}
|
||||
|
||||
void ProfileDialog::setQuickConnectMode(bool enabled)
|
||||
{
|
||||
m_quickConnectMode = enabled;
|
||||
|
||||
const bool showOrganizationalFields = !enabled;
|
||||
if (m_nameLabel != nullptr) {
|
||||
m_nameLabel->setVisible(showOrganizationalFields);
|
||||
}
|
||||
m_nameInput->setVisible(showOrganizationalFields);
|
||||
if (m_tagsLabel != nullptr) {
|
||||
m_tagsLabel->setVisible(showOrganizationalFields);
|
||||
}
|
||||
m_tagsInput->setVisible(showOrganizationalFields);
|
||||
m_folderHint->setVisible(showOrganizationalFields);
|
||||
|
||||
refreshAuthFields();
|
||||
}
|
||||
|
||||
void ProfileDialog::setProfile(const Profile& profile)
|
||||
{
|
||||
m_nameInput->setText(profile.name);
|
||||
@@ -237,12 +261,26 @@ Profile ProfileDialog::profile() const
|
||||
profile.rdpPerformanceProfile = protocol == QStringLiteral("RDP")
|
||||
? m_rdpPerformanceProfileInput->currentText()
|
||||
: QStringLiteral("Balanced");
|
||||
|
||||
if (m_quickConnectMode) {
|
||||
// Name is never entered in Quick Connect mode -- derive something
|
||||
// usable for the session tab's title. Only ever a starting point:
|
||||
// never written to the database directly, and fully editable if
|
||||
// the user later saves this connection as a real profile.
|
||||
profile.name = profile.username.isEmpty()
|
||||
? profile.host
|
||||
: QStringLiteral("%1@%2").arg(profile.username, profile.host);
|
||||
if (protocol != QStringLiteral("SSH")) {
|
||||
profile.name = QStringLiteral("%1 (%2)").arg(profile.name, protocol);
|
||||
}
|
||||
}
|
||||
|
||||
return profile;
|
||||
}
|
||||
|
||||
void ProfileDialog::accept()
|
||||
{
|
||||
if (m_nameInput->text().trimmed().isEmpty()) {
|
||||
if (!m_quickConnectMode && m_nameInput->text().trimmed().isEmpty()) {
|
||||
QMessageBox::warning(this,
|
||||
QStringLiteral("Validation Error"),
|
||||
QStringLiteral("Profile name is required."));
|
||||
|
||||
@@ -21,6 +21,12 @@ public:
|
||||
void setDialogTitle(const QString& title);
|
||||
void setDefaultFolderPath(const QString& folderPath);
|
||||
void setProfile(const Profile& profile);
|
||||
// Hides Name/Tags/Folder and skips the Name-required validation, for
|
||||
// Quick Connect (issue #23) -- a connection opened without ever being
|
||||
// saved as a profile. profile() derives a usable name from
|
||||
// host/username/protocol instead. Everything else (protocol-specific
|
||||
// fields, defaults) behaves identically to a normal New Profile.
|
||||
void setQuickConnectMode(bool enabled);
|
||||
Profile profile() const;
|
||||
|
||||
protected:
|
||||
@@ -42,7 +48,10 @@ private:
|
||||
QComboBox* m_rdpPerformanceProfileInput;
|
||||
QLabel* m_protocolHint;
|
||||
QLabel* m_folderHint;
|
||||
QWidget* m_nameLabel;
|
||||
QWidget* m_tagsLabel;
|
||||
QString m_defaultFolderPath;
|
||||
bool m_quickConnectMode;
|
||||
|
||||
void refreshAuthFields();
|
||||
};
|
||||
|
||||
+24
-2
@@ -151,6 +151,7 @@ ProfilesWindow::ProfilesWindow(QWidget* parent)
|
||||
m_newButton(nullptr),
|
||||
m_editButton(nullptr),
|
||||
m_deleteButton(nullptr),
|
||||
m_quickConnectButton(nullptr),
|
||||
m_repository(std::make_unique<ProfileRepository>())
|
||||
{
|
||||
setupUi();
|
||||
@@ -226,10 +227,12 @@ void ProfilesWindow::setupUi()
|
||||
m_newButton = new QPushButton(QStringLiteral("New"), this);
|
||||
m_editButton = new QPushButton(QStringLiteral("Edit"), this);
|
||||
m_deleteButton = new QPushButton(QStringLiteral("Delete"), this);
|
||||
m_quickConnectButton = new QPushButton(QStringLiteral("Quick Connect"), this);
|
||||
|
||||
buttonRow->addWidget(m_newButton);
|
||||
buttonRow->addWidget(m_editButton);
|
||||
buttonRow->addWidget(m_deleteButton);
|
||||
buttonRow->addWidget(m_quickConnectButton);
|
||||
buttonRow->addStretch();
|
||||
|
||||
auto* filterRow = new QHBoxLayout();
|
||||
@@ -300,6 +303,7 @@ void ProfilesWindow::setupUi()
|
||||
connect(m_newButton, &QPushButton::clicked, this, [this]() { createProfile(); });
|
||||
connect(m_editButton, &QPushButton::clicked, this, [this]() { editSelectedProfile(); });
|
||||
connect(m_deleteButton, &QPushButton::clicked, this, [this]() { deleteSelectedProfile(); });
|
||||
connect(m_quickConnectButton, &QPushButton::clicked, this, [this]() { quickConnect(); });
|
||||
}
|
||||
|
||||
void ProfilesWindow::loadProfiles()
|
||||
@@ -932,7 +936,11 @@ void ProfilesWindow::createProfile(const QString& defaultFolderPath)
|
||||
return;
|
||||
}
|
||||
|
||||
const Profile newProfile = dialog.profile();
|
||||
createProfileFromExisting(dialog.profile());
|
||||
}
|
||||
|
||||
bool ProfilesWindow::createProfileFromExisting(const Profile& newProfile)
|
||||
{
|
||||
if (!newProfile.folderPath.trimmed().isEmpty()) {
|
||||
m_repository->createFolder(newProfile.folderPath);
|
||||
}
|
||||
@@ -944,10 +952,24 @@ void ProfilesWindow::createProfile(const QString& defaultFolderPath)
|
||||
.arg(m_repository->lastError().isEmpty()
|
||||
? QStringLiteral("unknown error")
|
||||
: m_repository->lastError()));
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
loadProfiles();
|
||||
return true;
|
||||
}
|
||||
|
||||
void ProfilesWindow::quickConnect()
|
||||
{
|
||||
ProfileDialog dialog(this);
|
||||
dialog.setDialogTitle(QStringLiteral("Quick Connect"));
|
||||
dialog.setQuickConnectMode(true);
|
||||
|
||||
if (dialog.exec() != QDialog::Accepted) {
|
||||
return;
|
||||
}
|
||||
|
||||
emit connectRequested(dialog.profile());
|
||||
}
|
||||
|
||||
void ProfilesWindow::editSelectedProfile()
|
||||
|
||||
@@ -35,6 +35,18 @@ public:
|
||||
void exportProfiles();
|
||||
void importProfiles();
|
||||
void importFromMRemoteNG();
|
||||
// Opens the New/Edit Profile dialog with Name/Tags/Folder hidden (see
|
||||
// ProfileDialog::setQuickConnectMode()) and, on accept, emits
|
||||
// connectRequested() directly -- never writes to the repository
|
||||
// (issue #23).
|
||||
void quickConnect();
|
||||
// Persists an already-built Profile (e.g. from ProfileDialog::profile())
|
||||
// the same way createProfile() does after its own dialog accepts:
|
||||
// optional folder creation, repository insert with a failure warning,
|
||||
// then refresh. Used by SessionWindow's "Save as Profile..." action so
|
||||
// repository access stays inside ProfilesWindow. Returns true on
|
||||
// success.
|
||||
bool createProfileFromExisting(const Profile& newProfile);
|
||||
|
||||
signals:
|
||||
void connectRequested(const Profile& profile);
|
||||
@@ -49,6 +61,7 @@ private:
|
||||
QPushButton* m_newButton;
|
||||
QPushButton* m_editButton;
|
||||
QPushButton* m_deleteButton;
|
||||
QPushButton* m_quickConnectButton;
|
||||
std::unique_ptr<ProfileRepository> m_repository;
|
||||
std::unordered_map<qint64, Profile> m_profileCache;
|
||||
QString m_pendingTagFilterPreference;
|
||||
|
||||
@@ -2000,6 +2000,10 @@ void RdpSessionBackend::sendClipboardTextToRemote(rdp_freerdp* instance, const Q
|
||||
}
|
||||
|
||||
if (cliprdrContext == nullptr || cliprdrContext->ClientFormatList == nullptr) {
|
||||
// Kept in m_pendingLocalClipboardText and announced once the channel
|
||||
// reports MonitorReady (see onCliprdrMonitorReady()).
|
||||
emit eventLogged(QStringLiteral(
|
||||
"RDP clipboard: channel not ready; copied text will be sent once it is."));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -2012,7 +2016,11 @@ void RdpSessionBackend::sendClipboardTextToRemote(rdp_freerdp* instance, const Q
|
||||
formatList.formats = &format;
|
||||
|
||||
const UINT rc = cliprdrContext->ClientFormatList(cliprdrContext, &formatList);
|
||||
Q_UNUSED(rc);
|
||||
if (rc != CHANNEL_RC_OK) {
|
||||
emit eventLogged(
|
||||
QStringLiteral("RDP clipboard: failed to announce copied text to the server (0x%1).")
|
||||
.arg(rc, 0, 16));
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -2324,6 +2332,29 @@ void RdpSessionBackend::onCliprdrMonitorReady()
|
||||
}
|
||||
|
||||
emit eventLogged(QStringLiteral("RDP clipboard channel ready."));
|
||||
|
||||
QString pendingText;
|
||||
{
|
||||
std::lock_guard<std::mutex> guard(m_cliprdrMutex);
|
||||
pendingText = m_pendingLocalClipboardText;
|
||||
}
|
||||
if (!pendingText.isEmpty()) {
|
||||
sendClipboardTextToRemote(nullptr, pendingText);
|
||||
} else if (cliprdrContext->ClientFormatList != nullptr) {
|
||||
// MS-RDPECLIP requires the client to answer Monitor Ready with its
|
||||
// capabilities *and* an initial Format List (empty is fine); the
|
||||
// server doesn't treat the channel as initialized until it has
|
||||
// both. The server can send Monitor Ready again mid-session -- e.g.
|
||||
// when rdpclip.exe restarts -- and without this the clipboard
|
||||
// stayed dead until the session was reconnected.
|
||||
CLIPRDR_FORMAT_LIST emptyList = {};
|
||||
const UINT rc = cliprdrContext->ClientFormatList(cliprdrContext, &emptyList);
|
||||
if (rc != CHANNEL_RC_OK) {
|
||||
emit eventLogged(
|
||||
QStringLiteral("RDP clipboard: initial format list failed (0x%1).")
|
||||
.arg(rc, 0, 16));
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
+55
-6
@@ -28,6 +28,7 @@
|
||||
#include <QComboBox>
|
||||
#include <QProcessEnvironment>
|
||||
#include <QPushButton>
|
||||
#include <QShowEvent>
|
||||
#include <QScrollArea>
|
||||
#include <QThread>
|
||||
#include <QTimer>
|
||||
@@ -386,6 +387,16 @@ bool SessionTab::awaitingUserInput() const
|
||||
return m_awaitingUserInput;
|
||||
}
|
||||
|
||||
bool SessionTab::isQuickConnect() const
|
||||
{
|
||||
return m_profile.id <= 0;
|
||||
}
|
||||
|
||||
Profile SessionTab::profileForSaving() const
|
||||
{
|
||||
return m_profile;
|
||||
}
|
||||
|
||||
void SessionTab::connectSession()
|
||||
{
|
||||
if (m_state == SessionState::Connecting || m_state == SessionState::Connected) {
|
||||
@@ -778,17 +789,49 @@ void SessionTab::onBackendUsernameRequested(const QString& prompt)
|
||||
|
||||
void SessionTab::onBackendRemoteClipboardTextChanged(const QString& text)
|
||||
{
|
||||
if (text == m_lastSyncedClipboardText) {
|
||||
// Only the tab in front may touch the system clipboard. Every open
|
||||
// RDP/VNC tab listens to it, and a background session re-announcing its
|
||||
// own (stale) clipboard used to silently overwrite what was just copied
|
||||
// locally, and an update applied by one tab was forwarded on to every
|
||||
// other connected tab's remote as well.
|
||||
if (!isVisible() || text.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_lastSyncedClipboardText = text;
|
||||
QApplication::clipboard()->setText(text);
|
||||
QClipboard* clipboard = QApplication::clipboard();
|
||||
if (clipboard->text() == text) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_pendingClipboardEcho = text;
|
||||
m_pendingClipboardEchoMs = QDateTime::currentMSecsSinceEpoch();
|
||||
clipboard->setText(text);
|
||||
appendEvent(QStringLiteral("Clipboard: received %1 characters from remote.").arg(text.size()));
|
||||
}
|
||||
|
||||
void SessionTab::onSystemClipboardChanged()
|
||||
{
|
||||
if (!m_clipboardSyncSupported || m_state != SessionState::Connected) {
|
||||
const QClipboard* clipboard = QApplication::clipboard();
|
||||
const QString text = clipboard->mimeData()->hasText() ? clipboard->text() : QString();
|
||||
|
||||
// Suppress only the change we caused ourselves, and only briefly. The
|
||||
// old "same as last synced text" check had no expiry: once a send was
|
||||
// lost (channel not ready, remote clipboard changed to something we
|
||||
// never saw) copying that same text again did nothing, indefinitely.
|
||||
constexpr qint64 kEchoWindowMs = 1000;
|
||||
const bool isEcho = !m_pendingClipboardEcho.isEmpty() && text == m_pendingClipboardEcho
|
||||
&& QDateTime::currentMSecsSinceEpoch() - m_pendingClipboardEchoMs < kEchoWindowMs;
|
||||
m_pendingClipboardEcho.clear();
|
||||
if (isEcho) {
|
||||
return;
|
||||
}
|
||||
|
||||
pushLocalClipboardToRemote();
|
||||
}
|
||||
|
||||
void SessionTab::pushLocalClipboardToRemote()
|
||||
{
|
||||
if (!m_clipboardSyncSupported || m_state != SessionState::Connected || !isVisible()) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -798,12 +841,18 @@ void SessionTab::onSystemClipboardChanged()
|
||||
}
|
||||
|
||||
const QString text = clipboard->text();
|
||||
if (text == m_lastSyncedClipboardText) {
|
||||
if (text.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_lastSyncedClipboardText = text;
|
||||
emit requestSetClipboardText(text);
|
||||
appendEvent(QStringLiteral("Clipboard: sending %1 characters to remote.").arg(text.size()));
|
||||
}
|
||||
|
||||
void SessionTab::showEvent(QShowEvent* event)
|
||||
{
|
||||
QWidget::showEvent(event);
|
||||
pushLocalClipboardToRemote();
|
||||
}
|
||||
|
||||
void SessionTab::setupUi()
|
||||
|
||||
+20
-1
@@ -24,6 +24,7 @@ class QComboBox;
|
||||
class QLabel;
|
||||
class QPushButton;
|
||||
class QScrollArea;
|
||||
class QShowEvent;
|
||||
class KodoTerm;
|
||||
|
||||
struct SessionUiPreferences
|
||||
@@ -68,6 +69,14 @@ public:
|
||||
void setVncScaleToFit(bool scaleToFit);
|
||||
bool vncScaleToFit() const;
|
||||
bool awaitingUserInput() const;
|
||||
// True if this tab's profile was never saved to the repository (i.e.
|
||||
// it came from Quick Connect, issue #23, rather than a saved profile).
|
||||
// ProfileDialog::profile() always returns id <= 0 for an unsaved
|
||||
// profile, and Quick Connect is the only path that hands such a
|
||||
// profile to SessionWindow::addSessionTab() -- every other path looks
|
||||
// the profile up from the repository first, which returns id > 0.
|
||||
bool isQuickConnect() const;
|
||||
Profile profileForSaving() const;
|
||||
|
||||
signals:
|
||||
void tabTitleChanged(const QString& title);
|
||||
@@ -99,6 +108,11 @@ signals:
|
||||
void requestSetClipboardText(const QString& text);
|
||||
void requestProvideUsername(const QString& username);
|
||||
|
||||
protected:
|
||||
// A tab that wasn't in front while the local clipboard changed missed
|
||||
// that change (only the visible tab syncs), so catch it up on show.
|
||||
void showEvent(QShowEvent* event) override;
|
||||
|
||||
private slots:
|
||||
void onBackendStateChanged(SessionState state, const QString& message);
|
||||
void onBackendEventLogged(const QString& message);
|
||||
@@ -119,7 +133,11 @@ private:
|
||||
SessionConnectOptions m_lastConnectOptions;
|
||||
QString m_terminalThemeName;
|
||||
int m_terminalFontPointSize;
|
||||
QString m_lastSyncedClipboardText;
|
||||
// Text we just wrote to the system clipboard from a remote update, so
|
||||
// the dataChanged() it triggers isn't echoed straight back out. Only
|
||||
// honored briefly -- see onSystemClipboardChanged().
|
||||
QString m_pendingClipboardEcho;
|
||||
qint64 m_pendingClipboardEchoMs = 0;
|
||||
bool m_clipboardSyncSupported;
|
||||
|
||||
KodoTerm* m_sshTerminal;
|
||||
@@ -162,6 +180,7 @@ private:
|
||||
std::function<void(std::optional<QString>)> callback,
|
||||
bool maskInput = true);
|
||||
void hidePasswordPrompt();
|
||||
void pushLocalClipboardToRemote();
|
||||
bool validateProfileForConnect();
|
||||
void appendEvent(const QString& message);
|
||||
void setState(SessionState state, const QString& message);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "session_window.h"
|
||||
|
||||
#include "about_dialog.h"
|
||||
#include "profile_dialog.h"
|
||||
#include "profiles_window.h"
|
||||
#include "user_guide_dialog.h"
|
||||
#include <QApplication>
|
||||
@@ -148,6 +149,12 @@ SessionWindow::SessionWindow(QWidget* parent)
|
||||
actualSizeAction->setChecked(!tab->vncScaleToFit());
|
||||
}
|
||||
|
||||
QAction* saveAsProfileAction = nullptr;
|
||||
if (tab->isQuickConnect()) {
|
||||
menu.addSeparator();
|
||||
saveAsProfileAction = menu.addAction(QStringLiteral("Save as Profile..."));
|
||||
}
|
||||
|
||||
QAction* chosen = menu.exec(m_tabs->tabBar()->mapToGlobal(pos));
|
||||
if (chosen == disconnectAction) {
|
||||
tab->disconnectSession();
|
||||
@@ -188,6 +195,8 @@ SessionWindow::SessionWindow(QWidget* parent)
|
||||
tab->setVncScaleToFit(true);
|
||||
} else if (actualSizeAction != nullptr && chosen == actualSizeAction) {
|
||||
tab->setVncScaleToFit(false);
|
||||
} else if (saveAsProfileAction != nullptr && chosen == saveAsProfileAction) {
|
||||
saveTabAsProfile(tab);
|
||||
} else {
|
||||
for (QAction* themeAction : themeActions) {
|
||||
if (chosen == themeAction) {
|
||||
@@ -201,6 +210,7 @@ SessionWindow::SessionWindow(QWidget* parent)
|
||||
QMenu* fileMenu = menuBar()->addMenu(QStringLiteral("File"));
|
||||
QAction* newProfileAction = fileMenu->addAction(QStringLiteral("New Profile"));
|
||||
QAction* newFolderAction = fileMenu->addAction(QStringLiteral("New Folder"));
|
||||
QAction* quickConnectAction = fileMenu->addAction(QStringLiteral("Quick Connect..."));
|
||||
fileMenu->addSeparator();
|
||||
QAction* importProfilesAction = fileMenu->addAction(QStringLiteral("Import Profiles..."));
|
||||
QAction* exportProfilesAction = fileMenu->addAction(QStringLiteral("Export Profiles..."));
|
||||
@@ -216,6 +226,10 @@ SessionWindow::SessionWindow(QWidget* parent)
|
||||
&QAction::triggered,
|
||||
this,
|
||||
[this]() { m_profilesWidget->createFolderInCurrentContext(); });
|
||||
connect(quickConnectAction,
|
||||
&QAction::triggered,
|
||||
this,
|
||||
[this]() { m_profilesWidget->quickConnect(); });
|
||||
connect(importProfilesAction,
|
||||
&QAction::triggered,
|
||||
this,
|
||||
@@ -368,6 +382,19 @@ int SessionWindow::sessionTabCount() const
|
||||
return m_tabs->count() - 1;
|
||||
}
|
||||
|
||||
void SessionWindow::saveTabAsProfile(SessionTab* tab)
|
||||
{
|
||||
ProfileDialog dialog(this);
|
||||
dialog.setDialogTitle(QStringLiteral("Save as Profile"));
|
||||
dialog.setProfile(tab->profileForSaving());
|
||||
|
||||
if (dialog.exec() != QDialog::Accepted) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_profilesWidget->createProfileFromExisting(dialog.profile());
|
||||
}
|
||||
|
||||
void SessionWindow::loadUiPreferences()
|
||||
{
|
||||
QSettings settings;
|
||||
|
||||
@@ -32,6 +32,7 @@ private:
|
||||
void loadUiPreferences();
|
||||
void saveUiPreferences() const;
|
||||
int sessionTabCount() const;
|
||||
void saveTabAsProfile(SessionTab* tab);
|
||||
};
|
||||
|
||||
#endif
|
||||
Reference in new issue
Block a user