From 5173f4e000d10523c113b376a9ebf401af0d6e7b Mon Sep 17 00:00:00 2001 From: unsecretised Date: Sat, 27 Dec 2025 17:36:39 +0800 Subject: [PATCH 1/3] calculator functionality added --- src/app.rs | 14 +++++++ src/calculator.rs | 103 ++++++++++++++++++++++++++++++++++++++++++++++ src/commands.rs | 10 ++++- src/main.rs | 1 + 4 files changed, 127 insertions(+), 1 deletion(-) create mode 100644 src/calculator.rs diff --git a/src/app.rs b/src/app.rs index d0279d6..7818731 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,3 +1,4 @@ +use crate::calculator::Expression; use crate::commands::Function; use crate::config::Config; use crate::macos::{focus_this_app, transform_process_to_ui_element}; @@ -263,9 +264,22 @@ impl Tile { } self.handle_search_query_changed(); + + if self.results.is_empty() { + if let Some(res) = Expression::from_str(&self.query) { + self.results.push(App { + open_command: Function::Calculate(res), + desc: RUSTCAST_DESC_NAME.to_string(), + icons: None, + name: res.eval().to_string(), + name_lc: "".to_string(), + }); + } + } let new_length = self.results.len(); let max_elem = min(5, new_length); + if prev_size != new_length { window::resize( id, diff --git a/src/calculator.rs b/src/calculator.rs new file mode 100644 index 0000000..13fe07d --- /dev/null +++ b/src/calculator.rs @@ -0,0 +1,103 @@ +#[derive(Debug, Clone, Copy)] +pub struct Expression { + pub first_num: f64, + pub operation: Operation, + pub second_num: f64, +} + +#[derive(Debug, Clone, Copy)] +pub enum Operation { + Addition, + Subtraction, + Multiplication, + Division, + Power, +} + +impl Expression { + pub fn eval(&self) -> f64 { + match self.operation { + Operation::Addition => self.first_num + self.second_num, + Operation::Subtraction => self.first_num - self.second_num, + Operation::Multiplication => self.first_num * self.second_num, + Operation::Division => self.first_num / self.second_num, + Operation::Power => self.first_num.powf(self.second_num), + } + } + + 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; + } + + let first_num: f64 = first_str.parse().ok()?; + let second_num: f64 = second_str.parse().ok()?; + + Some(Expression { + first_num, + operation, + second_num, + }) +} + +/// 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(); + } + + // 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(); + } + + if end == 0 { + return None; // nothing that looks like a number + } + + let (num, rest) = s.split_at(end); + Some((num, rest)) +} diff --git a/src/commands.rs b/src/commands.rs index 2d1e7ac..1a050ad 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -4,7 +4,7 @@ use arboard::Clipboard; use objc2_app_kit::NSWorkspace; use objc2_foundation::NSURL; -use crate::config::Config; +use crate::{calculator::Expression, config::Config}; #[derive(Debug, Clone)] pub enum Function { @@ -12,6 +12,7 @@ pub enum Function { RunShellCommand(String, String), RandomVar(i32), GoogleSearch(String), + Calculate(Expression), OpenPrefPane, Quit, } @@ -59,6 +60,13 @@ impl Function { }); } + Function::Calculate(expr) => { + Clipboard::new() + .unwrap() + .set_text(expr.eval().to_string()) + .unwrap_or(()); + } + Function::OpenPrefPane => { thread::spawn(move || { NSWorkspace::new().openURL(&NSURL::fileURLWithPath( diff --git a/src/main.rs b/src/main.rs index b3215d5..113fa11 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,5 @@ mod app; +mod calculator; mod commands; mod config; mod macos; From e2123590f0743bb49b3b60e9e372aafc17fc8013 Mon Sep 17 00:00:00 2001 From: unsecretised Date: Sat, 27 Dec 2025 17:37:43 +0800 Subject: [PATCH 2/3] Update feature list --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 335ae6d..35babbc 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,6 @@ bit wonky, and will be fixed in the upcoming releases ### Planned: - [ ] Select the options using arrow keys 13/12/2025 -- [ ] Calculator 15/12/2025 - [ ] Popup note-taking 18/12/2025 - [ ] Clipboard History 20/12/2025 - [ ] Plugin Support 31/12/2025 (Partially implemented on 15/12/2025) @@ -79,6 +78,7 @@ bit wonky, and will be fixed in the upcoming releases - [x] Allow variables to be passed into custom shell scripts. - [x] Google your query. Simply type your query, and then put a `?` at the end, and press enter +- [x] Calculator (27/12/2025) ### Not Possible by me: From d5787285ca85e54491d770539290aa4f49a8a5b3 Mon Sep 17 00:00:00 2001 From: unsecretised Date: Sat, 27 Dec 2025 17:38:07 +0800 Subject: [PATCH 3/3] Clippy --- src/app.rs | 20 ++++++++++---------- src/calculator.rs | 2 +- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/app.rs b/src/app.rs index 7818731..849abc8 100644 --- a/src/app.rs +++ b/src/app.rs @@ -265,16 +265,16 @@ impl Tile { self.handle_search_query_changed(); - if self.results.is_empty() { - if let Some(res) = Expression::from_str(&self.query) { - self.results.push(App { - open_command: Function::Calculate(res), - desc: RUSTCAST_DESC_NAME.to_string(), - icons: None, - name: res.eval().to_string(), - name_lc: "".to_string(), - }); - } + if self.results.is_empty() + && let Some(res) = Expression::from_str(&self.query) + { + self.results.push(App { + open_command: Function::Calculate(res), + desc: RUSTCAST_DESC_NAME.to_string(), + icons: None, + name: res.eval().to_string(), + name_lc: "".to_string(), + }); } let new_length = self.results.len(); diff --git a/src/calculator.rs b/src/calculator.rs index 13fe07d..b5255e4 100644 --- a/src/calculator.rs +++ b/src/calculator.rs @@ -26,7 +26,7 @@ impl Expression { } pub fn from_str(s: &str) -> Option { - parse_expression(&s) + parse_expression(s) } }