mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-10-30 07:37:38 +00:00
Quartz moves to its own repo, the way the crypto primitives moved to crypto-kmp. Every module depends on com.vitorpamplona.quartz:quartz 2.0.0 (libs.quartz) instead of :quartz, and -PquartzDir=../quartz builds a local checkout instead of the artifact, for every module and for quic's own quartz dependency (which replaces the root build's substitution of the published quartz with :quartz). What moved with quartz: its CI jobs (lint tasks, test-quartz-linux-native, test-quartz-ios), the Maven Central publish step of the release workflow, jitpack.yml, tools/unicode-nfkc, the nostr-expert and event-store-semantics skills, the agent-skill check, and the README's library usage docs. What stays here: - The four quartz client tests that need a real Ktor socket relay (TrustNetworkSync*, GiantReqStreamTest, BulkDownloadBenchmark) move to geode's tests; geode forwards -PprodRelayBench for the latter. quartz's other relay-client tests now use a test relay of its own, which ends quartz's test dependency on geode. - The benchmark module's resources (previously read from quartz's androidDeviceTest resources) move into benchmark/; relayBench --real reads the corpus from there. - NoteComposeKindCoverageTest reads EventFactory.kt from quartz's JVM sources jar, or from the -PquartzDir checkout, passed by commonsUI's build. - commons and commonsUI declare crypto-kmp (and commons mls-kmp), which they import directly. Through quartz's api alone, a -PcryptoKmpDir checkout never reached the common metadata compile. - The catalog drops what only quartz used (kchesslib, negentropy, jackson-databind, sqlite-bundled, secp256k1-kmp common, compose runtime-annotation, the maven-publish plugin). agent_config_check checks a skill's quartz/... paths against a ../quartz checkout when there is one and skips them otherwise. AGENTS.md, the skills, CONTRIBUTING, BUILDING, RELEASE_OPS, PLANS and the READMEs point at the new repo. Needs quartz 2.0.0 (and crypto-kmp 1.0.0) on Maven Central before CI can resolve it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DEkJaDWYrcTbGvT8JLQCrG
110 lines
8.1 KiB
Markdown
110 lines
8.1 KiB
Markdown
---
|
||
name: desktop-expert
|
||
description: Desktop JVM shim in desktopApp/: AmethystDesktop window, DesktopMenuBar, platform ports, OS keyring; :desktopApp:run, packaging. Use when touching desktopApp, or to run/screenshot the shared UI headless (tools/desktop-run, Xvfb) to verify a UI change. NOT for writing shared screens.
|
||
---
|
||
|
||
# Desktop (JVM shim)
|
||
|
||
`desktopApp` is a **shim**: it opens a window and renders the same `AmethystApp` Android renders.
|
||
Screens, the navigation shell (bottom bar / rail / drawer, chosen by `ScreenLayoutSpec`) and the
|
||
deck are shared UI in `commonsUI` — see AGENTS.md "Navigation Shell" and "Feature Workflow", and
|
||
`compose-expert`. Never add a desktop-only screen or a desktop-specific navigation layout.
|
||
Background: `commons/plans/2026-09-27-one-ui-android-desktop.md`,
|
||
`commons/plans/2026-10-09-legacy-desktop-retirement.md` (the legacy desktop app is gone).
|
||
|
||
## Where things live (`desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/`)
|
||
|
||
| File | Role |
|
||
|---|---|
|
||
| `app/AmethystDesktop.kt` | `main()`: macOS AWT properties, data dir, `DesktopAppModules`, the `Window`, the CompositionLocals (`LocalAppPlatform`, `LocalNotePlatform`, …) around `AmethystApp`. `--publish-scheduled` runs headless (OS timer publishing scheduled posts). |
|
||
| `app/DesktopAppModules.kt` | The desktop graph (Android's `AppModules` minus phone-only parts). |
|
||
| `app/DesktopAppRoot.kt` | `AppRoot` impl; `jvmDestinations`. |
|
||
| `app/DesktopAppPlatform.kt` | `AppPlatform` (commonsUI `ui/platform/`); unoverridden members keep the default, which hides the feature. |
|
||
| `app/DesktopNotePlatform.kt`, `DesktopRichTextPlatform.kt` | `NotePlatform`, `RichTextPlatform` (commonsUI). |
|
||
| `app/DesktopAccountViewModelHost.kt`, `DesktopLocalCacheHost.kt`, `DesktopAppServices.kt` | `AccountViewModelHost`, `LocalCacheHost`, `AppServices` (commons). |
|
||
| `app/DesktopMenuBar.kt` | `DesktopNavigator`, the `Command` enum, `DesktopMenuBar`, `handleShortcut`. |
|
||
| `app/Legacy*Import.kt` | One-time import of the retired legacy app's accounts, deck, feeds, privacy lock. |
|
||
| `platform/PlatformInfo.kt` | `PlatformInfo.current` / `host`, `isMacOS`, `isWindows`, `isLinux`, `isGnome`, `isKde`. |
|
||
| `platform/SavedWindowBounds.kt` | Window position/size persisted to `window-bounds.properties`. |
|
||
| `platform/WaylandDisplay.kt`, `platform/AppWindow.kt` | Native Wayland: `WLToolkit` picked in `main()` on a Wayland session (`AMETHYST_X11=1` opts out); `AppWindow` is Compose's `Window` elsewhere and a Swing-drawn frame there. Open new top-level windows with `AppWindow`, never `Window`/`ComposeDialog`. |
|
||
| `platform/DesktopUiScale.kt` | X11 at a fractional scale (`Xft.dpi` 120 = 125%, `AMETHYST_SCALE` by hand): AWT draws whole scales only, so `AppWindow` raises Compose's density by the rest. `LocalDensity` is then not AWT's scale: convert AWT window/event coordinates with `awtPixelScale(component)`, and size AWT windows in AWT units. |
|
||
| `account/`, `tor/`, `network/`, `service/`, `ui/media/` | Account storage, Tor, HTTP, scheduled posts, video/audio players. |
|
||
| `update/` | Self-update: `InstallChannel` (how this copy was installed: DMG, Homebrew, MSI, zip, AppImage, tarball, .deb/.rpm, Flatpak), `DesktopUpdateInstaller` + `UpdateHelperScripts` (stage, then swap after quit), `UpdateDownloader` (SHA-256 checked). The checker is commons `appupdate`; dev runs need `-Damethyst.update.devCheck=true`. |
|
||
|
||
A new shared feature that needs a desktop behaviour = a member on one of the ports above
|
||
(default in the interface, override here), not a desktop branch in the shared screen.
|
||
|
||
## Shortcuts and menu bar
|
||
|
||
- One `Command` enum in `DesktopMenuBar.kt` drives both. `Command.shortcut` is Cmd+key on macOS,
|
||
Ctrl+key elsewhere (`isMac = PlatformInfo.host == Platform.MACOS`).
|
||
- macOS: the system `MenuBar` carries the shortcuts. Linux/Windows get **no** menu bar (Swing draws
|
||
it inside the window); shortcuts go through `handleShortcut` from the window's `onPreviewKeyEvent`.
|
||
- Deck commands are ignored unless `DeckCommandBus.active`; held chords are de-duplicated
|
||
(`AUTO_REPEAT_WINDOW_MS`).
|
||
- Menu labels are `Res.string.*` (commonsUI strings, per AGENTS.md "Strings").
|
||
|
||
## OS detection
|
||
|
||
`PlatformInfo.current` honours an override (`-Damethyst.platform=GNOME` or `AMETHYST_PLATFORM`) to
|
||
preview another OS's theming; `PlatformInfo.host` is the real OS — use it for anything that touches
|
||
the actual system (menu bar, Cmd key, shell-outs).
|
||
|
||
## Keys
|
||
|
||
Keys go to the OS keyring through `SecureKeyStorage`
|
||
(`commons/src/jvmMain/.../commons/keystorage/SecureKeyStorage.kt`, `java-keyring`). Without one they
|
||
go to a NIP-49 password file (`PasswordKeyFile`), and a login can opt out of keeping its key. The
|
||
status, prompts and Settings › Key storage are shared (`KeyStorageController`, provided by
|
||
`DesktopAppRoot.keyStorage`); see `commons/plans/2026-10-12-desktop-key-storage.md`.
|
||
|
||
The keyring breaks in release builds only, so test the packaged app: `Amethyst --keyring-self-test`
|
||
(CI runs it through `keyring-self-test.sh` in the repo's top-level scripts folder). ProGuard must keep jkeychain's native library and
|
||
dbus-java's ServiceLoader transport, and drop the signatures of jars it rewrites (hkdf):
|
||
`desktopApp/compose-rules.pro`, `verifyJkeychainNativeSurvivesProguard`,
|
||
`verifyDbusTransportSurvivesProguard`.
|
||
|
||
## Run and package
|
||
|
||
```bash
|
||
./gradlew :desktopApp:run # JDK 21 toolchain (jvmToolchain(21))
|
||
./gradlew :desktopApp:run -Damethyst.dataDir=/tmp/amy-test # separate profile; -Damethyst.debug=true for DEBUG logs
|
||
./gradlew :desktopApp:packageDmg # macOS; packageMsi (Windows), packageDeb / packageRpm (Linux)
|
||
```
|
||
|
||
- `mainClass = "com.vitorpamplona.amethyst.desktop.app.AmethystDesktopKt"`. Data lives in
|
||
`~/.amethyst/app` unless `amethyst.dataDir` is set.
|
||
- Packages land in `desktopApp/build/compose/binaries/main/` (release variants in `main-release/`).
|
||
- **Stop a running app before rebuilding shared modules.** A build rewrites the jars the running
|
||
`:desktopApp:run` JVM loaded and it crashes with `NoClassDefFoundError`. Relaunch it afterwards.
|
||
- Build fails? Check `java -version` (21+), then that `./gradlew :commons:compileKotlinJvm` resolves quartz and compiles.
|
||
|
||
## Verify a UI change by running it (headless too)
|
||
|
||
The window renders the shared UI, so this is how an agent checks a `commonsUI`/`commons` change for
|
||
real (AGENTS.md "Run the app before calling a UI change done"). `tools/desktop-run/` does it without
|
||
a display or a window manager: `run-headless.sh start` runs Xvfb + `:desktopApp:run` on a scratch
|
||
profile (`/tmp/amethyst-desktop-run/profile`), waits for the first paint and saves `shot.png`;
|
||
`Drive.java` (pure JDK `java.awt.Robot`) clicks, types, presses keys, scrolls and takes screenshots.
|
||
Read the screenshots with the image-reading tool. Details: `tools/desktop-run/README.md`.
|
||
|
||
- Log in with a public `npub` for a read-only session. A throwaway Sign Up account is only for flows
|
||
that must sign, and only when asked, because it publishes to real relays.
|
||
- The screen size picks the shell: `AMETHYST_SCREEN=480x900x24` gives the phone layout (bottom bar),
|
||
the default 1600x1000 gives the permanent drawer + docked notifications. Use a new
|
||
`AMETHYST_DISPLAY` per size.
|
||
- Linux packages bundle the JetBrains Runtime (build on `jbrsdk` 21, see BUILDING.md "Wayland and X11
|
||
on Linux"); `scripts/wayland-smoke-test.sh <launcher>` checks one on a sway with no Xwayland.
|
||
- Under Xvfb: no GL (the script sets `SKIKO_RENDER_API=SOFTWARE`), no keyring (password-file
|
||
fallback), no native video player ("Can't play this video"). Those are the environment, not your
|
||
change. `run-headless.sh logs` lists exceptions first.
|
||
- Never `pkill -f AmethystDesktopKt` from a tool shell: it matches the shell itself. Use `stop`.
|
||
|
||
For a check that must keep passing, write a Compose UI test (`createComposeRule()`, see
|
||
`desktopApp/src/jvmTest/.../desktop/ui/BlockInteractionsUiTest.kt`); CI runs them under
|
||
`xvfb-run --auto-servernum ./gradlew :desktopApp:test`.
|
||
|
||
Tests: `./gradlew :desktopApp:test` (a plain JVM module: sources sit in `src/jvmTest` but the task
|
||
is `test`); `desktopApp/src/jvmTest/.../desktop/testrelay/` has an in-process relay fixture
|
||
(`LaunchFixtureRelay`).
|