Browse / github.com/storytold/craft-launcher / patch
feat(macos): option to hide the Dock icon while running from the menu bar
macOS: option to hide the Dock icon when running from the menu bar Version: ArtCraft Launcher 0.4.1 (macOS universal build) What happens: With "Keep running in the tray when the window is closed" turned on, the launcher shows its icon in the menu bar, but the Dock icon also stays visible the whole time, even after the window is closed. So the launcher takes up a spot in both the menu bar and the Dock. Request: One of these would solve it: 1. A "Show in Dock" toggle in Settings under Running in the Background. 2. Hide the Dock icon automatically when the window is closed and the launcher is running from the menu bar, then show it again when the window
- Status
- open
- Score
- 0.00
- Adopters
- 0 other operators
- Author
- thebruge (imported from pull request #555, open)
- Base
307efcf45a080af9643e750d43034eb25540cef8- Size
- +162 −10 in 20 files
- Review
- pending
- Shared
- 2026-10-28
- Id
52bcb0587a078462792ab82c
How it went
No other operator has reported using it yet.
Diff
raw · .DS_Store AGENTS.md apps/artcraft-launcher/Cargo.toml apps/artcraft-launcher/src/dock.rs apps/artcraft-launcher/src/main.rs crates/native/src/self_update.rs crates/ui-egui/src/app.rs crates/ui-egui/src/dialogs.rs crates/ui-egui/src/i18n/cs.tsv crates/ui-egui/src/i18n/de.tsv crates/ui-egui/src/i18n/es.tsv crates/ui-egui/src/i18n/fr.tsv crates/ui-egui/src/i18n/hu.tsv crates/ui-egui/src/i18n/ja.tsv crates/ui-egui/src/i18n/pl.tsv crates/ui-egui/src/i18n/pt-BR.tsv crates/ui-egui/src/i18n/ru.tsv crates/ui-egui/src/i18n/zh-CN.tsv crates/ui-egui/src/settings.rs crates/ui-egui/tests/ui.rs
new file mode 100644GIT binary patchliteral 8196zcmeHLv2N5r5S_goA%O-&K@<sOsHhMT1tm^oC_({JDjJ0EF1kd?;S+a}(&3sOQJ_?*z=-@A+bZJ2152*MB-t4ZeowcoWh$yos??gK@`<~yf*BKL$**F-j5v>r>5-zqkuA^&dz?B}b|dVb^qtU*4}n2P=J+Sag`OdHWUpbDr0s(>n>3aA4Ag93PGb6e_o?>n#ds(>nRzDHY)JA;HBq^|o}@9v$dx2>{#2ux<Dob%4!DyiL6=owcc$(^U_~ry5^k7?%!z!r`!~zx23Z#os3H-<B^T8P>e-KUSh+^q|R!u3aA221-N!!q=z)5oZh$8@AqT>q{y<telfr}z9=Y=QkM)!FX3Xf^nCVXVoxnb|eef95F3rU^gx7~Bn7{T(eERM21AmC!^b6cK7WZm}zOFMKx1A$I=egQp-h&XI|4vi?mq$5i3p;rLIz2-E<Zzh<SFRP(XawyMg9enPj2+^B<zf!j+lHTFSHGyJw`9%0}8I*<JlpQ^hPb4U*DF@i_pc?_`^bFkQsc-%~1S{{Kv;_;{6zk0K`2f?8t)k6YgWgLN2DfhR+*!}6!^p)bO(bf|MXG^qUo_gccZ%UJsyxO?dNd3Z--zu@e?<KWUHZ;L<W*2dkDy!LOe(zeTK!J1wwN0%qpv@yE}xKjie{!|vms)p-%j5whuuz33!<7-XrF5g$^x~8SMou;5a>!-jD-4)fxr)A%pga03ZKd*mDDc+EjrHtH6R|zQXnYzv+nQz7xw;?xGJCuoUefCXIt3@;(Tr0$mn4t!SxmwH;yZvwJGR0gvV_;4(rQtJC=A~c>TT()O+}7Pg>{wuKll)!?|)tZZI@bszlT#i6951Jliteral 0HcmV?d00001@@ -155,7 +155,9 @@ Packaging facts the code relies on: `ai.storyteller.artcraft-launcher`, which toast notifications need. `portable.txt` beside the exe switches on portable mode (`ArtCraftLauncherData\` next to the exe). - The macOS bundle is `ArtCraft Launcher.app` (executable `Contents/MacOS/ArtCraft Launcher`), a- normal Dock app that can also sit in the menu bar: `LSUIElement` must never be set.+ normal Dock app that can also sit in the menu bar: `LSUIElement` must never be set. The user+ can hide the Dock icon while the window is put away in the menu bar; that switches the+ activation policy at run time (`apps/artcraft-launcher/src/dock.rs`). - Linux: the tray uses StatusNotifierItem over D-Bus and notifications use `org.freedesktop.Notifications`, both in pure Rust (no GTK, appindicator or libdbus). - Inside the Flatpak launcher (`process::in_sandbox()`), apps normally install as host Flatpaks@@ -39,13 +39,13 @@ zbus = "5" [target.'cfg(any(target_os = "macos", target_os = "windows"))'.dependencies] tray-icon = "0.25" -# macOS only: which desktop (Space) the window shows on (spaces.rs) and the "Reduce motion"-# setting (motion.rs). Safe AppKit calls only, and already in Cargo.lock through winit and-# tray-icon; pinned exactly to those versions.+# macOS only: which desktop (Space) the window shows on (spaces.rs), whether there's a Dock icon+# (dock.rs) and the "Reduce motion" setting (motion.rs). Safe AppKit calls only, and already in+# Cargo.lock through winit and tray-icon; pinned exactly to those versions. [target.'cfg(target_os = "macos")'.dependencies] objc2 = "=0.6.5" objc2-foundation = { version = "=0.3.2", default-features = false, features = ["std", "NSArray", "NSEnumerator"] }-objc2-app-kit = { version = "=0.3.2", default-features = false, features = ["std", "NSAccessibility", "NSApplication", "NSResponder", "NSWindow", "NSWorkspace"] }+objc2-app-kit = { version = "=0.3.2", default-features = false, features = ["std", "NSAccessibility", "NSApplication", "NSResponder", "NSRunningApplication", "NSWindow", "NSWorkspace"] } # Windows only: the "Animation effects" setting (motion.rs). Already in Cargo.lock through # artcraft-launcher-native.new file mode 100644@@ -0,0 +1,21 @@+//! macOS only: whether the launcher has a Dock icon.+//!+//! The launcher is a normal Dock app (`LSUIElement` is never set). With "hide the Dock icon while+//! the window is closed" on, it becomes a menu-bar-only (accessory) app while its window is put+//! away in the menu bar, and a Dock app again when the window comes back. Other platforms have no+//! Dock, so this module only exists on macOS.++use objc2::MainThreadMarker;+use objc2_app_kit::{NSApplication, NSApplicationActivationPolicy};++/// Shows (`true`) or hides (`false`) the launcher's Dock icon, which also adds it to or takes it+/// out of the app switcher (Cmd-Tab). Must run on the main thread, as the UI does; elsewhere it+/// does nothing.+pub fn show_icon(on: bool) {+ let Some(mtm) = MainThreadMarker::new() else { return };+ let policy = if on { NSApplicationActivationPolicy::Regular } else { NSApplicationActivationPolicy::Accessory };+ let app = NSApplication::sharedApplication(mtm);+ if app.activationPolicy() != policy && !app.setActivationPolicy(policy) {+ log::warn!("couldn't {} the Dock icon", if on { "show" } else { "hide" });+ }+}@@ -5,6 +5,8 @@ #![cfg_attr(windows, windows_subsystem = "windows")] #![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)] +#[cfg(target_os = "macos")]+mod dock; mod logging; mod motion; mod settings_file;@@ -331,6 +333,13 @@ enum OnClose { Quit, } +/// Whether the Dock icon shows (macOS): always while the window is out, and while it is put away in+/// the menu bar unless the user chose to hide it then.+#[cfg_attr(not(target_os = "macos"), allow(dead_code))]+fn shows_dock_icon(window_visible: bool, has_tray: bool, hide_dock_icon: bool) -> bool {+ window_visible || !(has_tray && hide_dock_icon)+}+ fn on_close(has_tray: bool, close_to_tray: bool, window_hides: bool) -> OnClose { if !(has_tray && close_to_tray) { OnClose::Quit@@ -424,6 +433,10 @@ impl Host for DesktopHost { cfg!(target_os = "macos") } + fn has_dock(&self) -> bool {+ cfg!(target_os = "macos")+ }+ fn login_item(&self) -> Option<bool> { Some(login::is_enabled()) }@@ -625,6 +638,9 @@ struct Shell<'a> { /// The "show on the current desktop" setting last applied to the window (macOS). #[cfg_attr(not(target_os = "macos"), allow(dead_code))] applied_follow: Option<bool>,+ /// Whether the Dock icon was last shown (macOS).+ #[cfg_attr(not(target_os = "macos"), allow(dead_code))]+ applied_dock: Option<bool>, } impl<'a> Shell<'a> {@@ -632,7 +648,7 @@ impl<'a> Shell<'a> { let window_hides = window_can_hide(cc.display_handle().ok().map(|d| d.as_raw())); // Started hidden at login but there's no tray to come back from: show the window. let visible = !hidden || launcher.tray.is_none();- let mut shell = Self { launcher, window_hides, visible: true, quitting: false, applied_follow: None };+ let mut shell = Self { launcher, window_hides, visible: true, quitting: false, applied_follow: None, applied_dock: None }; if !visible { shell.put_away(&cc.egui_ctx); } else if hidden {@@ -695,6 +711,16 @@ impl eframe::App for Shell<'_> { Request::Quit => self.quit(ctx), } }+ // After the requests, so a window being shown gets its Dock icon back before it comes out.+ #[cfg(target_os = "macos")]+ {+ let settings = &self.launcher.ui.settings;+ let on = shows_dock_icon(self.visible, self.launcher.tray.is_some(), settings.hide_dock_icon);+ if self.applied_dock != Some(on) {+ dock::show_icon(on);+ self.applied_dock = Some(on);+ }+ } // The close button puts the window away in the tray unless the user chose to quit. That is // the window manager's button or the header's own (no decorations), which asks to close // with `ViewportCommand::Close` and arrives here the same way.@@ -837,6 +863,15 @@ mod tests { } } + #[test]+ fn the_dock_icon_hides_only_while_the_window_is_away_in_the_menu_bar() {+ assert!(!shows_dock_icon(false, true, true));+ // The window is out, the user keeps the icon, or there's no menu bar icon to come back from.+ assert!(shows_dock_icon(true, true, true));+ assert!(shows_dock_icon(false, true, false));+ assert!(shows_dock_icon(false, false, true));+ }+ #[test] fn the_tray_alone_waits_until_asked() { assert_eq!(tray_step(&[]), TrayStep::Stay);@@ -965,7 +1000,8 @@ mod tests { fn quitting_overrides_a_close_to_the_tray() { let (mut launcher, _show, _) = launcher(); let ctx = launcher.windowless.clone();- let mut shell = Shell { launcher: &mut launcher, window_hides: false, visible: true, quitting: false, applied_follow: None };+ let mut shell =+ Shell { launcher: &mut launcher, window_hides: false, visible: true, quitting: false, applied_follow: None, applied_dock: None }; shell.put_away(&ctx); assert!(shell.launcher.closed_to_tray); shell.quit(&ctx);@@ -976,7 +1012,8 @@ mod tests { fn a_window_that_hides_is_not_closed_to_the_tray() { let (mut launcher, _show, _) = launcher(); let ctx = launcher.windowless.clone();- let mut shell = Shell { launcher: &mut launcher, window_hides: true, visible: true, quitting: false, applied_follow: None };+ let mut shell =+ Shell { launcher: &mut launcher, window_hides: true, visible: true, quitting: false, applied_follow: None, applied_dock: None }; shell.put_away(&ctx); assert!(!shell.visible); assert!(!shell.launcher.closed_to_tray);@@ -181,7 +181,8 @@ pub fn update( #[cfg(target_os = "macos")] Install::MacApp(bundle) => { let existing = Installed { version: None, source: InstallSource::MacApp, location: bundle.display().to_string(), package: None };- let placed = install::macos::install_dmg(&LAUNCHER, &file, Some(&existing), None, paths, &install::macos::Expect::release(release));+ let placed =+ install::macos::install_dmg(&LAUNCHER, &file, Some(&existing), None, false, paths, &install::macos::Expect::release(release)); let _ = std::fs::remove_file(&file); let placed = placed?; relaunch_after_exit(pid, &[Path::new("/usr/bin/open").as_os_str(), placed.as_os_str()])@@ -371,7 +372,7 @@ mod tests { let existing = Installed { version: None, source: InstallSource::MacApp, location: current.display().to_string(), package: None }; // An unsigned test bundle: the signature check has its own tests in `install`. let expect = install::macos::Expect { team: None, version: None };- let placed = install::macos::install_dmg(&LAUNCHER, &dmg, Some(&existing), None, &Paths::under(dir.path()), &expect).unwrap();+ let placed = install::macos::install_dmg(&LAUNCHER, &dmg, Some(&existing), None, false, &Paths::under(dir.path()), &expect).unwrap(); assert_eq!(placed, current); assert_eq!(std::fs::read_to_string(current.join("Contents/MacOS/ArtCraft Launcher")).unwrap(), "0.2.0"); assert_eq!(@@ -46,6 +46,11 @@ pub trait Host { fn has_desktops(&self) -> bool { false }+ /// The launcher has a Dock icon (macOS) that can be hidden while the window is put away in+ /// the menu bar.+ fn has_dock(&self) -> bool {+ false+ } /// `Some(enabled)` where starting at login is supported. fn login_item(&self) -> Option<bool> { None@@ -439,6 +439,13 @@ impl LauncherUi { section(ui, tl!("Running in the background")); if host.has_tray() { ui.checkbox(&mut self.settings.close_to_tray, tl!("Keep running in the tray when the window is closed"));+ if host.has_dock() {+ ui.add_enabled(+ self.settings.close_to_tray,+ egui::Checkbox::new(&mut self.settings.hide_dock_icon, tl!("Hide the Dock icon while the window is closed")),+ )+ .on_hover_text(tl!("The launcher stays in the menu bar only. Its Dock icon comes back when you open the window."));+ } } if host.has_desktops() { ui.checkbox(&mut self.settings.show_on_current_desktop, tl!("Show the window on the current desktop"))@@ -83,6 +83,8 @@ Running in the background Běh na pozadí Keep running in the tray when the window is closed Po zavření okna ponechat spuštěné v oznamovací oblasti Show the window on the current desktop Zobrazit okno na aktuální ploše+ Hide the Dock icon while the window is closed Skrýt ikonu v Docku, když je okno zavřené+ The launcher stays in the menu bar only. Its Dock icon comes back when you open the window. Launcher zůstane jen v řádku nabídek. Jeho ikona se do Docku vrátí, jakmile otevřete okno. Start ArtCraft Launcher when I log in Spouštět ArtCraft Launcher po přihlášení Community Komunita Questions, ideas and news from the ArtCraft team Dotazy, nápady a novinky od týmu ArtCraft@@ -96,6 +96,8 @@ Running in the background Im Hintergrund ausführen Keep running in the tray when the window is closed Beim Schließen des Fensters im Infobereich weiterlaufen Show the window on the current desktop Fenster auf dem aktuellen Schreibtisch zeigen+ Hide the Dock icon while the window is closed Dock-Symbol ausblenden, solange das Fenster geschlossen ist+ The launcher stays in the menu bar only. Its Dock icon comes back when you open the window. Der Launcher bleibt dann nur in der Menüleiste. Sein Dock-Symbol erscheint wieder, sobald du das Fenster öffnest. Start ArtCraft Launcher when I log in ArtCraft Launcher bei der Anmeldung starten Community Community Questions, ideas and news from the ArtCraft team Fragen, Ideen und Neuigkeiten vom ArtCraft-Team@@ -96,6 +96,8 @@ Running in the background En segundo plano Keep running in the tray when the window is closed Seguir abierto en la bandeja del sistema al cerrar la ventana Show the window on the current desktop Mostrar la ventana en el escritorio actual+ Hide the Dock icon while the window is closed Ocultar el icono del Dock mientras la ventana está cerrada+ The launcher stays in the menu bar only. Its Dock icon comes back when you open the window. ArtCraft Launcher se queda solo en la barra de menús. Su icono vuelve al Dock cuando abres la ventana. Start ArtCraft Launcher when I log in Abrir ArtCraft Launcher al iniciar sesión Community Comunidad Questions, ideas and news from the ArtCraft team Preguntas, ideas y noticias del equipo de ArtCraft@@ -94,6 +94,8 @@ Running in the background Exécution en arrière-plan Keep running in the tray when the window is closed Rester actif dans la zone de notification à la fermeture de la fenêtre Show the window on the current desktop Afficher la fenêtre sur le bureau actuel+ Hide the Dock icon while the window is closed Masquer l'icône du Dock quand la fenêtre est fermée+ The launcher stays in the menu bar only. Its Dock icon comes back when you open the window. Le launcher reste uniquement dans la barre des menus. Son icône revient dans le Dock quand vous ouvrez la fenêtre. Start ArtCraft Launcher when I log in Lancer ArtCraft Launcher à l'ouverture de session Community Communauté Questions, ideas and news from the ArtCraft team Questions, idées et actualités de l'équipe ArtCraft@@ -96,6 +96,8 @@ Running in the background Futtatás a háttérben Keep running in the tray when the window is closed Az ablak bezárásakor kerüljön a tálcára Show the window on the current desktop Ablak megjelenítése az aktuális asztalon+ Hide the Dock icon while the window is closed Dock-ikon elrejtése, amíg az ablak zárva van+ The launcher stays in the menu bar only. Its Dock icon comes back when you open the window. Az ArtCraft Launcher ilyenkor csak a menüsorban látható. A Dock-ikonja az ablak megnyitásakor visszatér. Start ArtCraft Launcher when I log in ArtCraft Launcher indítása bejelentkezéskor Community Közösség Questions, ideas and news from the ArtCraft team Kérdések, ötletek és hírek az ArtCraft csapatától@@ -98,6 +98,8 @@ Running in the background バックグラウンドで実行 Keep running in the tray when the window is closed ウィンドウを閉じてもトレイで実行し続ける Show the window on the current desktop 現在のデスクトップにウィンドウを表示+ Hide the Dock icon while the window is closed ウィンドウを閉じている間は Dock のアイコンを隠す+ The launcher stays in the menu bar only. Its Dock icon comes back when you open the window. ランチャーはメニューバーにだけ表示されます。ウィンドウを開くと Dock のアイコンが戻ります。 Start ArtCraft Launcher when I log in ログイン時に ArtCraft Launcher を起動 Community コミュニティ Questions, ideas and news from the ArtCraft team ArtCraft チームからの質問、アイデア、ニュース@@ -97,6 +97,8 @@ Running in the background Działanie w tle Keep running in the tray when the window is closed Po zamknięciu okna działaj w zasobniku systemowym Show the window on the current desktop Pokaż okno na bieżącym pulpicie+ Hide the Dock icon while the window is closed Ukryj ikonę w Docku, gdy okno jest zamknięte+ The launcher stays in the menu bar only. Its Dock icon comes back when you open the window. Launcher pozostaje wtedy tylko na pasku menu. Jego ikona wraca do Docka, gdy otworzysz okno. Start ArtCraft Launcher when I log in Uruchom ArtCraft Launcher po zalogowaniu Community Społeczność Questions, ideas and news from the ArtCraft team Pytania, pomysły i nowości od zespołu ArtCraft@@ -84,6 +84,8 @@ Running in the background Execução em segundo plano Keep running in the tray when the window is closed Continuar na bandeja do sistema ao fechar a janela Show the window on the current desktop Mostrar a janela na área de trabalho atual+ Hide the Dock icon while the window is closed Ocultar o ícone do Dock enquanto a janela estiver fechada+ The launcher stays in the menu bar only. Its Dock icon comes back when you open the window. O launcher fica apenas na barra de menus. O ícone volta ao Dock quando você abre a janela. Start ArtCraft Launcher when I log in Iniciar o ArtCraft Launcher ao fazer login Community Comunidade Questions, ideas and news from the ArtCraft team Dúvidas, ideias e novidades da equipe ArtCraft@@ -94,6 +94,8 @@ Running in the background Работа в фоне Keep running in the tray when the window is closed Оставаться в трее при закрытии окна Show the window on the current desktop Показывать окно на текущем рабочем столе+ Hide the Dock icon while the window is closed Скрывать значок в Dock, пока окно закрыто+ The launcher stays in the menu bar only. Its Dock icon comes back when you open the window. Лаунчер остаётся только в строке меню. Значок в Dock появится снова, когда вы откроете окно. Start ArtCraft Launcher when I log in Запускать ArtCraft Launcher при входе в систему Community Сообщество Questions, ideas and news from the ArtCraft team Вопросы, идеи и новости от команды ArtCraft@@ -78,6 +78,8 @@ Running in the background 后台运行 Keep running in the tray when the window is closed 关闭窗口后继续在系统托盘中运行 Show the window on the current desktop 在当前桌面显示窗口+ Hide the Dock icon while the window is closed 窗口关闭时隐藏程序坞图标+ The launcher stays in the menu bar only. Its Dock icon comes back when you open the window. 启动器只保留在菜单栏中。打开窗口后,程序坞图标会重新出现。 Start ArtCraft Launcher when I log in 登录时启动 ArtCraft Launcher Community 社区 Questions, ideas and news from the ArtCraft team 与 ArtCraft 团队交流问题、想法和动态@@ -98,6 +98,8 @@ pub struct Settings { /// macOS: showing the window brings it to the current desktop (Space) instead of switching to /// the desktop it was on. pub show_on_current_desktop: bool,+ /// macOS: while the window is put away in the menu bar, the launcher has no Dock icon.+ pub hide_dock_icon: bool, pub check_every: CheckEvery, /// Automatically download and install updates when found. #[serde(default)]@@ -179,6 +181,7 @@ impl Default for Settings { language: "auto".to_string(), close_to_tray: true, show_on_current_desktop: true,+ hide_dock_icon: false, check_every: CheckEvery::Hour, auto_update: false, notify: true,@@ -1018,6 +1018,62 @@ fn the_current_desktop_setting_is_only_offered_where_the_os_has_desktops() { assert!(Settings::default().show_on_current_desktop); } +#[test]+fn the_dock_icon_can_be_hidden_while_the_launcher_waits_in_the_menu_bar() {+ const HIDE: &str = "Hide the Dock icon while the window is closed";+ // NoHost has no Dock (Windows, Linux, web): no setting. It's off by default, so the Dock icon stays.+ let (mut h, _) = harness(ViewMode::Grid, egui::Theme::Dark, true);+ h.run_steps(6);+ h.get_by_label("Settings").click();+ h.run_steps(4);+ assert!(h.query_by_label(HIDE).is_none());+ assert!(!Settings::default().hide_dock_icon);++ // macOS with a menu bar icon (DesktopHost::has_dock).+ struct DockHost;+ impl Host for DockHost {+ fn has_tray(&self) -> bool {+ true+ }+ fn has_dock(&self) -> bool {+ true+ }+ }+ let mut backend = Some(Fake {+ events: events(),+ sent: Rc::default(),+ manage: true,+ pictures: Rc::default(),+ asked: Vec::new(),+ clock: Rc::new(Cell::new(NOW)),+ });+ let mut h = Harness::builder().with_size(egui::vec2(1180.0, 900.0)).build_ui_state(+ move |ui, state: &mut Option<LauncherUi>| {+ if state.is_none()+ && let Some(b) = backend.take()+ {+ *state = Some(LauncherUi::new(ui.ctx(), Box::new(b), Settings { notify: false, ..Settings::default() }));+ }+ if let Some(l) = state.as_mut() {+ l.logic(&ui.ctx().clone(), &mut DockHost);+ l.ui(ui, &mut DockHost);+ }+ },+ None,+ );+ let hide_dock_icon = |h: &Harness<'_, Option<LauncherUi>>| h.state().as_ref().is_some_and(|l| l.settings.hide_dock_icon);+ h.run_steps(6);+ h.get_by_label("Settings").click();+ h.run_steps(4);+ h.get_by_label(HIDE).click();+ h.run_steps(2);+ assert!(hide_dock_icon(&h));+ // Only meaningful while closing keeps the launcher in the menu bar.+ h.get_by_label("Keep running in the tray when the window is closed").click();+ h.run_steps(2);+ assert!(h.get_by_label(HIDE).accesskit_node().is_disabled());+}+ #[test] fn feed_announcements_and_alerts_show_until_dismissed() { use artcraft_launcher_core::feed::Notice;