Repository navigation
feat(macos): menu-bar item and reopen through AppKit via purego - #932
Merged
Merged
Conversation
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
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
On macOS the backend had no AppKit at all, so two things were missing:
--no-windowand the everyday close-the-window-then-click-the-icon case.The backend now runs AppKit's loop on the main thread through purego (new dependency,
CGO_ENABLED=0kept), as an accessory app so it stays off the Dock and Cmd-Tab.internal/appkit:Runpins the main thread, startsNSApplicationand runs the backend beside its loop;Handlewires reopen toShowWindowand terminate (logout, shutdown) tocoord.QuitwithNSTerminateLater, so a logout ends lich cleanly instead of reading as a crash on the next launch;Doruns a call on the main thread. ElsewhereRunjust calls the app.internal/tray/native_darwin.go:NSStatusItemwith 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 intoapp(), whichappkit.Runruns.docs/ceilings.md, theInfo.plist.tplcomment andCHANGELOG.mdfollow.Test plan
GOOS=linux|darwin|windows go build ./... && go vet ./...GOOS=darwin GOARCH=arm64|amd64 CGO_ENABLED=0 go build .go test ./...on Linuxopen -a Lich --args --no-window, then clicking the app opens the window