diff --git a/flake.nix b/flake.nix index 4f5d131..1971a9a 100644 --- a/flake.nix +++ b/flake.nix @@ -132,6 +132,35 @@ unset slermCache slermStale slermTmp slermDir slermGlob slermLib fi ''; + # macOS only gives a process a dock icon, a menu bar and a real + # application name when it runs from a bundle, so ship one and keep + # bin/slerm as a symlink into it for terminal use. + appBundle = version: '' + app="$out/Applications/slerm.app" + mkdir -p "$app/Contents/MacOS" "$app/Contents/Resources" + cat > "$app/Contents/Info.plist" < + + + + CFBundleNameslerm + CFBundleDisplayNameslerm + CFBundleExecutableslerm + CFBundleIdentifierorg.slerm.slerm + CFBundleInfoDictionaryVersion6.0 + CFBundlePackageTypeAPPL + CFBundleShortVersionString${version} + CFBundleVersion${version} + LSApplicationCategoryTypepublic.app-category.developer-tools + LSMinimumSystemVersion11.0 + NSHighResolutionCapable + NSSupportsAutomaticGraphicsSwitching + + + EOF + mv "$out/bin/slerm" "$app/Contents/MacOS/slerm" + ln -s "$app/Contents/MacOS/slerm" "$out/bin/slerm" + ''; ghosttyEnv = { GHOSTTY_SOURCE_DIR = "${ghostty-src}"; GHOSTTY_ZIG_SYSTEM_DIR = "${ghosttyDeps}"; @@ -183,6 +212,10 @@ "--skip=tests::osc7_wins_over_the_process_directory_but_only_for_this_machine" ]; + postInstall = lib.optionalString pkgs.stdenv.hostPlatform.isDarwin ( + appBundle manifest.package.version + ); + postFixup = lib.optionalString pkgs.stdenv.hostPlatform.isLinux '' wrapProgram "$out/bin/slerm" \ --suffix LD_LIBRARY_PATH : ${lib.makeLibraryPath runtimeLibraries} \ diff --git a/result b/result index 9c83b08..ff4d06d 120000 --- a/result +++ b/result @@ -1 +1 @@ -/nix/store/c0ngrz7m1zjs95pb9qm3rx5ka3sbvpvb-slerm-0.1.0 \ No newline at end of file +/nix/store/6vj9g5xzxd0whdnxd0zsg02m1rwsrlrn-slerm-0.1.0 \ No newline at end of file diff --git a/src/app.rs b/src/app.rs index a9c15db..8dbe636 100644 --- a/src/app.rs +++ b/src/app.rs @@ -23,6 +23,37 @@ pub struct App { suppressed: HashSet, } +/// Width kept free at the left of the tab bar for the macOS traffic lights, +/// which float over the content once the titlebar is hidden. +const TITLEBAR_INSET: f32 = 70.0; +/// Height the tab bar needs so the traffic lights fit inside it. +const TITLEBAR_HEIGHT: f32 = 28.0; + +/// With the titlebar hidden the tab bar takes its place, so let the empty space +/// beside the tabs drag the window and double-click to zoom, like a titlebar. +fn drag_window(ui: &mut egui::Ui) { + let used = ui.min_rect(); + let free = Rect::from_min_max( + egui::pos2(used.right(), used.top()), + egui::pos2(ui.max_rect().right(), used.bottom()), + ); + if free.width() <= 0.0 { + return; + } + let response = ui.interact( + free, + ui.id().with("titlebar drag"), + egui::Sense::click_and_drag(), + ); + if response.double_clicked() { + let maximized = ui.input(|i| i.viewport().maximized.unwrap_or(false)); + ui.ctx() + .send_viewport_cmd(egui::ViewportCommand::Maximized(!maximized)); + } else if response.drag_started_by(egui::PointerButton::Primary) { + ui.ctx().send_viewport_cmd(egui::ViewportCommand::StartDrag); + } +} + pub fn fonts(config: &Config) -> Result { let mut fonts = egui::FontDefinitions::default(); if let Some(path) = &config.font_path { @@ -353,6 +384,10 @@ impl eframe::App for App { let mut action = None; egui::TopBottomPanel::top("tabs").show(ctx, |ui| { ui.horizontal_wrapped(|ui| { + if cfg!(target_os = "macos") { + ui.add_space(TITLEBAR_INSET); + ui.set_min_height(TITLEBAR_HEIGHT); + } for (index, tab) in self.tabs.iter().enumerate() { let title = self.panes[&tab.active].terminal.title().unwrap_or(""); let label = if title.is_empty() { @@ -396,6 +431,9 @@ impl eframe::App for App { } }); }); + if cfg!(target_os = "macos") { + drag_window(ui); + } }); if let Some(error) = self.error.clone() { egui::TopBottomPanel::bottom("error").show(ctx, |ui| { diff --git a/src/main.rs b/src/main.rs index aa229f8..6da6eb3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -67,12 +67,21 @@ fn main() -> Result<()> { return Ok(()); } libghostty_vt::kitty::graphics::set_png_decoder(Some(Box::new(render::PngDecoder)))?; + let mut viewport = egui::ViewportBuilder::default() + .with_title("slerm") + .with_inner_size([config.window_width, config.window_height]) + .with_min_inner_size([320.0, 200.0]) + .with_app_id("slerm"); + if cfg!(target_os = "macos") { + // Hide the titlebar and draw under it; the traffic lights stay on top of + // the tab bar, which reserves room for them (see app::TITLEBAR_INSET). + viewport = viewport + .with_fullsize_content_view(true) + .with_titlebar_shown(false) + .with_title_shown(false); + } let options = eframe::NativeOptions { - viewport: egui::ViewportBuilder::default() - .with_title("slerm") - .with_inner_size([config.window_width, config.window_height]) - .with_min_inner_size([320.0, 200.0]) - .with_app_id("slerm"), + viewport, renderer: eframe::Renderer::Wgpu, ..Default::default() };