From dbcf03809bc758e7c2fa70f2badc7c03029ab8dd Mon Sep 17 00:00:00 2001 From: unsecretised Date: Tue, 30 Dec 2025 20:53:01 +0800 Subject: [PATCH] Add documentation --- src/app.rs | 57 ++++++++++++++++++- src/calculator.rs | 137 +++++++++++++++++++++++++--------------------- src/clipboard.rs | 4 ++ src/commands.rs | 6 +- src/config.rs | 12 ++++ src/macos.rs | 19 +++++-- src/utils.rs | 10 ++++ 7 files changed, 174 insertions(+), 71 deletions(-) diff --git a/src/app.rs b/src/app.rs index f3a0ffc..0a5cd57 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,3 +1,4 @@ +//! Main logic for the app use crate::calculator::Expression; use crate::clipboard::ClipBoardContentType; use crate::commands::Function; @@ -31,11 +32,20 @@ use std::cmp::min; use std::time::Duration; use std::{fs, thread}; +/// The default window width pub const WINDOW_WIDTH: f32 = 500.; + +/// The default window height pub const DEFAULT_WINDOW_HEIGHT: f32 = 65.; +/// The rustcast descriptor name to be put for all rustcast commands pub const RUSTCAST_DESC_NAME: &str = "RustCast"; +/// The main app struct, that represents an "App" +/// +/// This struct represents a command that rustcast can perform, providing the rustcast +/// the data needed to search for the app, to display the app in search results, and to actually +/// "run" the app. #[derive(Debug, Clone)] pub struct App { pub open_command: Function, @@ -46,6 +56,7 @@ pub struct App { } impl App { + /// This returns the basic apps that rustcast has, such as quiting rustcast and opening preferences pub fn basic_apps() -> Vec { vec![ App { @@ -65,6 +76,7 @@ impl App { ] } + /// This renders the app into an iced element, allowing it to be displayed in the search results pub fn render(&self, theme: &crate::config::Theme) -> impl Into> { let mut tile = Row::new().width(Fill).height(55); @@ -119,12 +131,14 @@ impl App { } } +/// The different pages that rustcast can have / has #[derive(Debug, Clone, PartialEq)] pub enum Page { Main, ClipboardHistory, } +/// The message type that iced uses for actions that can do something #[derive(Debug, Clone)] pub enum Message { OpenWindow, @@ -140,6 +154,7 @@ pub enum Message { _Nothing, } +/// The window settings for rustcast pub fn default_settings() -> Settings { Settings { resizable: false, @@ -156,6 +171,21 @@ pub fn default_settings() -> Settings { } } +/// This is the base window, and its a "Tile" +/// Its fields are: +/// - Theme ([`iced::Theme`]) +/// - Query (String) +/// - Query Lowercase (String, but lowercase) +/// - Previous Query Lowercase (String) +/// - Results (Vec<[`App`]>) the results of the search +/// - Options (Vec<[`App`]>) the options to search through +/// - Visible (bool) whether the window is visible or not +/// - Focused (bool) whether the window is focused or not +/// - Frontmost ([`Option>`]) the frontmost application before the window was opened +/// - Config ([`Config`]) the app's config +/// - Open Hotkey ID (`u32`) the id of the hotkey that opens the window +/// - Clipboard Content (`Vec<`[`ClipBoardContentType`]`>`) all of the cliboard contents +/// - Page ([`Page`]) the current page of the window (main or clipboard history) #[derive(Debug, Clone)] pub struct Tile { theme: iced::Theme, @@ -174,7 +204,7 @@ pub struct Tile { } impl Tile { - /// A base window + /// Initialise the base window pub fn new(keybind_id: u32, config: &Config) -> (Self, Task) { let (id, open) = window::open(default_settings()); @@ -227,6 +257,7 @@ impl Tile { ) } + /// This handles the iced's updates, which have all the variants of [Message] pub fn update(&mut self, message: Message) -> Task { match message { Message::OpenWindow => { @@ -412,6 +443,10 @@ impl Tile { } } + /// This is the view of the window. It handles the rendering of the window + /// + /// The rendering of the window size (the resizing of the window) is handled by the + /// [`Tile::update`] function. pub fn view(&self, wid: window::Id) -> Element<'_, Message> { if self.visible { let title_input = text_input(self.config.placeholder.as_str(), &self.query) @@ -456,10 +491,20 @@ impl Tile { } } + /// This returns the theme of the window pub fn theme(&self, _: window::Id) -> Option { Some(self.theme.clone()) } + /// This handles the subscriptions of the window + /// + /// The subscriptions are: + /// - Hotkeys + /// - Hot reloading + /// - Clipboard history + /// - Window close events + /// - Keypresses (escape to close the window) + /// - Window focus changes pub fn subscription(&self) -> Subscription { Subscription::batch([ Subscription::run(handle_hotkeys), @@ -492,6 +537,11 @@ impl Tile { ]) } + /// Handles the search query changed event. + /// + /// This is separate from the `update` function because it has a decent amount of logic, and + /// should be separated out to make it easier to test. This function is called by the `update` + /// function to handle the search query changed event. pub fn handle_search_query_changed(&mut self) { let filter_vec: &Vec = if self.query_lc.starts_with(&self.prev_query_lc) { self.prev_query_lc = self.query_lc.to_owned(); @@ -526,6 +576,7 @@ impl Tile { self.results = exact; } + /// Gets the frontmost application to focus later. pub fn capture_frontmost(&mut self) { use objc2_app_kit::NSWorkspace; @@ -533,6 +584,7 @@ impl Tile { self.frontmost = ws.frontmostApplication(); } + /// Restores the frontmost application. #[allow(deprecated)] pub fn restore_frontmost(&mut self) { use objc2_app_kit::NSApplicationActivationOptions; @@ -543,6 +595,7 @@ impl Tile { } } +/// This is the subscription function that handles hot reloading of the config fn handle_hot_reloading() -> impl futures::Stream { stream::channel(100, async |mut output| { let content = fs::read_to_string( @@ -563,6 +616,7 @@ fn handle_hot_reloading() -> impl futures::Stream { }) } +/// This is the subscription function that handles hotkeys for hiding / showing the window fn handle_hotkeys() -> impl futures::Stream { stream::channel(100, async |mut output| { let receiver = GlobalHotKeyEvent::receiver(); @@ -577,6 +631,7 @@ fn handle_hotkeys() -> impl futures::Stream { }) } +/// This is the subscription function that handles the change in clipboard history fn handle_clipboard_history() -> impl futures::Stream { stream::channel(100, async |mut output| { let mut clipboard = Clipboard::new().unwrap(); diff --git a/src/calculator.rs b/src/calculator.rs index b5255e4..f16e1c6 100644 --- a/src/calculator.rs +++ b/src/calculator.rs @@ -1,3 +1,6 @@ +//! This handle the logic for the calculator in rustcast + +/// A struct that represents an expression #[derive(Debug, Clone, Copy)] pub struct Expression { pub first_num: f64, @@ -5,6 +8,7 @@ pub struct Expression { pub second_num: f64, } +/// An enum that represents the different operations that can be performed on an expression #[derive(Debug, Clone, Copy)] pub enum Operation { Addition, @@ -15,6 +19,7 @@ pub enum Operation { } impl Expression { + /// This evaluates the expression pub fn eval(&self) -> f64 { match self.operation { Operation::Addition => self.first_num + self.second_num, @@ -25,79 +30,85 @@ impl Expression { } } + /// This parses an expression from a string (and is public) + /// + /// This function is public because it is used in the `handle_search_query_changed` function, + /// and the parse expression function, while doing the same thing, should not be public due to + /// the function name, not portraying the intention of the function. pub fn from_str(s: &str) -> Option { - parse_expression(s) - } -} - -fn parse_expression(s: &str) -> Option { - let s = s.trim(); - - // 1. Parse first (possibly signed) number with manual scan - let (first_str, rest) = parse_signed_number_prefix(s)?; - - // 2. Next non‑whitespace char must be the binary operator - let rest = rest.trim_start(); - let (op_char, rest) = rest.chars().next().map(|c| (c, &rest[c.len_utf8()..]))?; - - let operation = match op_char { - '+' => Operation::Addition, - '-' => Operation::Subtraction, - '*' => Operation::Multiplication, - '/' => Operation::Division, - '^' => Operation::Power, - _ => return None, - }; - - // 3. The remainder should be the second (possibly signed) number - let rest = rest.trim_start(); - let (second_str, tail) = parse_signed_number_prefix(rest)?; - // Optionally ensure nothing but whitespace after second number: - if !tail.trim().is_empty() { - return None; + Self::parse_expression(s) } - let first_num: f64 = first_str.parse().ok()?; - let second_num: f64 = second_str.parse().ok()?; + /// This is the function that parses an expression from a string + fn parse_expression(s: &str) -> Option { + let s = s.trim(); + + // 1. Parse first (possibly signed) number with manual scan + let (first_str, rest) = Self::parse_signed_number_prefix(s)?; + + // 2. Next non‑whitespace char must be the binary operator + let rest = rest.trim_start(); + let (op_char, rest) = rest.chars().next().map(|c| (c, &rest[c.len_utf8()..]))?; + + let operation = match op_char { + '+' => Operation::Addition, + '-' => Operation::Subtraction, + '*' => Operation::Multiplication, + '/' => Operation::Division, + '^' => Operation::Power, + _ => return None, + }; + + // 3. The remainder should be the second (possibly signed) number + let rest = rest.trim_start(); + let (second_str, tail) = Self::parse_signed_number_prefix(rest)?; + // Optionally ensure nothing but whitespace after second number: + if !tail.trim().is_empty() { + return None; + } - Some(Expression { - first_num, - operation, - second_num, - }) -} + let first_num: f64 = first_str.parse().ok()?; + let second_num: f64 = second_str.parse().ok()?; -/// Returns (number_lexeme, remaining_slice) for a leading signed float. -/// Very simple: `[+|-]?` + "anything until we hit whitespace or an operator". -fn parse_signed_number_prefix(s: &str) -> Option<(&str, &str)> { - let s = s.trim_start(); - if s.is_empty() { - return None; + Some(Expression { + first_num, + operation, + second_num, + }) } - let mut chars = s.char_indices().peekable(); + /// Returns (number_lexeme, remaining_slice) for a leading signed float. + /// Very simple: `[+|-]?` + "anything until we hit whitespace or an operator". + fn parse_signed_number_prefix(s: &str) -> Option<(&str, &str)> { + let s = s.trim_start(); + if s.is_empty() { + return None; + } + + let mut chars = s.char_indices().peekable(); - // Optional leading sign - if let Some((_, c)) = chars.peek() - && (*c == '+' || *c == '-') - { - chars.next(); - } + // Optional leading sign + if let Some((_, c)) = chars.peek() + && (*c == '+' || *c == '-') + { + chars.next(); + } - // Now consume until we hit an operator or whitespace - let mut end = 0; - while let Some((idx, c)) = chars.peek().cloned() { - if c.is_whitespace() || "+-*/^".contains(c) { - break; + // Now consume until we hit an operator or whitespace + let mut end = 0; + while let Some((idx, c)) = chars.peek().cloned() { + if c.is_whitespace() || "+-*/^".contains(c) { + break; + } + end = idx + c.len_utf8(); + chars.next(); } - end = idx + c.len_utf8(); - chars.next(); - } - if end == 0 { - return None; // nothing that looks like a number - } + if end == 0 { + return None; // nothing that looks like a number + } - let (num, rest) = s.split_at(end); - Some((num, rest)) + let (num, rest) = s.split_at(end); + Some((num, rest)) + } } diff --git a/src/clipboard.rs b/src/clipboard.rs index fb79447..b9ca0d3 100644 --- a/src/clipboard.rs +++ b/src/clipboard.rs @@ -1,3 +1,4 @@ +//! This has all the logic regarding the cliboard history use arboard::ImageData; use iced::{ Length::Fill, @@ -8,6 +9,7 @@ use iced::{ use crate::{app::Message, commands::Function}; +/// The kinds of clipboard content that rustcast can handle and their contents #[derive(Debug, Clone)] pub enum ClipBoardContentType { Text(String), @@ -15,6 +17,7 @@ pub enum ClipBoardContentType { } impl ClipBoardContentType { + /// Returns the iced element for rendering the clipboard item pub fn render_clipboard_item(&self) -> impl Into> { let mut tile = Row::new().width(Fill).height(55); @@ -58,6 +61,7 @@ impl ClipBoardContentType { } impl PartialEq for ClipBoardContentType { + /// Let cliboard items be comparable fn eq(&self, other: &Self) -> bool { if let Self::Text(a) = self && let Self::Text(b) = other diff --git a/src/commands.rs b/src/commands.rs index 1a97c80..4a3bb7e 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -1,3 +1,5 @@ +//! This handles all the different commands that rustcast can perform, such as opening apps, +//! copying to clipboard, etc. use std::{process::Command, thread}; use arboard::Clipboard; @@ -6,11 +8,12 @@ use objc2_foundation::NSURL; use crate::{calculator::Expression, clipboard::ClipBoardContentType, config::Config}; +/// The different functions that rustcast can perform #[derive(Debug, Clone)] pub enum Function { OpenApp(String), RunShellCommand(String, String), - RandomVar(i32), + RandomVar(i32), // Easter egg function CopyToClipboard(ClipBoardContentType), GoogleSearch(String), Calculate(Expression), @@ -19,6 +22,7 @@ pub enum Function { } impl Function { + /// Run the command pub fn execute(&self, config: &Config, query: &str) { match self { Function::OpenApp(path) => { diff --git a/src/config.rs b/src/config.rs index e3c4d0c..9a0c300 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,3 +1,4 @@ +//! This is the config file type definitions for rustcast use std::{path::Path, sync::Arc}; use iced::{theme::Custom, widget::image::Handle}; @@ -5,6 +6,7 @@ use serde::{Deserialize, Serialize}; use crate::{app::App, commands::Function, utils::handle_from_icns}; +/// The main config struct (effectively the config file's "schema") #[derive(Debug, Clone, Deserialize, Serialize)] #[serde(default)] pub struct Config { @@ -18,6 +20,7 @@ pub struct Config { } impl Default for Config { + /// The default config fn default() -> Self { Self { toggle_mod: "ALT".to_string(), @@ -31,6 +34,7 @@ impl Default for Config { } } +/// The settings you can set for the theme #[derive(Debug, Clone, Deserialize, Serialize)] #[serde(default)] pub struct Theme { @@ -90,6 +94,7 @@ impl From for iced::Theme { } impl Theme { + /// Return the text color in the theme config of type [`iced::Color`] pub fn text_color(&self, opacity: f32) -> iced::Color { let theme = self.to_owned(); iced::Color { @@ -100,6 +105,7 @@ impl Theme { } } + /// Return the background color in the theme config of type [`iced::Color`] pub fn bg_color(&self) -> iced::Color { iced::Color { r: self.background_color.0, @@ -110,6 +116,11 @@ impl Theme { } } +/// The rules for the buffer AKA search results +/// +/// - clear_on_hide is whether the buffer should be cleared when the window is hidden +/// - clear_on_enter is whether the buffer should be cleared when the user presses enter after +/// searching #[derive(Debug, Clone, Deserialize, Serialize)] #[serde(default)] pub struct Buffer { @@ -138,6 +149,7 @@ pub struct Shelly { } impl Shelly { + /// Converts the shelly struct to an app so that it can be added to the app list pub fn to_app(&self) -> App { let self_clone = self.clone(); let icon = self_clone.icon_path.and_then(|x| { diff --git a/src/macos.rs b/src/macos.rs index 4e51ee1..87f53dc 100644 --- a/src/macos.rs +++ b/src/macos.rs @@ -1,8 +1,9 @@ -#![allow(deprecated)] - +//! Macos specific logic, such as window settings, etc. #[cfg(target_os = "macos")] use iced::wgpu::rwh::WindowHandle; +/// This sets the activation policy of the app to Accessory, allowing rustcast to be visible ontop +/// of fullscreen apps #[cfg(target_os = "macos")] pub fn set_activation_policy_accessory() { use objc2::MainThreadMarker; @@ -13,6 +14,7 @@ pub fn set_activation_policy_accessory() { app.setActivationPolicy(NSApplicationActivationPolicy::Accessory); } +/// This carries out the window configuration for the macos window (only things that are macos specific) #[cfg(target_os = "macos")] pub fn macos_window_config(handle: &WindowHandle) { use iced::wgpu::rwh::RawWindowHandle; @@ -40,6 +42,8 @@ pub fn macos_window_config(handle: &WindowHandle) { } } +/// This is the function that forces focus onto rustcast +#[allow(deprecated)] #[cfg(target_os = "macos")] pub fn focus_this_app() { use objc2::MainThreadMarker; @@ -51,28 +55,31 @@ pub fn focus_this_app() { app.activateIgnoringOtherApps(true); } -// because objc2_application_services is mean and this type isn't public +/// This is the struct that represents the process serial number, allowing us to transform the process to a UI element #[repr(C)] struct ProcessSerialNumber { low: u32, hi: u32, } -/// see mostly https://github.com/electron/electron/blob/e181fd040f72becd135db1fa977622b81da21643/shell/browser/browser_mac.mm#L512C1-L532C2. +/// This is the function that transforms the process to a UI element, and hides the dock icon +/// +/// see mostly /// /// returns ApplicationServices OSStatus (u32) /// /// doesn't seem to do anything if you haven't opened a window yet, so wait to call it until after that. pub fn transform_process_to_ui_element() -> u32 { - use std::ptr; - use objc2_application_services::{ TransformProcessType, kCurrentProcess, kProcessTransformToUIElementApplication, }; + use std::ptr; + let psn = ProcessSerialNumber { low: 0, hi: kCurrentProcess, }; + unsafe { TransformProcessType( ptr::from_ref(&psn).cast(), diff --git a/src/utils.rs b/src/utils.rs index ecdf8ad..f6a4c53 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,3 +1,4 @@ +//! This has all the utility functions that rustcast uses use std::{ fs::{self, File}, io::Write, @@ -13,19 +14,24 @@ use rayon::iter::{IntoParallelIterator, ParallelIterator}; use crate::{app::App, commands::Function}; +/// The default error log path (works only on unix systems, and must be changed for windows +/// support) const ERR_LOG_PATH: &str = "/tmp/rustscan-err.log"; +/// This logs an error to the error log file pub(crate) fn log_error(msg: &str) { if let Ok(mut file) = File::options().create(true).append(true).open(ERR_LOG_PATH) { let _ = file.write_all(msg.as_bytes()).ok(); } } +/// This logs an error to the error log file, and exits the program pub(crate) fn log_error_and_exit(msg: &str) { log_error(msg); exit(-1) } +/// This converts an icns file to an iced image handle pub(crate) fn handle_from_icns(path: &Path) -> Option { let data = std::fs::read(path).ok()?; let family = IconFamily::read(std::io::Cursor::new(&data)).ok()?; @@ -45,6 +51,9 @@ pub(crate) fn handle_from_icns(path: &Path) -> Option { )) } +/// This gets all the installed apps in the given directory +/// +/// the directories are defined in [`crate::app::Tile::new`] pub(crate) fn get_installed_apps(dir: impl AsRef, store_icons: bool) -> Vec { let entries: Vec<_> = fs::read_dir(dir.as_ref()) .unwrap_or_else(|x| { @@ -163,6 +172,7 @@ pub(crate) fn get_installed_apps(dir: impl AsRef, store_icons: bool) -> Ve .collect() } +/// This converts a string to a [`Code`] enum so that it can be used as a hotkey pub fn to_key_code(key_str: &str) -> Option { match key_str.to_lowercase().as_str() { // Letters