Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,7 @@ Cargo.lock text eol=lf
LICENSE text eol=lf
.gitignore text eol=lf
.gitattributes text eol=lf
*.cpp text eol=lf
*.h text eol=lf
*.qml text eol=lf
CMakeLists.txt text eol=lf
7 changes: 7 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,13 @@ jobs:
cargo clippy --all-targets --locked -- -D warnings
cargo test --locked
cargo build --locked
- name: Qt manager dependencies
run: sudo apt-get update && sudo apt-get install -y cmake qt6-base-dev qt6-declarative-dev qml6-module-qtqml qml6-module-qtqml-models qml6-module-qtquick qml6-module-qtquick-controls qml6-module-qtquick-layouts qml6-module-qtquick-templates qml6-module-qtquick-window qml6-module-qtqml-workerscript
- name: Qt manager build and isolated tests
run: |
cmake -S ui -B build/ui -DCMAKE_BUILD_TYPE=Release
cmake --build build/ui --parallel 2
ctest --test-dir build/ui --output-on-failure
- name: Host recovery and repository tests
run: python -m unittest discover -s tests -v
- name: Standalone daemon integration
Expand Down
17 changes: 14 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ Hypertile Scene like any other application.

The standalone backend is implemented on `develop`: a Rust daemon and CLI with
Moonlight process supervision, journaled host recovery, and per-computer desktop
launcher entries. The graphical manager and installable packaging are not implemented yet.
launcher entries. The Qt graphical manager now provides connection controls for
configured computers. Installable packaging is not implemented yet.
`main` remains the locked project bootstrap; there is no published app release.

The host adapters come from the remote-stream work in
Expand All @@ -27,8 +28,8 @@ profiles, connection lifecycle, host display recovery, and user controls.
- Preserve Moonlight pairing and existing host authentication.
- Offer a standalone computer/settings window and, optionally, an Omarchy bar plugin.

Hypertile integration will use generic application launch and window matching.
Hypertile will own placement; Remote Desktops will own connections.
Hypertile Scenes uses generic application launch and window matching.
Hypertile owns placement; Remote Desktops owns connections.

See [the extraction plan](docs/EXTRACTION.md) for the implementation sequence,
migration requirements, and acceptance criteria.
Expand Down Expand Up @@ -80,3 +81,13 @@ Initial local measurements and their limits are in [validation](docs/VALIDATION.

MIT. This project is independently maintained and is not an official Omarchy,
Moonlight, or Sunshine application.

## Graphical manager

A separate Qt 6/QML manager is available on `develop` for existing configured
computers. It provides connection controls, profile selection, display recovery,
and per-computer launchers. Closing it leaves connections running.

See [UI build, preview, and interaction guide](docs/UI.md). Pairing and computer
configuration still use Moonlight and the backend setup guide; no installable
release has been published.
25 changes: 21 additions & 4 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@

The local daemon, CLI, and client supervisor use Rust with Tokio. Moonlight Qt
remains a separate client process; Sunshine remains the remote server. The
planned graphical manager uses Qt 6/QML in a separate process, communicating
over the daemon's Unix socket. The backend has no Qt dependency. No GUI or Qt
bridge is implemented in this feature.
graphical manager uses Qt 6/QML with a small C++ Qt bridge in a separate process.
It reads status over the daemon's Unix socket and submits explicit actions via
the Rust CLI. The backend has no Qt dependency. See [desktop manager](UI.md).

The existing Python macOS adapter and Windows transport/recovery algorithms are
kept behind a one-operation helper interface. Windows retains its PowerShell
Expand Down Expand Up @@ -99,4 +99,21 @@ require the local Hypertile plugin. Do not install a second console helper or
discard an existing journal during migration.

Scenes and layout browsing remain in Hypertile. Generic application launch and
window matching in Scenes are a subsequent feature.
window matching in Scenes now use the installed per-computer desktop entries.

## Manager process boundary

The Qt bridge has no streaming, supervision, configuration-writing, or recovery
implementation. Configuration listing runs the Rust CLI once at startup and on
explicit refresh, exposing only computer ID/name, host, platform, default
profile, and profile names. Pairing material is not part of this listing.
Commands use an absolute executable and an argv array, never a shell string.

Status uses asynchronous QLocalSocket request/reply framing, a 1.5-second
request timeout, a 2 MB reply limit, and at most one outstanding request. The
visible active manager polls every two seconds; an inactive manager stops
polling. Unchanged status does not emit a model update. Command processes have
a 60-second acknowledgement bound, with uncertainty reported if it expires;
only the CLI process is stopped, not the independent daemon or its host work.
No synchronous process/socket wait occurs on the GUI thread. These are resource
bounds, not measured latency or CPU claims.
3 changes: 2 additions & 1 deletion docs/RELEASING.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@

`develop` contains the standalone Rust backend and host adapters. `main` still
contains the locked bootstrap. There is no published application release,
installable package, graphical manager, or Omarchy marketplace submission yet.
installable package or Omarchy marketplace submission yet. A development Qt
manager is available for configured computers.

The initial CI/workflow bootstrap is complete. Its history was subsequently
linearized at the owner's request. This is not an application release and
Expand Down
77 changes: 77 additions & 0 deletions docs/UI.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# Desktop manager

The Qt 6/QML manager is a separate application for configured computers. It
shows connection state, selects an existing profile, connects, focuses,
disconnects, reconnects, restores a pending host display, and installs a
per-computer application launcher. It has no video renderer and owns no host
recovery. Closing it leaves the daemon and Moonlight sessions running.

## Run from a checkout

Build the Rust backend using the repository toolchain, then the Qt manager:

```sh
cargo build --locked
cmake -S ui -B build/ui -DCMAKE_BUILD_TYPE=Release
cmake --build build/ui --parallel 2
build/ui/remote-desktops-manager --backend "$PWD/target/debug/remote-desktops"
```

The UI requires Qt 6.4 or later: Quick, Quick Controls 2, Network, and the Qt
Test development module for the test target. No Qt libraries are linked into
the Rust backend. The manager accepts an absolute backend path; otherwise it
looks beside itself and then on PATH. Configuration and socket paths follow
the backend's XDG conventions.

Opening the manager reads configured computers and status. It does not start a
connection automatically. An explicit Connect starts the daemon when necessary.
Do not use a development binary to take over active production sessions merely
to test the UI. Use the isolated preview instead:

```sh
build/ui/remote-desktops-manager --demo
build/ui/remote-desktops-manager --demo --state restore-pending
ctest --test-dir build/ui --output-on-failure
```

Demo actions never spawn the backend or access its socket. The three example
computers are synthetic. `--state` also supports `idle`, `preflight`, `empty`,
and `unavailable`; `--compact` exercises the minimum window size. With an
offscreen platform, `--screenshot /tmp/manager.png` exports the rendered demo.
These preview and screenshot options require `--demo`.

## Interaction design

- A stable computer list sits beside the selected connection. Selection is
retained by computer ID, rather than by a changing row index.
- One primary action follows state: Connect, Open desktop, or Restore display.
Repeat actions are suppressed while an acknowledgement is pending. A request
acknowledgement is not presented as a successful connection.
- Profiles cannot change during a desired session; disconnect first. Reconnect
restarts the selected client. Disconnect cancels pending connection intent
and asks the daemon to restore its owned host settings.
- Host recovery errors remain visible, with technical detail available on
demand. Recovery records for removed computers remain in the list. There is
deliberately no one-click abandonment of the original host settings.
- Status loss preserves last-known records and labels them unavailable; it
does not pretend that a running remote session disconnected. Refresh retries
observation. Connect/Restore use the CLI's existing daemon-start behavior.
- No thumbnails or performance figures are invented. The device drawing is an
illustration. Window-ready is an identity match, not proof of a rendered
video frame or measured latency.
- Tab navigates controls, arrow keys navigate the focused computer list,
Ctrl+Enter invokes the primary action, Ctrl+R refreshes, and Escape closes
a dialog. Focus rings, accessible names, textual statuses, and restrained
hover transitions complement color cues.

## Current scope

This first manager manages existing configuration. Pairing still happens in
Moonlight; adding/editing computer and profile configuration follows the
[backend setup guide](BACKEND.md). The in-app help explains that flow. A guided
pairing/configuration editor is subsequent work; this UI does not offer a
placeholder form or write unvalidated settings.

The manager does not install itself, register a system service, migrate legacy
configuration, or change Hypertile. Packaging and a graphical setup wizard are
separate delivery steps.
2 changes: 1 addition & 1 deletion scripts/check.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from urllib.parse import unquote, urlsplit


TEXT_SUFFIXES = {".md", ".py", ".yml", ".yaml", ".sh", ".ps1", ".toml", ".json", ".rs", ".cs"}
TEXT_SUFFIXES = {".md", ".py", ".yml", ".yaml", ".sh", ".ps1", ".toml", ".json", ".rs", ".cs", ".cpp", ".h", ".qml", ".txt"}
TEXT_NAMES = {"LICENSE", ".gitignore", ".gitattributes"}
LINK = re.compile(r"\[[^\]\n]*\]\(([^)\n]+)\)")

Expand Down
12 changes: 11 additions & 1 deletion src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,17 @@ async fn run(cli: Cli) -> Result<()> {
}
if matches!(cli.command, Action::Computers) {
let value = host::call(json!({"operation":"validate","config":paths.config})).await?;
let entries=value.as_object().unwrap().iter().map(|(name,c)|json!({"computer":name,"profiles":c["profiles"].as_object().unwrap().keys().collect::<Vec<_>>()})).collect::<Vec<_>>();
let entries = value
.as_object()
.unwrap()
.iter()
.map(|(name, c)| {
let title = c["title"].as_str().unwrap_or(name);
json!({"computer":name, "name":title.strip_suffix(" - Moonlight").unwrap_or(title),
"host":c["host"], "platform":c["platform"], "default_profile":c["default_profile"],
"profiles":c["profiles"].as_object().unwrap().keys().collect::<Vec<_>>()})
})
.collect::<Vec<_>>();
println!("{}", serde_json::to_string_pretty(&entries)?);
return Ok(());
}
Expand Down
9 changes: 9 additions & 0 deletions tests/integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,15 @@ def test_oversized_socket_request_does_not_stall_other_commands(self):
sock.sendall(b"x" * 65_537)
self.assertEqual(self.cli("status")["computers"], [])

def test_manager_catalog_exposes_labels_without_pairing_material(self):
entries = self.cli("computers")
self.assertEqual([c["computer"] for c in entries], ["laptop", "other"])
self.assertEqual(entries[0]["name"], "laptop")
self.assertEqual(entries[0]["profiles"], ["desktop"])
self.assertEqual(set(entries[0]), {"computer", "name", "host", "platform", "default_profile", "profiles"})
self.assertNotIn("pairing_uuid", json.dumps(entries))
self.assertEqual(self.cli("status")["computers"], [])

def test_launcher_install_metadata_and_removal_preserve_other_apps(self):
installed = self.cli("launcher", "install", "laptop")
path = Path(installed["installed"])
Expand Down
25 changes: 25 additions & 0 deletions ui/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
cmake_minimum_required(VERSION 3.21)
project(RemoteDesktopsManager VERSION 0.1.0 LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_AUTOMOC ON)
find_package(Qt6 6.4 REQUIRED COMPONENTS Quick QuickControls2 Network Test)
qt_add_executable(remote-desktops-manager main.cpp Manager.cpp Manager.h)
qt_add_resources(remote-desktops-manager qml PREFIX "/" FILES qml/Main.qml qml/ActionButton.qml qml/ComputerGlyph.qml)
target_link_libraries(remote-desktops-manager PRIVATE Qt6::Quick Qt6::QuickControls2 Qt6::Network)
target_compile_options(remote-desktops-manager PRIVATE -Wall -Wextra -Wpedantic)
enable_testing()
qt_add_executable(manager-tests tests/manager.cpp Manager.cpp Manager.h)
target_link_libraries(manager-tests PRIVATE Qt6::Test Qt6::Network Qt6::Quick Qt6::QuickControls2)
qt_add_resources(manager-tests qmlTests PREFIX "/" FILES qml/Main.qml qml/ActionButton.qml qml/ComputerGlyph.qml)
add_test(NAME manager COMMAND manager-tests)
set_tests_properties(manager PROPERTIES ENVIRONMENT "QT_QPA_PLATFORM=offscreen;QT_QUICK_BACKEND=software")
add_test(NAME qml-smoke COMMAND remote-desktops-manager --demo --smoke-test)
set_tests_properties(qml-smoke PROPERTIES ENVIRONMENT "QT_QPA_PLATFORM=offscreen;QT_QUICK_BACKEND=software")

foreach(state idle preflight restore-pending empty unavailable)
add_test(NAME qml-${state} COMMAND remote-desktops-manager --demo --state ${state} --smoke-test)
set_tests_properties(qml-${state} PROPERTIES ENVIRONMENT "QT_QPA_PLATFORM=offscreen;QT_QUICK_BACKEND=software")
endforeach()
add_test(NAME qml-compact COMMAND remote-desktops-manager --demo --compact --smoke-test)
set_tests_properties(qml-compact PROPERTIES ENVIRONMENT "QT_QPA_PLATFORM=offscreen;QT_QUICK_BACKEND=software")
Loading