Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Desktop app: framework, native build and testing (design plan)

Status: the build has started (D-76); the rest of this plan is still a proposal. D-76 records the platforms, the Nucleus shell with a plain dev window, our own editor adapter and the screens. The open questions it lists (native image or JVM for release, a shared Compose module, database encryption, minimum OS versions) are not decided. Before D-76 this was a future plan, not a decision. Desktop is Phase 5 in 04-roadmap.md and out of Release 1 scope (D-07). This plan compares ways to build it, with the goal of a fast, lightweight app. Nothing here is agreed until it is recorded as a new D-xx. Several of the points below conflict with D-01 (native UI per platform) and D-12 (the editor is built on the platform text view). If we choose a Compose desktop app, new decisions must say how they apply to desktop.

Research dates: 2026-10-07, with options E and F added 2026-10-08. Facts about third-party projects come from their docs, release notes and community threads. The spike results below replace those claims where we measured something ourselves. Desktop has to run on Linux, macOS and Windows, so option C (native macOS only) is out.

Spike results (2026-10-07, macOS)

Branch spike/desktop-nucleus. It has three modules: app/desktopUi (the screens, editor and database setup, with no Nucleus), app/desktopApp (the Nucleus release shell and native build) and app/desktopDev (a plain Compose window for development, see surprise 1). Together they contain a notes list and an editor on the shared NotesCore and EditorSession, Nucleus 2.5.18 with the tao backend, Compose Multiplatform 1.12.1, and SQLDelight with sqlite-jdbc writing to a database file. Everything below was measured on an M-series Mac (8 cores, 16 GB) in a session with no screen access, so nothing was checked by eye. Linux, macOS and Windows ran in CI on 2026-10-08 (see the table below).

What worked

  • GraalVM native image with SQLDelight. Nucleus downloaded GraalVM CE 25.0.4 itself. The sqlite-jdbc native-image feature was picked up without any configuration. Inside the native binary, the smoke run (JOTRY_SMOKE=1) edits a note through EditorSession, saves it, reopens the file with a second core, searches, reads history and exports. It passes. No reflection config was written by hand.

  • Native build time: about 1 min 10 s, with a peak of 2.9 GB of build memory (on 16 GB, native-image uses 8 GB at most). Nucleus’s docs say 5–15 min on CI runners.

  • Startup and memory (time to first frame and resident memory, the same app on each runtime):

    Native imageJVM (jlink’d runtime)
    First frame, warm~235 ms~700 ms
    First launch after a build~2.9 s (macOS checks the new binary)~2.5 s
    Idle memory, empty database~127 MB~246 MB
    Idle memory, 10,000 notes~195 MB (142 MB with -Xmx64m)~273 MB
    App size126 MB (binary 87 MB)159 MB

    Seeding 10,000 notes through NotesCore took 13.7 s in native. The 10,000-note memory is mostly the list loading every NoteSummary at once, which the real app should page.

  • The editor on BasicTextField. It holds the visible text and sends each change to EditorSession.replaceText as a prefix/suffix diff. Styling is drawn from the session’s RichText through a VisualTransformation. Cmd+B, Cmd+I, Cmd+Z and Cmd+Shift+Z go to the session before the text field sees them, so the field’s own undo never runs. That matches the phones (D-12’s rule that platform undo is routed to the session).

  • UI tests are fast and simple. Five editor tests and three database tests run in about 1 s with runComposeUiTest, with no window, emulator or simulator.

  • Hot reload works with the tao backend. hotRun brought the app up, which contradicts the Slack report. Through the hot-reload MCP server, an agent can reload changed code into the running app, and status, get_logs, get_ui_error and restart work.

  • Configuration cache, Kotlin 2.4.20 and Compose 1.12.1: no conflicts with the existing build.

CI on all three OSes (GitHub-hosted runners, .github/workflows/desktop-spike.yml, run 37787707391, before the module split):

Linux (ubuntu-latest)Windows (windows-latest)macOS (macos-latest)
JVM testspasspasspass
Native build5 min 8 s, 4.0 GB peak9 min 19 s, 3.4 GB peak12 min, 2.1 GB peak
Native smoke test (SQLDelight)passpasspass
App folder168 MB (binary 107 MB)136 MB (binary 92 MB)126 MB
First frame, warm~170 ms after main (~920 ms since the process started, under xvfb-run)~180 ms~420 ms
Resultpasspasshung on exit in the 2nd startup run (surprise 8)

On Linux, about 750 ms pass between the process starting and main. That may be xvfb-run or loading the larger binary, and it needs checking on a real desktop.

Surprises (each one is fixed or worked around in the spike)

  1. The MCP server can’t see inside a Nucleus window. take_screenshot, get_semantic_tree, click and type_text time out with the tao window, and list_windows shows it with no title. A stock Compose desktop app (AWT window, no Nucleus) on the same machine and versions worked fully: tree, screenshot and window title. So with Nucleus, an agent can reload and restart the app but can’t look at it or drive it. Workaround in the spike: the screens live in app/desktopUi, which has no Nucleus dependency, and app/desktopDev shows them in a plain Compose window for development. Run it with ./gradlew :app:desktopDev:hotRun -Pcompose.reload.jbr.autoProvisioningEnabled=true, and its MCP server with ./gradlew :app:desktopDev:hotMcpServer. Through that server an agent read the UI tree as JSON (with the test tags, the editor text and the checkbox states), unchecked a task, typed into the editor, and saw the shared editor continue the checklist. The autosave then reached SQLite. Reading the tree costs far fewer tokens than screenshots. Window-level behavior (the tao title bar, menus, tray) is still checked only in app/desktopApp.
  2. A stock Compose window doesn’t work alongside Nucleus. With Nucleus on the classpath, application { Window { } } crashes (“addObserver must be called on the main thread”). Nucleus redefines the main thread. You can’t fall back to a plain window in the same app, or use one for debugging.
  3. About 85 MB of other platforms’ native libraries ended up in the binary. Native-image embedded Skiko for both Mac architectures, Windows ANGLE, Linux tao, and sqlite-jdbc builds for about 20 OS/CPU combinations, making a 149 MB binary. An -H:ExcludeResources pattern in app/desktopApp/build.gradle.kts keeps only the current platform’s libraries, which gives 87 MB. Nucleus doesn’t do this itself. optimization = SIZE saved only 6 MB and doubled the build time, so it’s not used.
  4. The JVM build left out java.sql. The jlink’d runtime doesn’t include it and sqlite-jdbc needs it, so the JVM app crashed on open until modules("java.sql") was added. Native image wasn’t affected.
  5. Toolbar buttons stole the editor’s focus, and the selection went with it, so Bold did nothing. The buttons are now not focusable (focusProperties { canFocus = false }). Every desktop toolbar and menu action needs this.
  6. Compose text fields can’t hold inline content. The phones put checkboxes in the text as a span or attachment. On desktop, list and task paragraphs get a text indent, and checkboxes, bullets and numbers are drawn in the gap from the text layout. It works and is tested, but it’s a different design from D-12’s adapters.
  7. Hot reload needs JetBrains Runtime 25. It downloads one with -Pcompose.reload.jbr.autoProvisioningEnabled=true. On macOS, the JVM run and hotRun tasks need -XstartOnFirstThread for tao, but that flag hangs a plain AWT window.
  8. A DecoratedWindow needs a TitleBar. Without one, the content is drawn under the macOS window buttons (seen in a screenshot). Separately, the native build sometimes hangs when the app exits right after its first frame. Locally that happened in 2 of 5 runs with a TitleBar and 0 of 5 without one, but on the macOS CI runner it also hung without one. Exiting 2 s after the first frame worked 5 of 5 times, and the JVM build never hung, so it looks like a startup race that users quitting normally won’t hit. Measurement runs now wait 1 s before exiting. Worth reporting upstream.
  9. Smaller things: GRAALVM_HOME on the dev machine pointed at Temurin, which would override Nucleus’s own GraalVM, so builds run with it unset. JDK 25 warns that sqlite-jdbc’s System.load will be blocked in a future release unless --enable-native-access is set. SQLDelight’s checked-in .db schema snapshots have no user_version, so the migration test sets it.

Still open after the spike

  • Memory on Linux and Windows (CI measured startup only), and startup on real desktops rather than CI runners.
  • Code signing and notarization, installers (DMG/NSIS/DEB), and auto-update weren’t tried.
  • IME composition (Chinese, Japanese), paste, and links in the desktop editor.
  • Nucleus upgrades: issue #671 tracks Compose 1.13 breaking changes, and #585 reports high memory use when switching themes.

ComposeTextEditor spike (2026-10-08, macOS, local)

Question: bring in ComposeTextEditor 3.0.3 (MIT, desktop artifact com.darkrockstudios:composetexteditor-desktop), or keep our own editor (DesktopEditor, a BasicTextField with a VisualTransformation)? The spike is LibraryEditor.kt in app/desktopUi, next to DesktopEditor.kt and not wired into the screens, with five tests in LibraryEditorUiTest. GitHub CI is out of credits, so everything ran locally.

What the spike does. The library is only the text surface (layout, caret, selection, IME, clipboard). EditorSession stays the single model. The library’s editOperations (insert, delete, replace) are mapped from line/char to flat offsets and forwarded to session.replaceText. Its own list and block behaviors are switched off (editBehaviors.clear()), or Enter on a checklist line would be continued twice. Undo, redo, Bold and checklist come from the session, and the session’s styled document replaces the library’s text with setText. Cmd/Ctrl+Z, Y and B are caught before the library sees them. All five tests pass, and the existing eight still pass.

What it showed

  • The library owns a second document model: lines of AnnotatedString, rich spans, block spans (lists, tasks, quotes, fences, tables), a Markdown dialect with its own converter, and its own undo history. Using it as a pure text surface works, but that leaves our RichText, MarkdownCodec and EditorSession as the only model and uses a small part of the library.
  • Every session-driven change (undo, redo, formatting, Enter in a list, anything with resetText) calls setText. That drops the library’s IME composition and its own state, and it is visible whenever the user is mid-word. DesktopEditor avoids this by only replacing the text when the session says so.
  • The library’s own undo history still records every edit. Cmd+Z is routed to the session, but the library’s context menu or touch toolbar Undo would bypass it (not tried).
  • The checkbox gutter does not port. The library draws tasks, bullets and numbers as its own block spans, and exposes no single TextLayoutResult to position our gutter on. Using its tasks means using its block model, and so a second Markdown codec to keep in step with MarkdownCodec (fidelity risk).
  • Text typed after a bold run only keeps the style through the library’s cursor styles, which differ from the session’s typingStyles. Not tested.
  • Cost: a 1.6 MB jar, one new transitive dependency (kotlinx-datetime), and a single-maintainer project (about 74 stars). The README itself says it may not be a permanent solution once BasicTextField2 can render rich text. Native image compatibility is untested.

What we’d gain. Lazy line layout for very large notes, a context menu, selection handles, accessibility and tuned input handling (dead keys, CJK composition) that we would otherwise build or test ourselves. DesktopEditor has IME composition, paste and links on its gap list.

Not measured: native image, typing latency on a 10k-line note, real key events and IME (the tests drive the state API), the library’s context menu.

Recommendation (to confirm): keep our own editor. Bringing the library in as a text surface keeps EditorSession intact, but it adds a dependency, a second undo history, setText resets and no checkbox support, in exchange for features a notes app rarely needs at note size. Revisit if BasicTextField shows real problems with IME or large notes. The spike code can stay on the branch as the reference for that.

Goal

A desktop Jotry that:

  • Starts fast and stays small in memory. The bar is “feels like a native notes app”, not “feels like an IDE”.
  • Uses the same NotesCore, database and file format as the phones, so notes round-trip safely (the Phase 5 exit criterion).
  • Can be tested well: UI tests in CI, plus an agent (Claude Code) that can launch, inspect and drive the running app while we work on it.

What we start with

  • Shared core: app/sharedLogic already has a jvm() target with the SQLDelight sqlite-driver (used for host tests today). core builds for JVM, JS and Wasm. A JVM desktop app can call NotesCore with almost no new shared code. A Kotlin/Native macOS target (macosArm64) would need adding, but it uses the same SQLDelight native driver as iOS.
  • Android UI (app/androidApp, about 19.5k lines with tests) is Jetpack Compose, but it is Android-only code. Moving screens into a shared Compose Multiplatform module means removing Android-only APIs (resources, ViewModel wiring, intents, the file picker, Gemini Nano titles).
  • The editor is the hard part. Per D-12, Android uses EditText (JotryEditText, EditorSpans) and iOS uses UITextView. Neither exists on desktop Compose. The formatting logic is shared (EditorSession, covered by EditorSessionTest), so each platform only needs a text-view adapter. On desktop Compose that adapter has to be built on BasicTextField, or a Swing text component hosted through SwingPanel. Nucleus’s tao backend (below) has no AWT, so it rules out SwingPanel.
  • iOS UI (app/iosApp, about 5.2k lines of Swift) is SwiftUI with a TextKit editor. TextKit came from AppKit, so the editor is the part of the iOS app that ports to macOS most directly.
  • app/desktopApp and app/sharedUI are template modules from the project wizard. They aren’t built (see settings.gradle.kts).

Options

A. Compose Multiplatform desktop on the JVM (official JetBrains plugin)

The default path. Ship a jlink’d JVM with the app (DMG/MSI/DEB through the Compose Gradle plugin).

  • Speed and size: the weakest of the options. Cold start is typically around a second or more, and idle memory is a few hundred MB, because a full HotSpot JVM and Skia are bundled. JDK 25+ AOT cache (Project Leyden) and ProGuard reduce startup, but not to native levels. Measured in the spike: about 700 ms to the first frame and about 250 MB idle.
  • Tooling: the best of the options. Normal IntelliJ debugging, Compose Hot Reload, and the hot-reload MCP server (see “Agent access” below) all target this setup.
  • Risk: low. It’s JetBrains’ supported path.

B. Compose desktop with Nucleus (JVM for development, GraalVM native image for release)

Nucleus is an MIT-licensed layer on top of Compose Multiplatform desktop. It isn’t a different UI toolkit. It adds:

  • Native windowing through a Rust tao backend with no AWT (the AWT backends are deprecated, and the docs say they are removed in 2.6), decorated windows, a native menu bar, tray, notifications, deep links, file associations and single-instance locking.
  • Packaging for 18 formats, store publishing (Mac App Store, Microsoft Store, Snap, Flathub), signing and notarization, and an auto-updater (GitHub or S3, SHA-512, delta updates).
  • GraalVM native image as a one-line switch, with reachability metadata generated automatically for dependencies. Its docs list SQLite, Ktor, kotlinx.serialization and Compose as working without extra configuration.

Reported numbers (from the project, not independent):

  • A Hello-World Compose window cold-starts in about 0.2 s using about 30 MB of RAM (Windows 11).
  • In their benchmark app (a heavy CPU and rendering stress test on an M4 Mac), native image reached a peak of 269–326 MB of RAM, compared with 616–695 MB on the JVM. App size was about the same (119–174 MB), because Skia dominates. A notes app should land well under those peaks, but it won’t get close to a Swift app’s size.

Concerns:

  • Maturity: about 400 stars and 28 forks, mostly one maintainer, version 2.5.x, with a 3.0 branch in progress. Some real apps use it (Tasks.org, AB Download Manager, CrossPaste). If the project stalls, we’d need to fall back to the official plugin. That’s realistic, because the UI is plain Compose, but we’d lose native windowing and native image.
  • Version coupling: it needs Compose Multiplatform 1.12.x, Kotlin 2.4.10+ and Gradle 9+. We’re on Compose 1.12.1 and Kotlin 2.4.20, so we fit today, but upgrades become gated on Nucleus releases.
  • Hot reload works in the spike, despite a Slack report. The hot-reload MCP server, though, can’t screenshot or read the UI of a tao window (see the spike results).
  • Native image limits: a closed world (no runtime reflection or class loading the build can’t see), no cross-compilation (build on each OS in a CI matrix), slower and memory-hungry builds, and debugging a native binary is much harder than on the JVM. In practice: develop and test on the JVM, and run a smoke test of the native build in CI.
  • sqlite-jdbc and SQLCipher: plain SQLite is listed as working. An encrypted database through SQLCipher on the JVM, and then inside native image, is a separate unknown (see the “Revisit if” note under D-09).

C. Native macOS app (SwiftUI/AppKit) on a Kotlin/Native core

Add a macosArm64 (and macosX64 if needed) target to sharedLogic, and build a macOS app that reuses the iOS SwiftUI code and the TextKit editor (NSTextView).

  • Speed and size: the best of the options. A Swift app with a static Kotlin/Native framework starts in a fraction of a second and idles at tens of MB. No GraalVM step is needed: Kotlin/Native already compiles ahead of time.
  • Fits D-01 and D-12 directly: native UI and a platform text view.
  • Reuses a lot: iOS screens, AppModel, the editor’s TextKit code, and the XCUITest setup. Apple’s on-device Foundation Models are available for generated titles on macOS too. Compose desktop has no equivalent.
  • Downside: macOS only. Windows and Linux would need option A or B later, so that would be a fourth UI.
  • Testing: XCUITest on macOS works like iOS, but it’s slow, and agent access goes through the macOS accessibility tree (a computer-use or AX-based MCP) rather than a Compose-aware tool.

D. Others considered

  • Tauri (a webview with a Rust shell): small binaries (about 10 MB) and low memory, but the UI and editor would be rewritten in web tech, and the core would run as Kotlin/JS or Wasm with a new SQLite bridge. That’s a third UI stack with no reuse. Not recommended.
  • Electron: heavy, the opposite of the goal. Rejected.
  • Compose on Kotlin/Native for desktop (no JVM at all): Compose Multiplatform has no supported Kotlin/Native desktop target, so this isn’t available.

E. A Rust UI toolkit with the Kotlin/Native core behind a C ABI

Analysis only (2026-10-08), nothing was built. Candidates are Slint, egui, iced, Dioxus, Xilem and GPUI. Kotlin/Native would export the shared logic as a static library with a C header, and the Rust side would call it (or the reverse).

  • Cost: high. It needs macosArm64, linuxX64 and mingwX64 targets in sharedLogic (to check against SQLDelight and our dependencies), a hand-written flat C API (no coroutines, Flow or generics across the boundary, manual ownership of strings and lists), and Gradle, Cargo and a C toolchain on every CI OS. It is a fourth UI stack with no reuse of the Android Compose screens.
  • Editor: the Rust toolkits have weak rich text editing and accessibility. Jotry’s editor would be custom work, probably worse than a platform text view or Compose’s.
  • Upside: small binary, fast start, no JVM or Skia, no GraalVM closed-world limits, no dependence on Nucleus.
  • Verdict: not recommended. Revisit only if native image misses the pass bar and all three OSes still matter.

F. Compose on JetBrains’ kotlin-desktop-toolkit (KDT)

kotlin-desktop-toolkit wraps native windowing in a Kotlin API and offers Skia-compatible rendering surfaces. Status read from its README on 2026-10-08, not tried:

  • It is labelled an internal JetBrains project, has no releases, no published Maven artifacts and about 200 stars. It is the foundation of JetBrains’ Air framework. Its README says it “will later” provide the OS integration layer for Compose for Desktop, so Compose integration is planned, not delivered. A third-party summary says its native layer is Rust (not verified).
  • Platform status: macOS is the most complete. Windows has no application menu, no Vulkan, and file dialogs are in progress. Linux X11 is decent, Wayland has no application menu. System tray and accessibility are missing everywhere, and custom text input is missing on Windows.
  • Not usable for Jotry today. Accessibility and text input matter for a notes editor.
  • Switching later: desktopUi is plain Compose with no Nucleus dependency, so the screens, state, EditorSession and tests carry over. Only the window shell (desktopApp, about 40 lines of Kotlin plus Gradle configuration) would be replaced. That is a few days of work on menus, tray and packaging. Going to KDT from Nucleus loses native image, the updater and the packaging formats until KDT or the official plugin covers them. Going from the plain JVM plugin (option A) is cheaper, because KDT may arrive under Compose with a version bump.
  • Plan: don’t build on it. Keep desktopUi free of Nucleus and KDT. Re-check KDT releases and the Compose Multiplatform changelog every few months, and revisit when a Compose-on-KDT artifact exists.

Answers to the questions asked

Is Nucleus good for Jotry? It’s the most complete way to get a Compose desktop app that feels native and starts fast: native windows and menus, installers, auto-update and native image in one plugin. The costs are dependence on a small project, an MCP server that can’t see inside its windows, and no Swing interop (which limits editor options). It’s worth a spike, not a commitment. Keep the UI as plain Compose so that dropping back to the official plugin (option A) stays a build change, not a rewrite.

Is desktop UI testing easier with Compose? Yes, much easier than Espresso or XCUITest:

  • runComposeUiTest (or the JUnit createComposeRule) runs in-process on the JVM. There’s no emulator or simulator, it renders offscreen, and tests take milliseconds to seconds. The finders and assertions are the same semantics API as our Android Compose tests.
  • Roborazzi, which we already use on Android, supports desktop screenshot tests.
  • Limits: these tests cover composables, not the native window shell. Nucleus’s tao window, native menu bar, tray, file dialogs and the native-image binary need a few separate smoke tests. Headless Linux CI may need xvfb. The desktop test API is still marked experimental.
  • If we share screens between Android and desktop in a Compose Multiplatform module, many UI tests can be written once in commonTest. Today the Android UI tests are Android-only.

Can an agent run and debug the app?

  • Compose Hot Reload MCP server (experimental, Hot Reload 1.2 / Compose Multiplatform 1.12): run the hotMcpServer Gradle task and point Claude Code at it in .mcp.json. It can reload code, restart the app or reset the UI, and read logs and UI exceptions. Community write-ups also list screenshots, reading the semantics tree, clicking and typing. Reports show rough edges: a log line on stdout breaking JSON-RPC, and Claude Code failing to connect. It works only on the JVM, in development, not on a native-image build. With Nucleus’s tao window, reload, restart, logs and errors work, but screenshots, the semantics tree, clicking and typing time out (spike result).
  • A small in-app test hook we control: a debug-only local endpoint (or a stdio MCP that’s part of the app in debug builds) that dumps the semantics tree and performs actions through the same API the UI tests use. This is more work, but it works for any backend and doesn’t depend on hot reload. It must never ship in release builds.
  • OS accessibility tools (computer use, AX-tree MCPs) work on any option, including native image and option C, if the app exposes accessibility correctly. Nucleus says its accessibility layer is checked against macOS AX, UI Automation and AT-SPI in CI.
  • Debugging: a full JVM debugger on JVM runs. A native-image binary can only be debugged with gdb/lldb and debug info, so reproduce bugs on the JVM build.

Is GraalVM native image the way to “fast and lightweight”? For a Compose app, it’s the biggest available win: much faster cold start, and roughly half the memory of the JVM. It doesn’t shrink the app much (Skia is large), and it adds build complexity. Only option C (native macOS) beats it, and only on the Mac.

Recommendation (to confirm)

  1. Decide the platform priority first. Decided by the user on 2026-10-07: Linux, macOS and Windows together, so option B with option A as the fallback.
  2. Spike before any D-xx. Done on macOS (see spike results). The original brief was: Measure Nucleus with native image against the JVM on a real Jotry slice, not Hello World:
    • Build: NotesCore with SQLite (and SQLCipher, if encryption is in scope by then), a notebook list, a notes list, and a BasicTextField editor wired to EditorSession with bold and a checklist.
    • Measure on the Mac (and on Windows if it’s in scope): cold start to first frame, idle RSS, RSS with 10k notes, typing latency, app size, and native build time in CI.
    • Check: hot reload and the hot-reload MCP under the Nucleus tao backend, runComposeUiTest plus Roborazzi in CI, and whether sqlite-jdbc works in native image.
    • Pass bar (proposal): cold start under 0.5 s, and idle RSS under 150 MB with 10k notes. Result on macOS: the native start passes (~235 ms). Memory passes only with a heap cap (142 MB with -Xmx64m, 195 MB without one), and paging the notes list would help more. Linux and Windows are still to run.
  3. Shared Compose UI is a separate decision. If desktop uses Compose, moving Android screens into a shared Compose Multiplatform module would give one UI and one set of UI tests for Android and desktop. It touches every Android screen, and it needs a desktop editor that matches D-12’s behavior.

Decisions needed

  • Which desktop platforms come first? All three (the user, 2026-10-07). This still needs to be recorded as a D-xx that refines D-01 and D-07.
  • Nucleus (B) or the official plugin (A)? Agent access is the main trade-off: the hot-reload MCP server fully supports only stock windows.
  • The desktop editor: BasicTextField adapter, or something else? This needs a D-xx that extends D-12.
  • Native image or JVM for release builds, based on the spike numbers.
  • Whether Android screens move to a shared Compose Multiplatform module.
    • Rough reuse estimate (2026-10-08, from file sizes, not checked import by import): app/androidApp/src/main is about 4.4k lines in 18 Kotlin files. Roughly 2.5–3k lines (theme, formatting, components, bars, drawer and the notes, search, trash, history and settings screens) could be shared after replacing Android resources, Context/Intent and file-picker calls and the androidx.lifecycle ViewModel. About 1k lines cannot: NoteEditor (601 lines), RichTextContent, MainActivity, JotryApplication and Gemini Nano titles. Desktop also needs its own layout (sidebar, menu bar, shortcuts), not the phone layout.
  • Desktop database encryption, the driver, and the minimum OS versions.

Sources