Development
How to work on DropTerm itself: the dev launcher loop and running without a shortcut. The manual test checklist lives in testing, the module map in architecture.
The reasoning behind non-trivial design decisions — peer surveys, recommendations, rejected alternatives — lives in the research records; start from the decision log.
Development launcher
Don’t point the shortcut at cargo run. COSMIC spawns a custom shortcut from
the compositor’s working directory (not this repo), so cargo can’t find
Cargo.toml, and every keypress would pay for a build check on top. Install a
launcher that always runs the current debug build instead:
just dev-install
That builds the debug profile and symlinks ~/.local/bin/cosmic-ext-dropterm-dev
to the dev image target/debug/cosmic-ext-dropterm-dev-image — a copy of the
dev-identity build that only the just dev* recipes ever rewrite. Set the
development shortcut’s Command to cosmic-ext-dropterm-dev once and leave it.
The -dev suffix keeps an installed release (from sudo just install or a
release tarball) separate: plain cosmic-ext-dropterm on PATH is always the
installed copy, and the two never shadow each other.
The dev build also carries its own runtime identity —
io.github.l1br3_io.CosmicExtDropterm.Dev, selected by the dev-identity
cargo feature the dev recipes build with: its own single-instance slot, bus
name, config file, state directory, and wayland namespace. A dev instance and
an installed release run at the same time without seeing each other —
launching one while the other runs never forwards to it, and each answers
only its own launcher and keybind. just dev (via dev-reload) kills only
the dev instance before starting the new build; an installed release keeps
running untouched, and just act / just toggle always drive the dev
instance.
The identity reaches the desktop integration too: just dev-install also
writes ~/.local/share/applications/io.github.l1br3_io.CosmicExtDropterm.Dev.desktop
(hidden from the app grid, like the release’s own entry), so the window
switcher labels the dev build’s anchor window “DropTerm (Dev)” instead of
“unknown” — and a hidden-tab bell notification from the dev build says
“DropTerm (Dev)” as its app name, so with both instances running you can
always tell which one rang.
The separation reaches the files: the dev build reads and writes
~/.config/io.github.l1br3_io.CosmicExtDropterm.Dev.ron and
~/.local/state/io.github.l1br3_io.CosmicExtDropterm.Dev/, so config-schema
churn on main cannot migrate the daily driver’s settings. To develop against
your real settings, copy them over once:
cp ~/.config/io.github.l1br3_io.CosmicExtDropterm.ron \
~/.config/io.github.l1br3_io.CosmicExtDropterm.Dev.ron
Deliberately a copy, not a share — dev experiments never write back to the real config.
One caveat: the launcher runs the dev image, whose identity only the
just dev* recipes can change. A plain cargo build (or cargo test, or
just check) refreshes the shared debug bin target/debug/cosmic-ext-dropterm
without the dev-identity feature — the launcher ignores it. Running that
shared bin directly (cargo run, just run) is still a release-identity
launch that forwards to (and tugs at) the installed copy; the dev loop always
goes through the launcher or the just dev* recipes.
The edit-build-see loop is then one command:
just dev
just dev rebuilds, refreshes the launcher, kills any running instance and
starts the new binary. Killing first matters: a plain relaunch only toggles the
already-running process, which is still the code from before your edit.
just dev-reload does just the restart, and just dev-install just the build
plus launcher.
To skip the manual step, just dev-watch reruns that loop whenever a file
under src/ (or Cargo.toml) changes:
just dev-watch
It needs cargo-watch
(cargo install cargo-watch). Each save rebuilds and restarts the overlay; a
failed build prints the compiler error, and the next save that compiles brings
it back.
Command map
Which recipe targets what, in one table:
| Recipe | Target |
|---|---|
just dev, dev-reload, dev-install, dev-watch |
Development — the debug build under the .Dev identity, launched as cosmic-ext-dropterm-dev, plus its (app-grid-hidden) desktop file |
just act …, toggle, show, hide, new-tab … |
The dev instance — the verbs ride the -dev launcher and the dev instance executes them |
just dev-applet |
Development — a second panel entry (“DropTerm (Dev)”) under ~/.local, tracking the dev terminal only |
just dev-uninstall |
Development — removes the -dev launcher, the dev desktop files, and the dev applet’s footprint |
just deps |
Prerequisites — checks the C toolchain, pkg-config, the system libraries, and cargo, installing what is missing via the distro’s package manager (apt/dnf/pacman/zypper) or rustup; the build and install recipes run it first, and it is a silent no-op when everything is present |
just build-all + sudo just install |
Installed — the release build of both packages into /usr; a release tarball’s usr/ tree lands in the same places, and sudo just uninstall removes either |
just check |
The gate — fmt + clippy + test across the workspace, each package also once under dev-identity |
just vendor / build-vendored |
Release machinery — the offline dependency snapshot and the offline build proof |
Running locally without a shortcut
No COSMIC shortcut configured yet? You can still open and dismiss the overlay from a local run:
just devbuilds, installs the launcher, and restart-launches the overlay under the dev identity; plaincargo run(orjust run) launches the release identity — it forwards to an installed copy’s instance if one is running.Ctrl+Shift+Qhides it.- Clicking another window hides it too (
hide_on_focus_lossdefaults totrue). - Running the command a second time still toggles.
Testing
Runtime behavior is only testable on real COSMIC hardware; the manual test
checklist every change answers to lives in testing.md. Headless
concerns stay in #[cfg(test)] module tests - just test runs them, and
just check (format, clippy, test) is the change Definition of Done.
The COSMIC validation VM
The applet rows at the bottom of testing.md need a live
COSMIC session; the validation VM is where they walk when no spare
hardware is at hand. The guest is a Pop!_OS 24.04 COSMIC image under
virt-manager/QEMU — /dev/kvm, virtio-gpu with virgl: cosmic-comp
renders GLES and libcosmic falls back to GL, so no guest Vulkan is
needed (cosmic-epoch#3831/#3844). One-time guest setup: install
qemu-guest-agent (the harness’s only channel in — no SSH), auto-login
via greetd so reboots return to the desktop unattended, then snapshot
the stock state and name it; every walk rewinds to it first.
The harness is scripts/vm.sh behind just vm-* recipes:
just vm-status— the preflight doctor (agent, session, disk headroom, snapshots). Run it first, always.just vm-exec/just vm-exec-user— run a command in the guest as root, or as the desktop user inside the session env (runtime dir + session bus + Wayland socket), which is how the CLI toggles behave exactly as from a guest terminal.just vm-put— transfer a file in, sha256 verified on both sides.just vm-screenshot— capture the display for evidence.just vm-snapshot/just vm-restore— clean points around each leg.just vm-push— build both artifacts host-side and install them in the guest exactly as the release tarball lays them down.
The walk, in order:
- Rewind to the stock snapshot; install the native terminal (and its
applet) from the published release tarball —
sha256sum -c sha256sums, copyusr/over/usr, reboot (auto-login returns it). - Add the applet to the panel: COSMIC Settings → Panel (the honest
human route), or the config route — append the applet id to
~/.config/cosmic/com.system76.CosmicPanel.Panel/v1/plugins_centerandpkill -x cosmic-panel; the session respawns the panel, which re-reads its config and spawns the applet. - Drive the rows. The applet’s toggle and running/not indicator ride
the session bus — watch them with
dbus-monitor --sessionin-guest. Scripted input goes through QEMU’s monitor:virsh send-keyfor keys,input-send-eventfor the pointer. Pointer quirks that matter: absolute moves go to the USB tablet (axis 0–32767 across the screen), the guest display runs 1280×800, and a batched move+click drops the button — send the move, then the press, then the release as three separate commands.
What the VM cannot prove: real-hardware timing (GPU throughput, scanout), and the two GUI steps a script should not fake — the Settings → Panel listing and the custom-keybind press belong to a human at the SPICE console.
Releasing
A release is one recipe and a push: just release X.Y.Z cuts and tags
locally, the push publishes (release.yml fires on the tag).
The whole sequence is driven by /release (the dropterm-release
skill, adapted from cosmic-ext-airlock’s release ritual): it derives the
version from the commits since the last tag, drafts the curation for
your review, and runs the recipe that commits locally and never pushes.
Everything that reaches the public world is typed by you, every
time — the two pushes. Before the tag push it
can offer a full-pipeline dry-run rehearsal: push main alone, dispatch
release.yml’s dry-run on it, and only push the tag once the artifact
set assembles green. The steps below are what the command does; they
remain the manual path.
- Curate the release content (uncommitted or committed, the recipe
takes either):
- releases.md - a
## vX.Y.Z (date)section for today, house style; the “Next (unreleased)” pointer, if any, is consumed by it - both
res/*.metainfo.xml- a<release version="X.Y.Z" date="…">entry each - README.md - the Status line and every
releases/download/v…install URL re-pointed at the new version - testing.md - the release-walk rows re-versioned
- ROADMAP.md - fixed bugs retired from the bugs list
- releases.md - a
just release X.Y.Z- verifies all of the above exists for today’s date, bumps both crates’ versions, refreshesCargo.lock, runsjust checkplus the appstream validation CI enforces, commits everything as one release-hygiene commit, and tagsvX.Y.Z. It never pushes - the push is the publish.- Publish:
git push origin main && git push origin vX.Y.Z. release.yml runs the test suite on the release bits, builds the per-arch tarballs andvendor.tar, proves the vendored offline build, guards tag againstCargo.toml, and publishes the GitHub release withsha256sums.
The version is semver: additive features take the minor, fixes the patch. A failed gate mid-recipe leaves the version bumps uncommitted in the tree; fix and re-run.
Architecture
The module map - the two crates, the module inventory, the load-bearing seams, and where the reasoning lives - is in architecture.md.