Skip to content

Commit 906068e

Browse files
authored
Merge pull request #49 from unsecretised/calculator
Calculator
2 parents b0e3e67 + d578728 commit 906068e

5 files changed

Lines changed: 128 additions & 2 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,6 @@ bit wonky, and will be fixed in the upcoming releases
5555
### Planned:
5656

5757
- [ ] Select the options using arrow keys 13/12/2025
58-
- [ ] Calculator 15/12/2025
5958
- [ ] Popup note-taking 18/12/2025
6059
- [ ] Clipboard History 20/12/2025
6160
- [ ] 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
7978
- [x] Allow variables to be passed into custom shell scripts.
8079
- [x] Google your query. Simply type your query, and then put a `?` at the end,
8180
and press enter
81+
- [x] Calculator (27/12/2025)
8282

8383
### Not Possible by me:
8484

src/app.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use crate::calculator::Expression;
12
use crate::commands::Function;
23
use crate::config::Config;
34
use crate::macos::{focus_this_app, transform_process_to_ui_element};
@@ -263,9 +264,22 @@ impl Tile {
263264
}
264265

265266
self.handle_search_query_changed();
267+
268+
if self.results.is_empty()
269+
&& let Some(res) = Expression::from_str(&self.query)
270+
{
271+
self.results.push(App {
272+
open_command: Function::Calculate(res),
273+
desc: RUSTCAST_DESC_NAME.to_string(),
274+
icons: None,
275+
name: res.eval().to_string(),
276+
name_lc: "".to_string(),
277+
});
278+
}
266279
let new_length = self.results.len();
267280

268281
let max_elem = min(5, new_length);
282+
269283
if prev_size != new_length {
270284
window::resize(
271285
id,

src/calculator.rs

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
#[derive(Debug, Clone, Copy)]
2+
pub struct Expression {
3+
pub first_num: f64,
4+
pub operation: Operation,
5+
pub second_num: f64,
6+
}
7+
8+
#[derive(Debug, Clone, Copy)]
9+
pub enum Operation {
10+
Addition,
11+
Subtraction,
12+
Multiplication,
13+
Division,
14+
Power,
15+
}
16+
17+
impl Expression {
18+
pub fn eval(&self) -> f64 {
19+
match self.operation {
20+
Operation::Addition => self.first_num + self.second_num,
21+
Operation::Subtraction => self.first_num - self.second_num,
22+
Operation::Multiplication => self.first_num * self.second_num,
23+
Operation::Division => self.first_num / self.second_num,
24+
Operation::Power => self.first_num.powf(self.second_num),
25+
}
26+
}
27+
28+
pub fn from_str(s: &str) -> Option<Expression> {
29+
parse_expression(s)
30+
}
31+
}
32+
33+
fn parse_expression(s: &str) -> Option<Expression> {
34+
let s = s.trim();
35+
36+
// 1. Parse first (possibly signed) number with manual scan
37+
let (first_str, rest) = parse_signed_number_prefix(s)?;
38+
39+
// 2. Next non‑whitespace char must be the binary operator
40+
let rest = rest.trim_start();
41+
let (op_char, rest) = rest.chars().next().map(|c| (c, &rest[c.len_utf8()..]))?;
42+
43+
let operation = match op_char {
44+
'+' => Operation::Addition,
45+
'-' => Operation::Subtraction,
46+
'*' => Operation::Multiplication,
47+
'/' => Operation::Division,
48+
'^' => Operation::Power,
49+
_ => return None,
50+
};
51+
52+
// 3. The remainder should be the second (possibly signed) number
53+
let rest = rest.trim_start();
54+
let (second_str, tail) = parse_signed_number_prefix(rest)?;
55+
// Optionally ensure nothing but whitespace after second number:
56+
if !tail.trim().is_empty() {
57+
return None;
58+
}
59+
60+
let first_num: f64 = first_str.parse().ok()?;
61+
let second_num: f64 = second_str.parse().ok()?;
62+
63+
Some(Expression {
64+
first_num,
65+
operation,
66+
second_num,
67+
})
68+
}
69+
70+
/// Returns (number_lexeme, remaining_slice) for a leading signed float.
71+
/// Very simple: `[+|-]?` + "anything until we hit whitespace or an operator".
72+
fn parse_signed_number_prefix(s: &str) -> Option<(&str, &str)> {
73+
let s = s.trim_start();
74+
if s.is_empty() {
75+
return None;
76+
}
77+
78+
let mut chars = s.char_indices().peekable();
79+
80+
// Optional leading sign
81+
if let Some((_, c)) = chars.peek()
82+
&& (*c == '+' || *c == '-')
83+
{
84+
chars.next();
85+
}
86+
87+
// Now consume until we hit an operator or whitespace
88+
let mut end = 0;
89+
while let Some((idx, c)) = chars.peek().cloned() {
90+
if c.is_whitespace() || "+-*/^".contains(c) {
91+
break;
92+
}
93+
end = idx + c.len_utf8();
94+
chars.next();
95+
}
96+
97+
if end == 0 {
98+
return None; // nothing that looks like a number
99+
}
100+
101+
let (num, rest) = s.split_at(end);
102+
Some((num, rest))
103+
}

src/commands.rs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,15 @@ use arboard::Clipboard;
44
use objc2_app_kit::NSWorkspace;
55
use objc2_foundation::NSURL;
66

7-
use crate::config::Config;
7+
use crate::{calculator::Expression, config::Config};
88

99
#[derive(Debug, Clone)]
1010
pub enum Function {
1111
OpenApp(String),
1212
RunShellCommand(String, String),
1313
RandomVar(i32),
1414
GoogleSearch(String),
15+
Calculate(Expression),
1516
OpenPrefPane,
1617
Quit,
1718
}
@@ -59,6 +60,13 @@ impl Function {
5960
});
6061
}
6162

63+
Function::Calculate(expr) => {
64+
Clipboard::new()
65+
.unwrap()
66+
.set_text(expr.eval().to_string())
67+
.unwrap_or(());
68+
}
69+
6270
Function::OpenPrefPane => {
6371
thread::spawn(move || {
6472
NSWorkspace::new().openURL(&NSURL::fileURLWithPath(

src/main.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
mod app;
2+
mod calculator;
23
mod commands;
34
mod config;
45
mod macos;

0 commit comments

Comments
 (0)