diff --git a/.gitignore b/.gitignore index ed49934..eb5a316 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1 @@ -target/release +target diff --git a/Cargo.lock b/Cargo.lock index e876de5..689f299 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -246,6 +246,36 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" +[[package]] +name = "cocoa" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6140449f97a6e97f9511815c5632d84c8aacf8ac271ad77c559218161a1373c" +dependencies = [ + "bitflags 1.3.2", + "block", + "cocoa-foundation", + "core-foundation", + "core-graphics 0.23.2", + "foreign-types 0.5.0", + "libc", + "objc", +] + +[[package]] +name = "cocoa-foundation" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c6234cbb2e4c785b456c0644748b1ac416dd045799740356f8363dfe00c93f7" +dependencies = [ + "bitflags 1.3.2", + "block", + "core-foundation", + "core-graphics-types", + "libc", + "objc", +] + [[package]] name = "codespan-reporting" version = "0.11.1" @@ -961,7 +991,9 @@ dependencies = [ name = "moomer" version = "0.1.0" dependencies = [ + "cocoa", "image", + "objc", "pixels", "screenshots", "winit", diff --git a/Cargo.toml b/Cargo.toml index dd278d9..d5d3d30 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,3 +9,7 @@ winit = "0.28" pixels = "0.13" image = "0.24" +[target.'cfg(target_os = "macos")'.dependencies] +cocoa = "0.25" +objc = "0.2" + diff --git a/README.md b/README.md index 7956887..963a8b6 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # Moomer +![Banner](banner.webp) + Zoomer application for macOS. A poor man's clone of [tsoding/boomer](https://github.com/tsoding/boomer). ## Quick Start diff --git a/banner.webp b/banner.webp new file mode 100644 index 0000000..7c68bc9 Binary files /dev/null and b/banner.webp differ diff --git a/src/main.rs b/src/main.rs index 53ded40..dfcacfb 100644 --- a/src/main.rs +++ b/src/main.rs @@ -12,6 +12,74 @@ use winit::platform::macos::WindowExtMacOS; #[cfg(target_os = "macos")] use std::process::Command; +#[cfg(target_os = "macos")] +use cocoa::appkit::{NSApp, NSApplication}; +#[cfg(target_os = "macos")] +use cocoa::base::nil; + +#[cfg(target_os = "macos")] +fn activate_current_process() -> Result<(), Box> { + unsafe { + let app = NSApp(); + if app != nil { + app.activateIgnoringOtherApps_(true); + } + } + + Ok(()) +} + +#[cfg(target_os = "macos")] +fn get_frontmost_app() -> Result> { + let script = r#" + tell application "System Events" + set frontApp to first application process whose frontmost is true + return name of frontApp + end tell + "#; + + let output = Command::new("osascript") + .arg("-e") + .arg(script) + .output()?; + + Ok(String::from_utf8_lossy(&output.stdout).trim().to_string()) +} + +#[cfg(target_os = "macos")] +fn activate_app_by_name(app_name: &str) -> Result<(), Box> { + if app_name == "moomer" { + let script = r#" + tell application "System Events" + set moomerWindows to every window of (first application process whose name is "moomer") + if (count of moomerWindows) > 0 then + set frontmost of first application process whose name is "moomer" to true + perform action "AXRaise" of item 1 of moomerWindows + end if + end tell + "#; + + let _ = Command::new("osascript") + .arg("-e") + .arg(script) + .output()?; + } else { + let script = format!(r#" + tell application "System Events" + set targetApp to first application process whose name is "{}" + set frontmost of targetApp to true + end tell + "#, app_name); + + let _ = Command::new("osascript") + .arg("-e") + .arg(&script) + .output()?; + } + + Ok(()) +} + #[cfg(target_os = "macos")] fn exit_fullscreen_if_active() -> Result<(), Box> { let script = r#" @@ -42,21 +110,6 @@ fn exit_fullscreen_if_active() -> Result<(), Box> { Ok(()) } -#[cfg(target_os = "macos")] -fn send_cmd_tab() -> Result<(), Box> { - let script = r#" - tell application "System Events" - key code 48 using command down - end tell - "#; - - Command::new("osascript") - .arg("-e") - .arg(script) - .output()?; - - Ok(()) -} struct ViewState { zoom: f32, @@ -64,6 +117,7 @@ struct ViewState { offset_y: f32, drag_start: Option<(f32, f32)>, last_cursor_pos: (f32, f32), + activation_attempts: u32, } impl ViewState { @@ -74,6 +128,7 @@ impl ViewState { offset_y: 0.0, drag_start: None, last_cursor_pos: (0.0, 0.0), + activation_attempts: 0, } } } @@ -82,6 +137,19 @@ fn main() -> Result<(), Box> { #[cfg(target_os = "macos")] let _ = exit_fullscreen_if_active(); + #[cfg(target_os = "macos")] + let previous_app = get_frontmost_app().ok().and_then(|name| { + if name == "moomer" || name == "Terminal" || name == "iTerm2" || name == "iTerm" { + None + } else { + Some(name) + } + }); + + #[cfg(not(target_os = "macos"))] + #[allow(unused_variables)] + let previous_app: Option = None; + let screens = Screen::all()?; let screen = screens.first().ok_or("No screens found")?; @@ -106,13 +174,10 @@ fn main() -> Result<(), Box> { window.set_visible(true); window.focus_window(); - #[cfg(target_os = "macos")] - window.set_simple_fullscreen(true); - #[cfg(target_os = "macos")] { - std::thread::sleep(std::time::Duration::from_millis(200)); - let _ = send_cmd_tab(); + window.set_simple_fullscreen(true); + let _ = activate_current_process(); } let window_size = window.inner_size(); @@ -129,6 +194,10 @@ fn main() -> Result<(), Box> { match event { Event::WindowEvent { event, .. } => match event { WindowEvent::CloseRequested => { + #[cfg(target_os = "macos")] + if let Some(ref app_name) = previous_app { + let _ = activate_app_by_name(app_name); + } *control_flow = ControlFlow::Exit; } WindowEvent::Resized(new_size) => { @@ -204,6 +273,10 @@ fn main() -> Result<(), Box> { WindowEvent::KeyboardInput { input, .. } => { if input.state == ElementState::Pressed { if let Some(VirtualKeyCode::Q) = input.virtual_keycode { + #[cfg(target_os = "macos")] + if let Some(ref app_name) = previous_app { + let _ = activate_app_by_name(app_name); + } *control_flow = ControlFlow::Exit; } } @@ -211,6 +284,20 @@ fn main() -> Result<(), Box> { _ => {} }, Event::RedrawRequested(_) => { + #[cfg(target_os = "macos")] + if view_state.activation_attempts < 2 { + view_state.activation_attempts += 1; + let _ = activate_current_process(); + + if let Ok(current_app) = get_frontmost_app() { + if current_app == "moomer" { + view_state.activation_attempts = 2; + } else if view_state.activation_attempts < 2 { + window.request_redraw(); + } + } + } + render( pixels.frame_mut(), &img_buffer,