Skip to content

feat(macos): menu-bar item and reopen through AppKit via purego - #932

Merged
omartelo merged 1 commit into
mainfrom
feat/macos-tray-reopen
Oct 24, 2026
Merged

omartelo merged 1 commit into
mainfrom
feat/macos-tray-reopen

Conversation

@omartelo

Copy link
Copy Markdown
Collaborator

Summary

On macOS the backend had no AppKit at all, so two things were missing:

  • Reopen. Opening lich from Finder, the Dock, Launchpad or Spotlight while it ran with its window closed did nothing: LaunchServices sees the app running and sends a reopen, which a process without an application loop never hears. This hit both a Login Item started with --no-window and the everyday close-the-window-then-click-the-icon case.
  • Tray. The menu-bar item needs AppKit, and lich is built without cgo, so a windowless lich on a Mac showed nothing.

The backend now runs AppKit's loop on the main thread through purego (new dependency, CGO_ENABLED=0 kept), as an accessory app so it stays off the Dock and Cmd-Tab.

  • internal/appkit: Run pins the main thread, starts NSApplication and runs the backend beside its loop; Handle wires reopen to ShowWindow and terminate (logout, shutdown) to coord.Quit with NSTerminateLater, so a logout ends lich cleanly instead of reading as a crash on the next launch; Do runs a call on the main thread. Elsewhere Run just calls the app.
  • internal/tray/native_darwin.go: NSStatusItem with Show lich, the running-session count and Quit, in the window's labels, refreshed like the Linux/Windows tray.
  • main.go: the CLI still answers first; everything after it moved into app(), which appkit.Run runs.
  • docs/ceilings.md, the Info.plist.tpl comment and CHANGELOG.md follow.

Test plan

  • GOOS=linux|darwin|windows go build ./... && go vet ./...
  • GOOS=darwin GOARCH=arm64|amd64 CGO_ENABLED=0 go build .
  • go test ./... on Linux
  • macOS: close the window keeping lich running; the menu-bar item shows, Show lich and Quit work
  • macOS: with the window closed, clicking lich in the Dock or Launchpad opens it again
  • macOS: open -a Lich --args --no-window, then clicking the app opens the window
  • macOS: logging out with lich running neither hangs the logout nor shows the crash notice at the next launch
  • macOS: with the window open, clicking the app only brings it forward

The backend runs AppKit's loop on the main thread as an accessory app, so
opening lich again from Finder, the Dock or a Login Item started with
--no-window reaches it as a reopen and opens the window, logout quits it
the way lich quit does, and the tray menu shows in the menu bar.
@omartelo
omartelo merged commit 7450905 into main Oct 24, 2026
7 checks passed
@omartelo
omartelo deleted the feat/macos-tray-reopen branch October 24, 2026 01:11
omartelo added a commit that referenced this pull request Oct 24, 2026
#932 gave macOS a menu-bar item on AppKit through purego. Its menu now carries
what the Linux and Windows tray does: the running services count, a slot per
running service with Show output, Restart and Stop, and Stop all services. A
slot's submenu entries are told apart by their tag, and the logic behind them
is the one the other trays share. The ceiling that said macOS has no tray
names the eight-slot limit instead, and that these entries never ran on a Mac.
omartelo added a commit that referenced this pull request Oct 24, 2026
* feat: project services, commands lich starts and keeps running

A project can save services: a container, a tunnel, a dev server. lich runs
each one in the project's directory on a PTY of its own, without the user's
rc files and with no shell behind it, so when the command exits the service
has stopped and its last output stays readable until it starts again. Ctrl+C
at its console and Stop both read as a stop, not a failure.

Backend: a project_services table (migration 11), the services.* RPCs and
the services and service-reveal events, internal/services for the state of
every run, terminal.StartService for the PTY, Start with lich at boot and a
graceful stop of every service on quit. Service PTYs are not sessions: they
stay out of LiveCount and the turn-state catch-up. The tray lists the running
services with Show output, Restart and Stop.

Window: a Services row in the sidebar footer above the profile, closed to a
count and open upward to a row per service; the same list from a Services
button on the collapsed rail, which also shows the profile now; a console in
place of the session terminal; Settings > Services per project; a Start the
project's services action in the palette; the close dialog counts services,
so closing a window with only services running asks instead of quitting.

TestOpenDropsTheTerminalThemeSetting drops project_services with the other
tables later migrations add, because the version-0 database it rebuilds never
had it. TestOnlyProjectScopedSectionsStayLitInAProject lists Services, a new
project-scoped section.

* feat(services): tie a service to its project's tab, and polish the footer

A service now lives as long as its project's tab. Closing the tab stops the
project's services (killed past a 10 second grace), reopening the project
starts the ones saved to start with lich, and lich starting brings up only
those whose project has a tab open. Home has no tab to close, so its services
run as long as lich does; Settings > Services edits them with Global selected.
The tray only ever lists services of open projects, so Show output no longer
reopens a tab.

In the window, the running dot of the Services row, the rail badge and the
console bar beats: a ring grows out of it for 0.8 seconds at the spinner's
rate, then rests with no frames until the next beat 2.4 seconds on. A
continuous pulse at a tenth of that rate held the window at 6% of a core.
The rows keep a plain dot. Start all is an icon with its words in a tooltip,
and the service list no longer reserves a scrollbar gutter, so its rows end
where the cards above them do.

* feat(services): list the running services in the macOS menu bar too

#932 gave macOS a menu-bar item on AppKit through purego. Its menu now carries
what the Linux and Windows tray does: the running services count, a slot per
running service with Show output, Restart and Stop, and Stop all services. A
slot's submenu entries are told apart by their tag, and the logic behind them
is the one the other trays share. The ceiling that said macOS has no tray
names the eight-slot limit instead, and that these entries never ran on a Mac.

* test(relay): count a delivery's writes once it is submitted, not stamped

TestADoneWithOneErrandOpenStallsItWithoutANotice failed on CI with
"writes = 2, want no notice". stampDelivery marks a ticket delivered before
the paste is written, and the Enter follows a beat later, so the test, which
counted the target's writes as soon as it saw the ticket delivered, could
count the paste alone and then read the Enter as a notice. Holding the Enter
back 30ms in the fake terminal made it fail 5 runs out of 5; waiting for the
ticket to be submitted, which is after both writes, passes all 5 with the same
delay. The two-errand test beside it counted the same way and waits the same
way now.

The test asserted something the contract never promised: that a delivered
ticket's writes are all in.
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.

1 participant