Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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:

Expand Down
14 changes: 14 additions & 0 deletions src/app.rs
Original file line number Diff line number Diff line change
@@ -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};
Expand Down Expand Up @@ -263,9 +264,22 @@ impl Tile {
}

self.handle_search_query_changed();

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();

let max_elem = min(5, new_length);

if prev_size != new_length {
window::resize(
id,
Expand Down
103 changes: 103 additions & 0 deletions src/calculator.rs
Original file line number Diff line number Diff line change
@@ -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<Expression> {
parse_expression(s)
}
}

fn parse_expression(s: &str) -> Option<Expression> {
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))
}
10 changes: 9 additions & 1 deletion src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,15 @@ 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 {
OpenApp(String),
RunShellCommand(String, String),
RandomVar(i32),
GoogleSearch(String),
Calculate(Expression),
OpenPrefPane,
Quit,
}
Expand Down Expand Up @@ -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(
Expand Down
1 change: 1 addition & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
mod app;
mod calculator;
mod commands;
mod config;
mod macos;
Expand Down