diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index d57bd79..d5e7dfc 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -1,19 +1,84 @@ -name: Release +name: Rust CI/CD on: push: - branches: [ main ] + branches: [main] + pull_request: + branches: [main, develop] jobs: + fmt: + name: Format Check + runs-on: macos-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Rust + uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: stable + override: true + components: rustfmt + + - name: Run cargo fmt + run: cargo fmt --all -- --check + + clippy: + name: Clippy Lints + runs-on: macos-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Rust + uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: stable + override: true + components: clippy + + - name: Install Dependencies + run: | + brew install protobuf + brew install swift-protobuf + + - name: Cache cargo registry + uses: actions/cache@v4 + with: + path: ~/.cargo/registry + key: ${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }} + + - name: Cache cargo index + uses: actions/cache@v4 + with: + path: ~/.cargo/git + key: ${{ runner.os }}-cargo-index-${{ hashFiles('**/Cargo.lock') }} + + - name: Cache cargo build + uses: actions/cache@v4 + with: + path: target + key: ${{ runner.os }}-cargo-build-target-clippy-${{ hashFiles('**/Cargo.lock') }} + + - name: Run cargo clippy + run: cargo clippy --all-targets -- -D warnings + release: + name: Build and Release runs-on: macos-latest + # Only run release on pushes to main (not on PRs) + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + needs: [fmt, clippy] permissions: contents: write steps: - name: Checkout code uses: actions/checkout@v4 with: - fetch-depth: 0 # This ensures we get all git history + fetch-depth: 0 # This ensures we get all git history - name: Setup Rust uses: actions-rs/toolchain@v1 @@ -28,6 +93,24 @@ jobs: brew install protobuf brew install swift-protobuf + - name: Cache cargo registry + uses: actions/cache@v4 + with: + path: ~/.cargo/registry + key: ${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }} + + - name: Cache cargo index + uses: actions/cache@v4 + with: + path: ~/.cargo/git + key: ${{ runner.os }}-cargo-index-${{ hashFiles('**/Cargo.lock') }} + + - name: Cache cargo build + uses: actions/cache@v4 + with: + path: target + key: ${{ runner.os }}-cargo-build-target-release-${{ hashFiles('**/Cargo.lock') }} + - name: Get Version id: get_version run: | diff --git a/README.md b/README.md index 2bd2eb6..16a99c4 100644 --- a/README.md +++ b/README.md @@ -35,3 +35,40 @@ To resolve the problem, run the following command in Terminal: ``` xattr -d com.apple.quarantine ~/Downloads/FastForward.dmg ``` + +## Development + +### Prerequisites +- Rust (stable toolchain) +- Xcode Command Line Tools +- protobuf: `brew install protobuf` +- swift-protobuf: `brew install swift-protobuf` + +### Building from Source + +```bash +# Clone the repository +git clone https://github.com/yourusername/fast-forward.git +cd fast-forward + +# Build in debug mode +cargo build + +# Build in release mode +cargo build --release + +# Run the application +cargo run +``` + +### Code Quality + +This project uses `cargo fmt` and `cargo clippy` to maintain code quality. + +```bash +# Format code +cargo fmt --all + +# Run linter +cargo clippy --all-targets -- -D warnings +``` diff --git a/src/applications.rs b/src/applications.rs index dd865ff..45b15ea 100644 --- a/src/applications.rs +++ b/src/applications.rs @@ -5,10 +5,10 @@ use log::error; use objc2::rc::Retained; use objc2_app_kit::{NSApplicationActivationOptions, NSRunningApplication, NSWorkspace}; -use crate::ui::input::SearchQuery; -use crate::window::Window; use crate::socket_message::App as Application; +use crate::ui::input::SearchQuery; use crate::ui::list::List; +use crate::window::Window; #[derive(Debug, Clone)] pub struct Applications { @@ -22,7 +22,7 @@ impl Default for Applications { Self { list: Vec::new(), index: 0, - loading: true + loading: true, } } } @@ -31,7 +31,7 @@ impl Default for Applications { pub enum ActionType { Activate, Hide, - Quit + Quit, } #[derive(Debug, Clone, Copy)] @@ -51,7 +51,7 @@ impl Applications { cx.set_global(Self { list, index: 0, - loading: false + loading: false, }); } @@ -66,12 +66,19 @@ impl Applications { cx.set_global(applications); } - pub fn update_list_entry(cx: &mut App, app: Option<&Application>, index_type: Option, reset: bool) { + pub fn update_list_entry( + cx: &mut App, + app: Option<&Application>, + index_type: Option, + reset: bool, + ) { let applications = cx.global::(); let mut applications = applications.clone(); if let Some(app) = app { - if let Some(existing_app_index) = applications.list.iter().position(|a| a.name == app.name) { + if let Some(existing_app_index) = + applications.list.iter().position(|a| a.name == app.name) + { if index_type.is_some() { applications.list.remove(existing_app_index); } else { @@ -80,7 +87,8 @@ impl Applications { } if let Some(index_type) = index_type { - let target_index = Self::get_index_from_type(&applications.list, applications.index, index_type); + let target_index = + Self::get_index_from_type(&applications.list, applications.index, index_type); applications.list.insert(target_index, app.clone()); } } @@ -90,7 +98,9 @@ impl Applications { // Reset the active index and search query. if reset { Self::update_active_index(cx, IndexType::Start); - cx.set_global(SearchQuery { value: String::new() }); + cx.set_global(SearchQuery { + value: String::new(), + }); } } @@ -113,14 +123,14 @@ impl Applications { unsafe { native_app.activateWithOptions(NSApplicationActivationOptions::empty()); } - }, + } ActionType::Hide => { Self::update_list_entry(cx, Some(app), Some(IndexType::End), true); unsafe { native_app.hide(); } - }, + } ActionType::Quit => { Self::update_list_entry(cx, Some(app), None, true); @@ -142,7 +152,11 @@ impl Applications { } } - fn get_index_from_type(list: &[Application], current_index: usize, index_type: IndexType) -> usize { + fn get_index_from_type( + list: &[Application], + current_index: usize, + index_type: IndexType, + ) -> usize { match index_type { IndexType::Start => 0, IndexType::End => list.len(), @@ -153,14 +167,16 @@ impl Applications { } else { current_index - 1 } - }, + } } } fn get_running_app_instance(app: &Application) -> Option> { unsafe { let running_applications = NSWorkspace::sharedWorkspace().runningApplications(); - running_applications.iter().find(|item| item.localizedName().unwrap().to_string() == app.name) + running_applications + .iter() + .find(|item| item.localizedName().unwrap().to_string() == app.name) } } } diff --git a/src/assets.rs b/src/assets.rs index 662800e..4375e3f 100644 --- a/src/assets.rs +++ b/src/assets.rs @@ -1,5 +1,5 @@ -use std::{fs, path::PathBuf}; use anyhow::anyhow; +use std::{fs, path::PathBuf}; use gpui::{AssetSource, Result, SharedString}; use rust_embed::RustEmbed; diff --git a/src/commander/handlers.rs b/src/commander/handlers.rs index 00e79b0..e24b6ec 100644 --- a/src/commander/handlers.rs +++ b/src/commander/handlers.rs @@ -1,18 +1,21 @@ -use std::sync::atomic::{AtomicBool, Ordering}; use gpui::{AsyncApp, Result}; +use std::sync::atomic::{AtomicBool, Ordering}; +use super::events::{EventType, HotkeyEvent, TrayEvent}; +use crate::socket_message::socket_message::Event as SocketEvent; use crate::{ applications::{ActionType, Applications, IndexType}, config::Config, window::Window, }; -use super::events::{EventType, HotkeyEvent, TrayEvent}; -use crate::socket_message::socket_message::Event as SocketEvent; static ESCAPE_PRESSED: AtomicBool = AtomicBool::new(false); static SPACE_PRESSED: AtomicBool = AtomicBool::new(false); -pub(super) fn handle_event(cx: &AsyncApp, event: EventType) -> Result<(), Box> { +pub(super) fn handle_event( + cx: &AsyncApp, + event: EventType, +) -> Result<(), Box> { match event { EventType::HotkeyEvent(event) => handle_hotkey_event(cx, event), EventType::TrayEvent(event) => handle_tray_event(cx, event), @@ -20,12 +23,13 @@ pub(super) fn handle_event(cx: &AsyncApp, event: EventType) -> Result<(), Box Result<(), Box> { +fn handle_hotkey_event( + cx: &AsyncApp, + event: HotkeyEvent, +) -> Result<(), Box> { match event { HotkeyEvent::ShowWindow(offset) => { - cx.update(|cx| { - Window::show(cx, offset) - })?; + cx.update(|cx| Window::show(cx, offset))?; } HotkeyEvent::HideWindow => { cx.update(|cx| { @@ -53,7 +57,7 @@ fn handle_tray_event(cx: &AsyncApp, event: TrayEvent) -> Result<(), Box { cx.update(|cx| cx.open_url("https://github.com/gaauwe/fast-forward"))?; } @@ -64,19 +68,36 @@ fn handle_tray_event(cx: &AsyncApp, event: TrayEvent) -> Result<(), Box Result<(), Box> { +fn handle_socket_event( + cx: &AsyncApp, + event: SocketEvent, +) -> Result<(), Box> { match event { SocketEvent::List(event) => { cx.update(|cx| Applications::update_list(cx, event.apps.clone()))?; } SocketEvent::Launch(event) => { - cx.update(|cx| Applications::update_list_entry(cx, event.app.as_ref(), Some(IndexType::Start), false))?; + cx.update(|cx| { + Applications::update_list_entry( + cx, + event.app.as_ref(), + Some(IndexType::Start), + false, + ) + })?; } SocketEvent::Close(event) => { cx.update(|cx| Applications::update_list_entry(cx, event.app.as_ref(), None, false))?; } SocketEvent::Activate(event) => { - cx.update(|cx| Applications::update_list_entry(cx, event.app.as_ref(), Some(IndexType::Start), false))?; + cx.update(|cx| { + Applications::update_list_entry( + cx, + event.app.as_ref(), + Some(IndexType::Start), + false, + ) + })?; } } Ok(()) diff --git a/src/commander/mod.rs b/src/commander/mod.rs index 5686faf..f23ad13 100644 --- a/src/commander/mod.rs +++ b/src/commander/mod.rs @@ -28,7 +28,8 @@ impl Commander { .timer(Duration::from_millis(50)) .await; } - }).detach(); + }) + .detach(); cx.set_global::(Self { tx }); } diff --git a/src/config.rs b/src/config.rs index 5516298..e5c8352 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,7 +1,7 @@ -use log::error; use crate::theme::{Theme, ThemeConfig}; use anyhow::{Context, Result}; use gpui::{App, Global}; +use log::error; use serde::Deserialize; use std::fs; use std::path::{Path, PathBuf}; @@ -48,7 +48,7 @@ impl Config { Ok(config) => { cx.set_global(config.clone()); config - }, + } Err(e) => { error!("Failed to load configuration: {e}"); cx.set_global(Config::default()); @@ -85,17 +85,38 @@ impl Config { Self { theme: Theme { - primary: config.theme.primary.map_or(default_theme.primary, Into::into), - background: config.theme.background.map_or(default_theme.background, Into::into), - foreground: config.theme.foreground.map_or(default_theme.foreground, Into::into), + primary: config + .theme + .primary + .map_or(default_theme.primary, Into::into), + background: config + .theme + .background + .map_or(default_theme.background, Into::into), + foreground: config + .theme + .foreground + .map_or(default_theme.foreground, Into::into), muted: config.theme.muted.map_or(default_theme.muted, Into::into), - muted_foreground: config.theme.muted_foreground.map_or(default_theme.muted_foreground, Into::into), + muted_foreground: config + .theme + .muted_foreground + .map_or(default_theme.muted_foreground, Into::into), border: config.theme.border.map_or(default_theme.border, Into::into), }, general: General { - show_tray: config.general.show_tray.map_or(default_general.show_tray, Into::into), - enable_left_cmd: config.general.enable_left_cmd.map_or(default_general.enable_left_cmd, Into::into), - disable_quit: config.general.disable_quit.map_or(default_general.disable_quit, Into::into), + show_tray: config + .general + .show_tray + .map_or(default_general.show_tray, Into::into), + enable_left_cmd: config + .general + .enable_left_cmd + .map_or(default_general.enable_left_cmd, Into::into), + disable_quit: config + .general + .disable_quit + .map_or(default_general.disable_quit, Into::into), }, } } diff --git a/src/hotkey.rs b/src/hotkey.rs index 0c08952..a5fc1e4 100644 --- a/src/hotkey.rs +++ b/src/hotkey.rs @@ -1,12 +1,18 @@ use gpui::{App, Global}; use log::error; -use tokio::sync::mpsc::UnboundedSender; use std::sync::atomic::{AtomicBool, Ordering}; +use tokio::sync::mpsc::UnboundedSender; use core_foundation::runloop::{kCFRunLoopCommonModes, CFRunLoop, CFRunLoopSource}; -use core_graphics::event::{CGEventFlags, CGEventTap, CGEventTapLocation, CGEventTapOptions, CGEventTapPlacement, CGEventType, EventField}; +use core_graphics::event::{ + CGEventFlags, CGEventTap, CGEventTapLocation, CGEventTapOptions, CGEventTapPlacement, + CGEventType, EventField, +}; -use crate::{commander::{Commander, EventType, HotkeyEvent}, config::Config}; +use crate::{ + commander::{Commander, EventType, HotkeyEvent}, + config::Config, +}; pub static IS_ACTIVE: AtomicBool = AtomicBool::new(false); @@ -23,7 +29,7 @@ pub enum Key { /// Left Command key (keycode 55) LeftCommand = 55, /// Any other key - Other + Other, } impl From for Key { @@ -34,7 +40,7 @@ impl From for Key { 53 => Key::Escape, 55 => Key::LeftCommand, 54 => Key::RightCommand, - _ => Key::Other + _ => Key::Other, } } } @@ -63,34 +69,35 @@ impl Hotkey { if let Some(hotkey_event) = handler.handle_flags_changed(keycode, flags) { handler.send_event(hotkey_event); } - }, + } CGEventType::KeyDown => { if let Some(hotkey_event) = handler.handle_flags_changed(keycode, flags) { handler.send_event(hotkey_event); } - if let Some((hotkey_event, should_block)) = handler.handle_key_down(keycode, &mut flags) { + if let Some((hotkey_event, should_block)) = + handler.handle_key_down(keycode, &mut flags) + { handler.send_event(hotkey_event); if should_block { new_event.set_type(CGEventType::Null); } } new_event.set_flags(flags); - }, + } _ => {} } Some(new_event) }, - ).unwrap_or_else(|e| { + ) + .unwrap_or_else(|e| { panic!("Failed to create event tap: {:?}", e); }); - let loop_source = tap.mach_port - .create_runloop_source(0) - .unwrap_or_else(|e| { - panic!("Failed to create runloop source: {:?}", e); - }); + let loop_source = tap.mach_port.create_runloop_source(0).unwrap_or_else(|e| { + panic!("Failed to create runloop source: {:?}", e); + }); unsafe { current.add_source(&loop_source, kCFRunLoopCommonModes); @@ -106,7 +113,6 @@ impl Hotkey { impl Global for Hotkey {} - struct EventHandler { tx: UnboundedSender, enable_left_cmd: bool, @@ -127,9 +133,7 @@ impl EventHandler { fn handle_flags_changed(&self, keycode: i64, flags: CGEventFlags) -> Option { let is_active = match keycode.into() { - Key::RightCommand => { - Some((flags.contains(CGEventFlags::CGEventFlagCommand), 0)) - } + Key::RightCommand => Some((flags.contains(CGEventFlags::CGEventFlagCommand), 0)), Key::Tab => { if self.enable_left_cmd { Some((flags.contains(CGEventFlags::CGEventFlagCommand), 1)) @@ -161,7 +165,11 @@ impl EventHandler { }) } - fn handle_key_down(&self, keycode: i64, flags: &mut CGEventFlags) -> Option<(HotkeyEvent, bool)> { + fn handle_key_down( + &self, + keycode: i64, + flags: &mut CGEventFlags, + ) -> Option<(HotkeyEvent, bool)> { if !IS_ACTIVE.load(Ordering::SeqCst) { return None; } @@ -170,15 +178,9 @@ impl EventHandler { flags.remove(CGEventFlags::CGEventFlagCommand); match Key::from(keycode) { - Key::Escape if !self.disable_quit => { - Some((HotkeyEvent::QuitApplication, true)) - }, - Key::Space => { - Some((HotkeyEvent::HideApplication, true)) - }, - _ => { - None - } + Key::Escape if !self.disable_quit => Some((HotkeyEvent::QuitApplication, true)), + Key::Space => Some((HotkeyEvent::HideApplication, true)), + _ => None, } } diff --git a/src/logger.rs b/src/logger.rs index 95b0c08..30df8ab 100644 --- a/src/logger.rs +++ b/src/logger.rs @@ -1,4 +1,7 @@ -use std::{fs::{self, File}, panic, thread}; +use std::{ + fs::{self, File}, + panic, thread, +}; use env_logger::Builder; use log::{error, LevelFilter}; diff --git a/src/main.rs b/src/main.rs index c56cd1e..62cce2d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,42 +5,43 @@ mod commander; mod config; mod hotkey; mod logger; -mod theme; mod socket; +mod theme; mod tray; mod ui; mod window; +#[allow(clippy::all)] mod socket_message { include!(concat!(env!("OUT_DIR"), "/_.rs")); } use applications::Applications; use assets::Assets; +use cocoa::appkit::NSApplication; +use cocoa::appkit::NSApplicationActivationPolicy; +use cocoa::base::nil; use commander::Commander; use config::Config; +use gpui::{App, Application}; use hotkey::Hotkey; use logger::Logger; use macos_accessibility_client::accessibility::application_is_trusted_with_prompt; -use theme::Theme; use socket::Socket; +use theme::Theme; use tray::Tray; use window::Window; -use cocoa::appkit::NSApplication; -use cocoa::appkit::NSApplicationActivationPolicy; -use cocoa::base::nil; -use gpui::{App, Application}; #[tokio::main] async fn main() { - Application::new() - .with_assets(Assets) - .run(|cx: &mut App| { + Application::new().with_assets(Assets).run(|cx: &mut App| { // Start the application in accessory mode, which means it won't appear in the dock. // - https://developer.apple.com/documentation/appkit/nsapplication/activationpolicy-swift.enum/accessory unsafe { let ns_app = NSApplication::sharedApplication(nil); - ns_app.setActivationPolicy_(NSApplicationActivationPolicy::NSApplicationActivationPolicyAccessory); + ns_app.setActivationPolicy_( + NSApplicationActivationPolicy::NSApplicationActivationPolicyAccessory, + ); } // Initialize the application components. diff --git a/src/socket.rs b/src/socket.rs index a35129a..2e6815a 100644 --- a/src/socket.rs +++ b/src/socket.rs @@ -1,16 +1,16 @@ +use anyhow::Context; +use gpui::App; +use log::error; +use prost::Message; use std::fs; +use std::io::prelude::*; use std::path::PathBuf; use std::process::Stdio; -use gpui::App; -use tokio::io::{AsyncReadExt, AsyncBufReadExt, BufReader}; +use tokio::io::{AsyncBufReadExt, AsyncReadExt, BufReader}; use tokio::net::UnixStream; -use tokio::sync::mpsc::UnboundedSender; use tokio::process::{Child, Command}; +use tokio::sync::mpsc::UnboundedSender; use tokio::time::{sleep, Duration}; -use anyhow::Context; -use std::io::prelude::*; -use prost::Message; -use log::error; use crate::commander::{Commander, EventType}; use crate::socket_message::SocketMessage; @@ -45,7 +45,10 @@ impl Socket { async fn handle_connection(tx: &UnboundedSender) -> std::io::Result<()> { let (stream, swift_monitor) = Self::establish_connection().await?; - let mut connection = Socket { stream, swift_monitor }; + let mut connection = Socket { + stream, + swift_monitor, + }; loop { if let Err(e) = Self::handle_message(&mut connection, tx).await { @@ -66,7 +69,10 @@ impl Socket { Ok((stream, swift_monitor)) } - async fn handle_message(connection: &mut Socket, tx: &UnboundedSender) -> std::io::Result<()> { + async fn handle_message( + connection: &mut Socket, + tx: &UnboundedSender, + ) -> std::io::Result<()> { let message = Self::read_message(connection).await?; Self::process_message(message, tx)?; Ok(()) @@ -84,16 +90,20 @@ impl Socket { .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e)) } - fn process_message(message: SocketMessage, tx: &UnboundedSender) -> std::io::Result<()> { + fn process_message( + message: SocketMessage, + tx: &UnboundedSender, + ) -> std::io::Result<()> { match message.event { Some(event) => { tx.send(EventType::SocketEvent(event)) - .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?; + .map_err(std::io::Error::other)?; Ok(()) } - None => { - Err(std::io::Error::new(std::io::ErrorKind::InvalidData, "Message missing event")) - } + None => Err(std::io::Error::new( + std::io::ErrorKind::InvalidData, + "Message missing event", + )), } } @@ -108,8 +118,7 @@ impl Socket { } } - Err(std::io::Error::new( - std::io::ErrorKind::Other, + Err(std::io::Error::other( "Swift process terminated or message not found", )) } @@ -149,16 +158,14 @@ impl Socket { let binary_path = Self::save_swift_binary(); match &binary_path { Ok(path) => { - let process =Command::new(path) + let process = Command::new(path) .env_remove("DYLD_LIBRARY_PATH") .stdout(Stdio::piped()) .spawn()?; Ok(process) - }, - Err(_) => { - Err(std::io::Error::new(std::io::ErrorKind::Other, "Failed to save Swift binary")) - }, + } + Err(_) => Err(std::io::Error::other("Failed to save Swift binary")), } } } diff --git a/src/theme.rs b/src/theme.rs index 89ffe28..a1d2b10 100644 --- a/src/theme.rs +++ b/src/theme.rs @@ -1,4 +1,4 @@ -use gpui::{App, Global, Hsla, hsla}; +use gpui::{hsla, App, Global, Hsla}; use serde::Deserialize; use crate::config::Config; diff --git a/src/tray.rs b/src/tray.rs index e4ac6a1..56d7e82 100644 --- a/src/tray.rs +++ b/src/tray.rs @@ -1,7 +1,10 @@ use gpui::{App, Global}; use log::error; use tokio::sync::mpsc::UnboundedSender; -use tray_icon::{menu::{Menu, MenuEvent, MenuItem}, TrayIcon, TrayIconBuilder}; +use tray_icon::{ + menu::{Menu, MenuEvent, MenuItem}, + TrayIcon, TrayIconBuilder, +}; use crate::commander::{Commander, EventType, TrayEvent}; @@ -26,7 +29,6 @@ impl MenuId { } } - impl Tray { pub fn new(cx: &mut App) { let tx = cx.global::().tx.clone(); @@ -45,18 +47,16 @@ impl Tray { .build() .unwrap(); - MenuEvent::set_event_handler(Some(move |event: MenuEvent| { - match event.id.0.as_str() { - id if id == MenuId::Settings.as_str() => Self::send_tray_event(&tx, TrayEvent::Settings), - id if id == MenuId::About.as_str() => Self::send_tray_event(&tx, TrayEvent::About), - id if id == MenuId::Quit.as_str() => Self::send_tray_event(&tx, TrayEvent::Quit), - _ => {} + MenuEvent::set_event_handler(Some(move |event: MenuEvent| match event.id.0.as_str() { + id if id == MenuId::Settings.as_str() => { + Self::send_tray_event(&tx, TrayEvent::Settings) } + id if id == MenuId::About.as_str() => Self::send_tray_event(&tx, TrayEvent::About), + id if id == MenuId::Quit.as_str() => Self::send_tray_event(&tx, TrayEvent::Quit), + _ => {} })); - cx.set_global(Self { - _tray: tray, - }); + cx.set_global(Self { _tray: tray }); } fn send_tray_event(tx: &UnboundedSender, event: TrayEvent) { @@ -76,10 +76,9 @@ impl Tray { let (width, height) = image.dimensions(); let rgba = image.into_raw(); - tray_icon::Icon::from_rgba(rgba, width, height) - .unwrap_or_else(|e| { - panic!("Failed to create icon: {:?}", e); - }) + tray_icon::Icon::from_rgba(rgba, width, height).unwrap_or_else(|e| { + panic!("Failed to create icon: {:?}", e); + }) } } diff --git a/src/ui/icon/mod.rs b/src/ui/icon/mod.rs index 56cd0e2..fe3195b 100644 --- a/src/ui/icon/mod.rs +++ b/src/ui/icon/mod.rs @@ -1,4 +1,4 @@ -use gpui::{App, IntoElement, RenderOnce, Styled, Transformation, Window, px, svg}; +use gpui::{px, svg, App, IntoElement, RenderOnce, Styled, Transformation, Window}; use crate::theme::Theme; @@ -10,16 +10,10 @@ pub(crate) struct Icon { transformation: Transformation, } -#[derive( - Debug, - PartialEq, - Eq, - Copy, - Clone, -)] +#[derive(Debug, PartialEq, Eq, Copy, Clone)] pub enum IconName { ArrowCircle, - ExternalLink + ExternalLink, } pub enum IconSize { diff --git a/src/ui/input/mod.rs b/src/ui/input/mod.rs index b67ed19..4fd54fb 100644 --- a/src/ui/input/mod.rs +++ b/src/ui/input/mod.rs @@ -1,24 +1,22 @@ mod blink_cursor; -use std::{ops::Range, sync::atomic::Ordering}; use blink_cursor::BlinkCursor; use gpui::{ - actions, div, fill, point, prelude::*, px, relative, size, App, AppContext, Bounds, CursorStyle, ElementId, ElementInputHandler, Entity, EntityInputHandler, FocusHandle, Focusable, Global, GlobalElementId, KeyBinding, KeyDownEvent, LayoutId, PaintQuad, Pixels, ShapedLine, SharedString, Style, TextRun, UTF16Selection, UnderlineStyle, Window + actions, div, fill, point, prelude::*, px, relative, size, App, AppContext, Bounds, + CursorStyle, ElementId, ElementInputHandler, Entity, EntityInputHandler, FocusHandle, + Focusable, Global, GlobalElementId, KeyBinding, KeyDownEvent, LayoutId, PaintQuad, Pixels, + ShapedLine, SharedString, Style, TextRun, UTF16Selection, UnderlineStyle, Window, }; +use std::{ops::Range, sync::atomic::Ordering}; use unicode_segmentation::UnicodeSegmentation; -use crate::{applications::{Applications, IndexType}, hotkey::IS_ACTIVE, theme::Theme}; +use crate::{ + applications::{Applications, IndexType}, + hotkey::IS_ACTIVE, + theme::Theme, +}; -actions!( - text_input, - [ - Tab, - ShiftTab, - Backspace, - Left, - Right, - ] -); +actions!(text_input, [Tab, ShiftTab, Backspace, Left, Right,]); pub struct SearchQuery { pub value: String, @@ -58,7 +56,8 @@ impl TextInput { }; // Observe the blink cursor to repaint the view when it changes. - cx.observe(&input.blink_cursor, |_, _, cx| cx.notify()).detach(); + cx.observe(&input.blink_cursor, |_, _, cx| cx.notify()) + .detach(); // Blink the cursor when the window is active, pause when it's not. cx.observe_window_activation(window, |input, window, cx| { @@ -78,7 +77,8 @@ impl TextInput { if IS_ACTIVE.load(Ordering::SeqCst) { cx.activate(true); } - }).detach(); + }) + .detach(); cx.bind_keys([ KeyBinding::new("tab", Tab, None), @@ -89,14 +89,15 @@ impl TextInput { ]); cx.set_global(SearchQuery { - value: String::new() + value: String::new(), }); cx.observe_global::(|input, cx| { if cx.global::().value.is_empty() { input.clear(cx); } - }).detach(); + }) + .detach(); input } @@ -163,7 +164,12 @@ impl TextInput { }); } - fn on_key_down_for_blink_cursor(&mut self, _: &KeyDownEvent, _window: &mut Window, cx: &mut Context) { + fn on_key_down_for_blink_cursor( + &mut self, + _: &KeyDownEvent, + _window: &mut Window, + cx: &mut Context, + ) { self.pause_blink_cursor(cx); } @@ -263,7 +269,11 @@ impl EntityInputHandler for TextInput { }) } - fn marked_text_range(&self, _window: &mut Window, _cx: &mut Context) -> Option> { + fn marked_text_range( + &self, + _window: &mut Window, + _cx: &mut Context, + ) -> Option> { self.marked_range .as_ref() .map(|range| self.range_to_utf16(range)) @@ -287,8 +297,7 @@ impl EntityInputHandler for TextInput { .unwrap_or(self.selected_range.clone()); self.value = - (self.value[0..range.start].to_owned() + new_text + &self.value[range.end..]) - .into(); + (self.value[0..range.start].to_owned() + new_text + &self.value[range.end..]).into(); self.selected_range = range.start + new_text.len()..range.start + new_text.len(); self.marked_range.take(); @@ -316,12 +325,15 @@ impl EntityInputHandler for TextInput { .unwrap_or(self.selected_range.clone()); self.value = - (self.value[0..range.start].to_owned() + new_text + &self.value[range.end..]) - .into(); + (self.value[0..range.start].to_owned() + new_text + &self.value[range.end..]).into(); self.marked_range = Some(range.start..range.start + new_text.len()); self.selected_range = new_selected_range_utf16 .as_ref() - .map(|range_utf16| self.range_from_utf16(range_utf16)).map_or_else(|| range.start + new_text.len()..range.start + new_text.len(), |new_range| new_range.start + range.start..new_range.end + range.end); + .map(|range_utf16| self.range_from_utf16(range_utf16)) + .map_or_else( + || range.start + new_text.len()..range.start + new_text.len(), + |new_range| new_range.start + range.start..new_range.end + range.end, + ); cx.notify(); } @@ -481,10 +493,11 @@ impl Element for TextElement { window.handle_input( &focus_handle, ElementInputHandler::new(bounds, self.input.clone()), - cx + cx, ); let line = prepaint.line.take().unwrap(); - line.paint(bounds.origin, window.line_height(), window, cx).unwrap(); + line.paint(bounds.origin, window.line_height(), window, cx) + .unwrap(); if focus_handle.is_focused(window) { if let Some(cursor) = prepaint.cursor.take() { diff --git a/src/ui/list/mod.rs b/src/ui/list/mod.rs index 4a4ce34..b572b9e 100644 --- a/src/ui/list/mod.rs +++ b/src/ui/list/mod.rs @@ -1,14 +1,21 @@ use std::time::Duration; -use gpui::{div, img, percentage, prelude, px, uniform_list, Animation, AnimationExt, App, Context, Div, Element, InteractiveElement, IntoElement, ParentElement, Render, ScrollStrategy, Styled, Transformation, UniformListScrollHandle, Window}; -use prelude::FluentBuilder; use fuzzy_matcher::skim::SkimMatcherV2; use fuzzy_matcher::FuzzyMatcher; +use gpui::{ + div, img, percentage, prelude, px, uniform_list, Animation, AnimationExt, App, Context, Div, + Element, InteractiveElement, IntoElement, ParentElement, Render, ScrollStrategy, Styled, + Transformation, UniformListScrollHandle, Window, +}; +use prelude::FluentBuilder; -use crate::{applications::Applications, theme::Theme}; -use crate::socket_message::App as Application; use super::icon::{IconColor, IconSize}; -use super::{icon::{Icon, IconName}, input::SearchQuery}; +use super::{ + icon::{Icon, IconName}, + input::SearchQuery, +}; +use crate::socket_message::App as Application; +use crate::{applications::Applications, theme::Theme}; pub struct List { pub items: Vec, @@ -31,9 +38,7 @@ impl List { if query.is_empty() { list.retain(|item| item.pid != 0); } else { - list.retain(|item| { - matcher.fuzzy_match(&item.name, query).is_some() - }); + list.retain(|item| matcher.fuzzy_match(&item.name, query).is_some()); list.sort_by(|a, b| { let score_a = matcher.fuzzy_match(&a.name, query).unwrap_or(0); @@ -48,7 +53,10 @@ impl List { } else if a.pid == 0 && b.pid != 0 { std::cmp::Ordering::Greater } else if a.pid == 0 && b.pid == 0 { - match (a.path.starts_with("/Applications/"), b.path.starts_with("/Applications/")) { + match ( + a.path.starts_with("/Applications/"), + b.path.starts_with("/Applications/"), + ) { (true, false) => std::cmp::Ordering::Less, (false, true) => std::cmp::Ordering::Greater, _ => std::cmp::Ordering::Equal, @@ -72,7 +80,7 @@ impl List { list } - fn render_empty_state(&self, theme: &Theme, child: impl IntoElement) -> Div { + fn render_empty_state(&self, theme: &Theme, child: impl IntoElement) -> Div { div() .flex() .h_full() @@ -95,14 +103,17 @@ impl Render for List { self.list.scroll_to_item(index, ScrollStrategy::Top); if loading { - return self.render_empty_state( - theme, - Icon::new(IconName::ArrowCircle, IconSize::Default, IconColor::Default).with_animation( - "arrow-circle", - Animation::new(Duration::from_secs(2)).repeat(), - |icon, delta| icon.transform(Transformation::rotate(percentage(delta))), + return self + .render_empty_state( + theme, + Icon::new(IconName::ArrowCircle, IconSize::Default, IconColor::Default) + .with_animation( + "arrow-circle", + Animation::new(Duration::from_secs(2)).repeat(), + |icon, delta| icon.transform(Transformation::rotate(percentage(delta))), + ), ) - ).into_any(); + .into_any(); } // Update the list with the filtered applications. @@ -110,38 +121,53 @@ impl Render for List { let scroll_handle = self.list.clone(); if !self.items.is_empty() { - div().child(uniform_list(cx.entity().clone(), "entries", self.items.len(), { - let list = self.items.clone(); - move |_this, range, _window, cx| { - let theme = cx.global::(); - - range.map(|i| { - let name = list[i].name.to_string(); - let icon = list[i].icon.to_string(); - let pid = list[i].pid; - - div() - .id(i) - .cursor_pointer() - .flex() - .h(px(40.0)) - .w_full() - .items_center() - .gap_2() - .rounded(px(4.0)) - .p_1() - .text_sm() - .text_color(theme.foreground) - .when(i == index, |cx| cx.bg(theme.muted)) - .child(img(icon).h(px(32.0)).w(px(32.0))) - .child(div().child(name).flex_1()) - .when(pid == 0, |cx| cx.child(div().mr_0p5().child(Icon::new(IconName::ExternalLink, IconSize::Small, IconColor::Muted)))) - .when(pid == 0, |cx| cx.opacity(0.6)) - }).collect::>() - } - }).h_full().track_scroll(scroll_handle)).h_full() + div() + .child( + uniform_list(cx.entity().clone(), "entries", self.items.len(), { + let list = self.items.clone(); + move |_this, range, _window, cx| { + let theme = cx.global::(); + + range + .map(|i| { + let name = list[i].name.to_string(); + let icon = list[i].icon.to_string(); + let pid = list[i].pid; + + div() + .id(i) + .cursor_pointer() + .flex() + .h(px(40.0)) + .w_full() + .items_center() + .gap_2() + .rounded(px(4.0)) + .p_1() + .text_sm() + .text_color(theme.foreground) + .when(i == index, |cx| cx.bg(theme.muted)) + .child(img(icon).h(px(32.0)).w(px(32.0))) + .child(div().child(name).flex_1()) + .when(pid == 0, |cx| { + cx.child(div().mr_0p5().child(Icon::new( + IconName::ExternalLink, + IconSize::Small, + IconColor::Muted, + ))) + }) + .when(pid == 0, |cx| cx.opacity(0.6)) + }) + .collect::>() + } + }) + .h_full() + .track_scroll(scroll_handle), + ) + .h_full() } else { self.render_empty_state(theme, "No applications found") - }.into_any() + } + .into_any() } } diff --git a/src/ui/mod.rs b/src/ui/mod.rs index be3d568..a500caf 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -2,10 +2,13 @@ pub mod icon; pub mod input; pub mod list; -use gpui::{div, prelude, px, App, AppContext, ClickEvent, Context, Entity, InteractiveElement, IntoElement, ParentElement, Render, StatefulInteractiveElement, Styled, Window}; +use gpui::{ + div, prelude, px, App, AppContext, ClickEvent, Context, Entity, InteractiveElement, + IntoElement, ParentElement, Render, StatefulInteractiveElement, Styled, Window, +}; use input::{SearchQuery, TextInput}; -use prelude::FluentBuilder; use macos_accessibility_client::accessibility::application_is_trusted_with_prompt; +use prelude::FluentBuilder; use crate::{applications::Applications, config::Config, theme::Theme, ui::list::List}; @@ -27,10 +30,19 @@ impl Container { let trusted = application_is_trusted_with_prompt(); - Self { input, list, trusted } + Self { + input, + list, + trusted, + } } - fn update_accessibility_permission(&mut self, _: &ClickEvent, _window: &mut Window, cx: &mut Context) { + fn update_accessibility_permission( + &mut self, + _: &ClickEvent, + _window: &mut Window, + cx: &mut Context, + ) { if application_is_trusted_with_prompt() { if let Ok(current_exe) = std::env::current_exe() { cx.restart(Some(current_exe)); @@ -58,7 +70,7 @@ impl Container { fn render_accessibility_prompt( &self, theme: &Theme, - listener: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static + listener: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static, ) -> impl IntoElement { div() .flex() @@ -94,7 +106,7 @@ impl Container { .px_2() .mt_3() .cursor_pointer() - .child("Check again") + .child("Check again"), ) } @@ -102,7 +114,7 @@ impl Container { &self, theme: &Theme, label: impl Into, - shortcut: impl Into + shortcut: impl Into, ) -> impl IntoElement { div() .flex() @@ -115,7 +127,7 @@ impl Container { div() .text_color(theme.muted_foreground) .mb_0p5() - .child(shortcut.into()) + .child(shortcut.into()), ) } } @@ -139,11 +151,12 @@ impl Render for Container { .p(px(5.0)) .mx(px(10.)) .child(self.input.clone()) - .when(self.trusted, |cx| { - cx.child(self.list.clone()) - }) + .when(self.trusted, |cx| cx.child(self.list.clone())) .when(!self.trusted, |element| { - element.child(self.render_accessibility_prompt(theme, cx.listener(Self::update_accessibility_permission))) + element.child(self.render_accessibility_prompt( + theme, + cx.listener(Self::update_accessibility_permission), + )) }) .child( div() @@ -161,7 +174,7 @@ impl Render for Container { .child(self.render_action_button(theme, "Hide", "␣")) .when(!disable_quit, |cx| { cx.child(self.render_action_button(theme, "Quit", "⎋")) - }) + }), ) } } diff --git a/src/window.rs b/src/window.rs index 018788e..4dd007b 100644 --- a/src/window.rs +++ b/src/window.rs @@ -1,8 +1,14 @@ use core_graphics::display::{CGDisplay, CGPoint}; -use gpui::{App, AppContext, Bounds, DisplayId, Global, Pixels, Point, Size, WindowBackgroundAppearance, WindowBounds, WindowHandle, WindowKind, WindowOptions}; +use gpui::{ + App, AppContext, Bounds, DisplayId, Global, Pixels, Point, Size, WindowBackgroundAppearance, + WindowBounds, WindowHandle, WindowKind, WindowOptions, +}; use mouse_position::mouse_position::Mouse; -use crate::{applications::{Applications, IndexType}, ui::Container}; +use crate::{ + applications::{Applications, IndexType}, + ui::Container, +}; pub struct Window { pub window: WindowHandle, @@ -13,13 +19,16 @@ impl Window { pub fn new(cx: &mut App) { // Calculate the bounds of the active display. let display_id = Self::get_active_display_id(cx); - let bounds = cx.displays().iter().find(|d| d.id() == display_id).map_or(Bounds { - origin: Point::new(Pixels::from(0.0), Pixels::from(0.0)), - size: Size { - width: Pixels::from(1920.0), - height: Pixels::from(1080.0), + let bounds = cx.displays().iter().find(|d| d.id() == display_id).map_or( + Bounds { + origin: Point::new(Pixels::from(0.0), Pixels::from(0.0)), + size: Size { + width: Pixels::from(1920.0), + height: Pixels::from(1080.0), + }, }, - }, |d| d.bounds()); + |d| d.bounds(), + ); // Calculate the height and position of the window. let height = Pixels(bounds.size.height.0 * 0.5); @@ -29,24 +38,23 @@ impl Window { let y: Pixels = Pixels(bounds.size.height.0 * 0.3); // Launch the window. - let window = cx.open_window( - WindowOptions { - titlebar: None, - window_bounds: Some(WindowBounds::Windowed(Bounds::new( - Point { x, y }, - Size { width, height }, - ))), - window_background: WindowBackgroundAppearance::Blurred, - kind: WindowKind::PopUp, - is_movable: false, - display_id: Some(display_id), - ..Default::default() - }, - |window, cx| { - cx.new(|cx| Container::new(window, cx)) - }, - ) - .unwrap(); + let window = cx + .open_window( + WindowOptions { + titlebar: None, + window_bounds: Some(WindowBounds::Windowed(Bounds::new( + Point { x, y }, + Size { width, height }, + ))), + window_background: WindowBackgroundAppearance::Blurred, + kind: WindowKind::PopUp, + is_movable: false, + display_id: Some(display_id), + ..Default::default() + }, + |window, cx| cx.new(|cx| Container::new(window, cx)), + ) + .unwrap(); // Auto focus the input field. window @@ -55,10 +63,7 @@ impl Window { }) .unwrap(); - cx.set_global(Self { - window, - display_id, - }); + cx.set_global(Self { window, display_id }); } pub fn show(cx: &mut App, offset: usize) { @@ -75,26 +80,37 @@ impl Window { // Delay the opening of the window to prevent flickering. if display_id != active_display_id { cx.spawn(|cx| async move { - cx.background_executor().timer(std::time::Duration::from_millis(0)).await; + cx.background_executor() + .timer(std::time::Duration::from_millis(0)) + .await; cx.update(|cx| { Self::new(cx); }) - }).detach(); + }) + .detach(); } else { Self::new(cx); } } pub fn hide(cx: &mut App) { - let _ = cx.global::().window.clone().update(cx, |_view, _window, cx| { - cx.hide(); - }); + let _ = cx + .global::() + .window + .clone() + .update(cx, |_view, _window, cx| { + cx.hide(); + }); } pub fn close(cx: &mut App) { - let _ = cx.global::().window.clone().update(cx, |_view, window, _cx| { - window.remove_window(); - }); + let _ = cx + .global::() + .window + .clone() + .update(cx, |_view, window, _cx| { + window.remove_window(); + }); } fn get_active_display_id(cx: &mut App) -> DisplayId { @@ -112,7 +128,10 @@ impl Window { let display = CGDisplay::new(display_id); let bounds = display.bounds(); - if bounds.contains(&CGPoint { x: f64::from(x), y: f64::from(y) }) { + if bounds.contains(&CGPoint { + x: f64::from(x), + y: f64::from(y), + }) { // Find the corresponding GPUI display, since that returns a DisplayId that we can use to open a window. let gpui_display = gpui_displays.iter().find(|d| { // We can't access the private integer, but the struct does implement fmt based on the private integer 🥴. @@ -125,7 +144,7 @@ impl Window { } fallback_display_id - }, + } Mouse::Error => fallback_display_id, } }