Skip to content

Add native macOS client and host with LAN discovery - #47

Open
acrogenesis wants to merge 16 commits into
omacom:mainfrom
acrogenesis:acrogenesis/macos-client-host
Open

acrogenesis wants to merge 16 commits into
omacom:mainfrom
acrogenesis:acrogenesis/macos-client-host

Conversation

@acrogenesis

@acrogenesis acrogenesis commented Oct 12, 2026 •

Copy link
Copy Markdown
Member

Gliff can now view Omarchy from a Mac and view a Mac from Omarchy through the existing SSH transport. Launching Gliff on macOS opens a native Nearby Computers picker; Linux hosts advertise their SSH endpoint through Avahi, and manual SSH addresses remain available.

The change keeps the Linux client and server behind target-specific entry points and adds an AppKit viewer, ScreenCaptureKit capture, CoreGraphics input and lazy text clipboard transfer. Mac hosting runs in a LaunchServices-launched app so Screen Recording and Accessibility grants apply to Gliff. VideoToolbox hardware H.264 is the default on the Mac host, with OpenH264 fallback after startup or runtime failure and an explicit CPU option.

The same change includes the shared app icon, staged Mac installer, development archives and maintainer-owned Developer ID/notarization release tooling. Linux retains main’s 0.4.1 capture modes and fallbacks; Mac viewers negotiate embedded cursors, and both clients retain the cursor and idle-session fixes. Native packet/frame buffering, input release and transport teardown are bounded; temporary pipe-read buffers are drained after each short read, and parallel first-use helper installation is serialized. Linux uses main's integrated NVIDIA plane imports and decoder bounds checks; unsupported imports retain the existing CPU fallback.

The development TCP listener shares Linux's loopback-only policy and requires explicit --allow-remote for network exposure. SSH fallbacks run through /bin/sh, command-line aliases retain their configured SSH ports, and discovered selections retain their advertised ports. Mac network writes use a bounded task and write deadlines so stalled video cannot block input release or disconnect handling; discovery reconfiguration restarts its publisher to apply port changes.

The Mac connection picker incorporates rx7x12’s layout suggestion and testing feedback. It is a compact standalone window with system appearance/accent colors, computer icons, inline SSH fields and native controls. The window opens at its content-fitting minimum, shows up to four computer rows before scrolling and preserves a window you enlarge. Tab changes focus; Return connects and Escape cancels.

The picker offers Mirror screen or Private screen, remembered independently for each SSH address and port. Mirror remains the initial default and preserves the host screen's proportions. Private creates a separate Linux desktop following the Mac window. Retina size limits preserve aspect ratio and logical pointer coordinates, and fullscreen/backing changes update the remote viewport.

Mac clients now export the active Unicode keyboard layout to Hyprland, including German Y/Z, umlauts, Shift/Caps Lock, Option symbols and common dead keys. Input-source changes update the session keymap. Mac hosts continue using their active local layout; input-method composition and custom multi-character keys remain outside the initial scope.

Screenshots

Nearby Computers on macOS

Compact standalone Mac connection picker with nearby computer, SSH fields and screen mode
Dark appearance Standalone Mac connection picker in dark appearance with Private screen selected

Native Mac client viewing Omarchy

A live SSH session showing a dedicated Omarchy terminal on a private output.

Native Gliff Mac client displays a live Omarchy desktop and terminal

Omarchy client viewing the Mac desktop

The clean Mac Space mirrored over SSH.

Gliff on Omarchy displays the clean Mac desktop with its menu bar and Dock

Validation

  • All five CI checks pass on final review fix efc8c8a, including macOS 15 Apple Silicon/Intel native checks, release builds and development packages. The merge includes all changes from main 58c7424 (0.4.1), including upstream NVIDIA imports, bounded decoders, capture/output handling, input fixes and clipboard changes. The Linux UI is kept in its target-specific module; the duplicate branch NVIDIA implementation was removed in favor of main's integrated version.
  • Linux: scripts/check.sh, formatting, Clippy with warnings denied and 300 tests pass (one existing ignored). Linux release build, Bash syntax and warning-level ShellCheck pass.
  • M1 Max/macOS 27.0.1: GLIFF_REQUIRE_HARDWARE_ENCODER=1 scripts/check.sh, formatting, Clippy, 105 tests and native reader/input/layout/picker checks pass. Mac release build and development packaging pass.
  • Automated-review regressions cover wildcard/mixed-address listener rejection, real SSH alias-port resolution, literal arguments through PATH/app/local-bin fallbacks in available shells, and discovery port changes against a fake running publisher. The Mac stalled-video test leaves an encoded payload unread while verifying key/button releases, Bye/EOF handling and timeout cleanup against a fake native input sink, including keeping capture stdout drained until helper exit. Bridge/SSH stdout stalls also have deadline coverage. Original-command csh and original-script discovery negative controls reproduce the reported bugs; isolated inline-write and early-capture-close negative controls reproduce both input-release failures.
  • The reported packaged-app startup failure is reproduced with the old picker: its own bundle ID used as a preferences suite traps on macOS 27. The fixed picker uses standard defaults inside Gliff's bundle and a safe named-suite fallback outside it. A new same-ID app-bundle regression covers production default preferences, alongside the unbundled and injected-preference checks.
  • On merge ace7645, a newly built 0.4.1 development archive was extracted and its bundle signature verified. The actual bundled gliff-native choose opens, answers inspection and cancels cleanly, without the own-suite warning. The actual Contents/MacOS/gliff executable starts and keeps its picker alive. Tests use a separate extracted app; installed apps and privacy grants are unchanged.
  • On that archive, the packaged native viewer memory check on macOS 27: 240 Retina frames, peak RSS 116 MiB, zero settled growth. Native reader checks retain the unchanged 128 MiB RSS and 16 MiB settled-growth caps.
  • On the preceding merge ace7645, full GPU-enabled scripts/e2e.sh passed on RTX 5090/NVIDIA 615.78.08: Vulkan Video encoding, Dual420/Single420 and mixed CPU/GPU quality checks, capture, mirror resize/forced CPU capture fallback, rotation, odd-sized captures, text/binary/file clipboard, input-method keymap switching and shortcut inhibition. No CPU-viewer override or excluded GPU cases. The nested compositor uses the private Aquamarine format fix; installed libraries/configuration are unchanged.
  • Aquamarine #451 merged into upstream main as 163b138 on October 22. Its Headless.cpp exactly matches the previously tested patch. The October 19 comparison reproduced the original 0x0/GBM failure in three of three runs and passed actual capture/encode/decode with the fix in three of three runs.

Earlier live-session checks, predating this main merge:

  • October 19: a production Mac client connected to a disposable Linux host, accepted its full active Unicode keymap, displayed video and resized a private output to 2200x1440 at scale 2. Real Mac host → Linux GPU decoding passed eight frames, two keyframes and resize.
  • Live sessions in both directions: decoded frames, resizing, typing, mouse buttons, scroll, modifiers and lazy text paste passed. Mac host Caps Lock and held-input release on disconnect passed. Hardware, forced CPU and Dual420 checks passed; terminating the codec produced a decodable CPU fallback keyframe, and terminating capture closed the relay.
  • German/US layout checks verify positions, umlauts, Shift/Caps Lock, Option, dead keys and input-source updates. Picker checks cover compact sizing, host/port preferences and keyboard navigation; viewport checks cover fullscreen/backing-scale updates.

Before general release

General release is pending the following checks:

  • The long GUI stability run was interrupted; sustained sessions, sleep/wake and clean install/upgrade validation remain open.
  • Omacom must provide its Developer ID Application and notarization credentials for public downloads. Public signing and notarization have not run locally.
  • macOS 13 and Intel runtime validation remain open. The native code targets macOS 13 and CI adds arm64/Intel builds.

The documented initial Mac scope is CPU viewer decoding, text-only clipboard and mirror hosting. Automatic LAN discovery currently finds Linux hosts from the Mac picker. Private Mac outputs, automatic reconnect and Linux discovery of Mac hosts are follow-up work.

The companion OPR recipe PR #1009 adds the PNG/discovery payload for Linux packaging. This branch inherits the 0.4.1 workspace version from main.

@acrogenesis
acrogenesis force-pushed the acrogenesis/macos-client-host branch from 23fbccd to 23e1497 Compare October 13, 2026 00:24
@rx7x12

rx7x12 commented Oct 13, 2026

Copy link
Copy Markdown

Cool PR, @acrogenesis! Being able to use Gliff between a Mac and Omarchy is really exciting.

One small suggestion for the macOS app: would it be possible to follow Apple's Human Interface Guidelines a bit more closely for the UI? I think it would help Gliff feel more at home on macOS and blend in with the rest of the system.

I put together a quick mockup (made with Claude) of how the Nearby Computers picker could look:

image

Of course this is just an idea, and I totally understand if it's out of scope for this PR or better suited as a follow-up. Thanks again for all the work!

@acrogenesis

Copy link
Copy Markdown
Member Author

thanks @rx7x12 I improved the design. works dark mode now too.

@acrogenesis

Copy link
Copy Markdown
Member Author

hey @dhh does omacom have an apple developer account? to publish/sign apps?

@teomurgi

Copy link
Copy Markdown
Contributor

i guess this supersedes https://github.com/teomurgi/mac-gliff

@Unayung

Unayung commented Oct 15, 2026

Copy link
Copy Markdown
Contributor

Nice work on this. In case it is useful for the viewer side: the Mac viewer here decodes with gliff_sw (OpenH264 on the CPU), and I have a hardware decode path for macOS that might drop in.

crates/gliff-mac on my macos-client branch:

  • Two VideoToolbox sessions decode main and aux at the same time, into IOSurface-backed NV12.
  • A Metal compute kernel reads the planes in place and does the AVC444 recombine and the BT.709 convert. It is built with fused multiply-add off, so its output matches gliff_proto::color bit for bit. The tests check this on real Dual420 and Single420 streams, and against the CPU tier.
  • The frame lands in a shared MTLBuffer that the UI can wrap without a copy.

At 3440x1440 Dual420 on an M5, decode plus recombine takes about 4.1 ms per frame on average, 4.9 ms at p95.

The client around it is the existing GTK client, built on macOS against Homebrew's GTK. The decoder doesn't depend on GTK, though, so it should work behind an AppKit viewer too. Happy to help fit it in if you want it.

🤖 Generated with Claude Code

@rx7x12

rx7x12 commented Oct 17, 2026 •

Copy link
Copy Markdown

Testing feedback: CI build 38201e2 (gliff-macos-ARM64)

Tested the CI artifact from this run on a MacBook Air (M4), macOS , connecting to an Omarchy host. Thanks for picking up the Nearby Computers mockup so quickly! Here is what I ran into:

1. Wrong keyboard layout in the session

My Mac uses a German keyboard layout, but the remote session used a US/English layout, so several keys (e.g. Y/Z, umlauts, symbols) produced the wrong characters. The README promises "your keyboard layout, not the remote's", which works Linux-to-Linux, but the Mac client doesn't seem to pass the local layout to the session yet.

2. Headless / private screen for the Mac client – maybe as the default?

On Linux, gliff --headless user@host creates a private remote screen that follows the window size. It would be great if the Mac app offered this too, and I'd suggest considering it as the default for the Mac client:

  • The remote screen would match the Mac window and fullscreen size, so no letterboxing (see issue 4).
  • It doesn't take over or disturb the physical screen on the Omarchy machine.
  • It also works when the host has no monitor attached.

Mirroring would still be useful to see what's on the host's real screen, so a simple "Private screen / Mirror screen" choice in the connect dialog, remembered per machine, would cover both cases.

3. Connect dialog sits halfway between the old window and the mockup

The picker now has the same content as the mockup, but the window around it doesn't match yet:

  • Title bar: the build uses a standard title bar with a centered "Gliff" title. The mockup uses a unified/transparent title bar with "Gliff" next to the traffic lights.
  • Presentation: in the build, the dialog is an inset card that fills almost the whole window, with a thin margin and no shadow. In the mockup, it's a smaller card centered on an empty window background with a drop shadow, so it clearly reads as a sheet over the app window.
  • Result: it looks like a "box inside a box", neither a plain standalone window nor a proper sheet.

Either direction would feel native: a clean standalone window without the inner card, or a real sheet over the main window as in the mockup. (Dark appearance in my screenshot is just my system setting.)

4. Fullscreen viewer does not fill the screen vertically

In fullscreen, the remote desktop doesn't reach the top and bottom edges of the display. There is an empty band at the top and at the bottom, so the image is letterboxed instead of edge-to-edge.

Screenshots:
Bildschirmfoto 2026-10-17 um 19 12 05

Bildschirmfoto 2026-10-17 um 19 26 10

Just an idea: here's a quick mockup of how this could look in the connect sheet, following the Nearby Computers mockup above. Totally fine if it's out of scope for this PR or better as a follow-up.

Connect sheet with screen option@1x

@acrogenesis

Copy link
Copy Markdown
Member Author

Addressed the testing feedback in 792c689:

  • Keyboard layout: the Mac client now sends its actual Unicode keyboard layout and updates it when the input source changes. German Y/Z, umlauts, Shift/Caps/Option and common dead keys are covered by captured-layout and native checks.
  • Screen choice: the picker offers Mirror screen / Private screen, remembered per SSH host and port. Mirror remains the initial default; Private creates a Linux screen that follows the viewer size.
  • Picker: removed the inset outer card, used a transparent title bar and native window background, and kept the window sized to its content. Updated light/dark screenshots are in the description.
  • Fullscreen/Retina: resolution limits preserve aspect ratio and pointer coordinates, and fullscreen/backing-scale changes send viewport updates. Private follows the window; mirroring retains the physical screen's aspect ratio.

Validation: Linux 220 tests and Mac 92 Rust tests pass, along with fmt/clippy and the native keyboard, picker, fullscreen and memory checks. A live Mac client displayed a private Linux screen at 2200×1440, scale 2. All five CI checks pass, including Apple Silicon and Intel.

The separate nested-headless compositor bug still reproduces against current Aquamarine, 3/3 runs; the patch passes 3/3. Since GitHub refused to reopen #447, the now-vouched replacement is Aquamarine #451.

One remaining hardware limitation: the full NVIDIA Linux GPU-decode E2E fails image quality on RTX 5090 / 615.78.08. Fresh diagnostics locate corruption at the VA-API → Vulkan surface import: NVDEC surfaces downloaded and CPU-recombined score 59.5 dB, while the current combined/disjoint NV12 import gives 7.6 dB. Importing the exported Y/UV planes separately as R8/RG8 reads them back byte-for-byte correctly. That diagnostic is not a shipped decoder fix. GPU encoding with CPU decoding passes; the Mac viewer uses that CPU decode path. The focused CPU-viewer E2E passes, but the full hardware suite remains failing.

@acrogenesis

acrogenesis commented Oct 19, 2026 •

Copy link
Copy Markdown
Member Author

Fixed the NVIDIA GPU-decoding corruption in 4e992fc.

NVDEC was producing correct decoded surfaces, but importing its two separately exported R8/RG88 layers as one disjoint NV12 Vulkan image misread their tiling. The decoder now imports each layer separately as R8/RG8, preserves the exported layout and applies foreign-ownership barriers to both images. Hardware decoding and GPU recombination stay enabled; the shared-object NV12 path is retained.

  • Dual420 quality: 7.6 → 55.8 dB; Single420: 5.5 → 54.9 dB.
  • The full scripts/e2e.sh passes with GPU decoding enabled, using the private patched Aquamarine build for the nested compositor. No GPU cases excluded. Added Single420 and delayed CPU-encode/GPU-decode pixel-quality checks.
  • A real Mac host → Linux GPU-decoder session passes eight frames and resize. Additional small/padded-size codec probes pass.
  • Linux formatting, Clippy and all 220 tests pass. Bash syntax and ShellCheck pass.
  • All five CI checks pass on this exact commit, including both Apple Silicon and Intel checks, release builds and development packages.

This resolves the GPU limitation noted in my previous comment. The Aquamarine fix remains a separate prerequisite for nested headless-output tests; no installed compositor libraries or Mac app permissions were changed.

@rx7x12

rx7x12 commented Oct 25, 2026 •

Copy link
Copy Markdown

macOS CI build doesn't start: gliff-native choose crashes

Tested: gliff-0.4.0-macos-arm64-development.zip from this PR's CI , Apple Silicon, macOS 27.

Symptom: The app doesn't open. From a terminal, Contents/MacOS/gliff exits with Error: unexpected end of file, after this message from the helper: "Using your own bundle identifier as an NSUserDefaults suite name does not make sense…". Running gliff-native choose directly exits with code 133 (SIGTRAP).

Cause: In crates/gliff-macos/native/Gliff.swift, ComputerPicker.init force-unwraps UserDefaults(suiteName: "org.omacom.gliff")!. Inside the bundle, the helper's own bundle ID is org.omacom.gliff, so the call returns nil and the unwrap traps. This doesn't happen with cargo run, because there the helper runs outside the bundle.

Fix:

preferences: UserDefaults = Bundle.main.bundleIdentifier == "org.omacom.gliff" ? .standard : UserDefaults(suiteName: "org.omacom.gliff")!

I rebuilt gliff-native with this change and swapped it into the CI bundle. The picker then opens, and connecting to a Gliff node on the LAN works: the remote desktop is shown.

@acrogenesis

acrogenesis commented Oct 25, 2026 •

Copy link
Copy Markdown
Member Author

Fixed the packaged-app startup report in ace7645.

The picker now uses UserDefaults.standard inside Gliff's own bundle, with a safe named-suite fallback outside it. Explicit preference injection remains available for tests. Added a same-ID app-bundle regression: the old code reproduces the warning/trap on macOS 27.0.1; the fixed bundled and unbundled pickers open and cancel successfully.

Also merged current main 58c7424 / 0.4.1 and resolved all seven conflicts. Upstream's integrated NVIDIA import and Linux fixes are retained, with the Linux UI in its platform module. Mac decoder call sites now use main's dimension-checked API.

Validation: 296 Linux tests, 99 Mac tests, fmt/Clippy/native checks and the full GPU-enabled hardware E2E pass. A freshly packaged/extracted 0.4.1 app passes signature verification, native picker inspection/cancellation and actual app executable startup on macOS 27.0.1. The packaged viewer's 240-frame Retina memory check peaks at 116 MiB with zero settled growth. Installed apps, privacy grants and compositor libraries are unchanged.

All five CI checks and build artifacts pass on this exact commit, including both Apple Silicon and Intel checks/builds/packages. The downloadable development packages now use version 0.4.1.

@greptile-apps

greptile-apps Bot commented Oct 25, 2026 •

Copy link
Copy Markdown

RetriggerView in GreptileConfidence Score: 5/5

[High impact] The latest documentation change appears safe to merge.

Summary

Adds a native macOS viewer and host, LAN discovery, and Mac packaging while keeping SSH as the main transport.

  • Mac users can pick a computer and view its desktop in a native window.
  • A Mac can share its display with a Gliff client over SSH.
  • Linux desktops can advertise their SSH address for nearby Mac pickers.
  • SSH connections keep their chosen ports and run server commands consistently.
  • Linux keeps its GTK client and Hyprland host behind Linux-only entry points.
  • Mac builds can be bundled, installed, and packaged for development and release.

Reviews (4) · Last reviewed commit: "Document native macOS shortcut and fulls..." · Reviewed by Greptile

Comment thread crates/gliff-macos/src/server.rs Resolved
Comment thread crates/gliff-transport/src/ssh.rs Resolved
Comment thread crates/gliff-macos/src/client.rs Resolved
Comment thread crates/gliff-macos/src/server.rs Resolved
Comment thread bin/enable-discovery Resolved
Comment thread crates/gliff-macos/src/server.rs Resolved
@Unayung

Unayung commented Nov 2, 2026

Copy link
Copy Markdown
Contributor

Two problems I hit running the GTK client from this PR on a Mac against Omarchy, with fixes on my fork in case they help. Tested on an Apple Silicon MacBook Pro, macOS 27.0.1, GTK 4.24.1, libadwaita 1.10.0.

My branch is this PR merged with upstream 0.4.1, so the GTK main.rs differs a lot from this PR's head and the commits do not cherry-pick cleanly. Treat them as a reference for the approach.

1. Shortcuts macOS claims never reach the remote

macOS matches its own hotkeys before the app gets the key event, so inhibit_system_shortcuts has nothing to inhibit and GTK never sees them. My Mac binds Cmd+I to "Select the previous input source" (symbolic hotkey 60), so Super+I never reached Hyprland. I only verified Cmd+I. Other shortcuts macOS handles itself, such as Cmd+Space or Cmd+Tab, should behave the same way.

Fix: while the picture has the keyboard, a HID-level CGEventTap takes each key before that matching and posts a copy to our own pid with CGEventPostToPid. The existing key controller then forwards it as usual, so the keycode mapping and the release hotkey are unchanged. The copies are tagged in kCGEventSourceUserData so the tap lets them through. The tap is switched from the focus enter/leave handlers that already inhibit and restore system shortcuts. It needs the Accessibility permission. Cmd+Opt+Esc and secure input stay with macOS.

Commit: Unayung@f30f50d

2. The remote's top bar is hard to use in native fullscreen

In native fullscreen, moving the pointer to the top edge slides in the menu bar and the title bar, over the remote's top bar. Returning HideDock | HideMenuBar from window:willUseFullScreenPresentationOptions: stops that. GTK's GdkMacosWindow is its own delegate and does not implement the method, so I add it at runtime. A reveal zone across the top of the window still swallows pointer motion. I logged the motion controller's y: on the way up, events stopped somewhere between y=21 and y=32 and resumed at y=2–5 after 690 to 1424 ms. That is the band where Omarchy's bar sits, so the remote cursor lags each time it enters the bar.

Fix: on macOS the fullscreen button no longer uses a fullscreen Space. The window turns borderless over the screen area below the camera housing (NSScreen.safeAreaInsets.top), with the menu bar and Dock hidden, and gets its style mask and frame back on exit. With that, motion arrives all the way to y=0 and I no longer notice the lag. The green title-bar button still enters native fullscreen, with the menu bar hidden. The fullscreen button's state now drives the fullscreen header, and the header only slides in at the top edge after the release hotkey, so that edge belongs to the remote while the picture has the keyboard.

Commit: Unayung@c674eeb

Happy to rework either one as a PR against your branch if you want them.

@acrogenesis

Copy link
Copy Markdown
Member Author

Reviewed both reference commits against the current native AppKit client. The GTK focus-controller and fullscreen-button patches do not apply to this branch's Swift window and input routing. The underlying limitations are relevant: reserved macOS shortcuts can be consumed before AppKit receives them, and native fullscreen can reserve the top edge.

Documented both limitations in 8a87245 rather than claiming the native client already handles them. Linux scripts/check.sh and the same source's complete macOS 27.0.1 checks pass, including native input/release/fullscreen-resize, keyboard layout, picker and bounded memory checks. These checks do not reproduce the reserved-hotkey or top-edge timing reports. A native adaptation would need explicit keyboard-capture consent, secure-input and release safeguards, plus safe-area handling; the reference patches are useful design evidence for that work.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants