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
57 changes: 56 additions & 1 deletion src/app.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
//! Main logic for the app
use crate::calculator::Expression;
use crate::clipboard::ClipBoardContentType;
use crate::commands::Function;
Expand Down Expand Up @@ -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,
Expand All @@ -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<App> {
vec![
App {
Expand All @@ -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<iced::Element<'_, Message>> {
let mut tile = Row::new().width(Fill).height(55);

Expand Down Expand Up @@ -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,
Expand All @@ -140,6 +154,7 @@ pub enum Message {
_Nothing,
}

/// The window settings for rustcast
pub fn default_settings() -> Settings {
Settings {
resizable: false,
Expand All @@ -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<Retained<NSRunningApplication>>`]) 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,
Expand All @@ -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<Message>) {
let (id, open) = window::open(default_settings());

Expand Down Expand Up @@ -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<Message> {
match message {
Message::OpenWindow => {
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -456,10 +491,20 @@ impl Tile {
}
}

/// This returns the theme of the window
pub fn theme(&self, _: window::Id) -> Option<Theme> {
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<Message> {
Subscription::batch([
Subscription::run(handle_hotkeys),
Expand Down Expand Up @@ -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<App> = if self.query_lc.starts_with(&self.prev_query_lc) {
self.prev_query_lc = self.query_lc.to_owned();
Expand Down Expand Up @@ -526,13 +576,15 @@ impl Tile {
self.results = exact;
}

/// Gets the frontmost application to focus later.
pub fn capture_frontmost(&mut self) {
use objc2_app_kit::NSWorkspace;

let ws = NSWorkspace::sharedWorkspace();
self.frontmost = ws.frontmostApplication();
}

/// Restores the frontmost application.
#[allow(deprecated)]
pub fn restore_frontmost(&mut self) {
use objc2_app_kit::NSApplicationActivationOptions;
Expand All @@ -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<Item = Message> {
stream::channel(100, async |mut output| {
let content = fs::read_to_string(
Expand All @@ -563,6 +616,7 @@ fn handle_hot_reloading() -> impl futures::Stream<Item = Message> {
})
}

/// This is the subscription function that handles hotkeys for hiding / showing the window
fn handle_hotkeys() -> impl futures::Stream<Item = Message> {
stream::channel(100, async |mut output| {
let receiver = GlobalHotKeyEvent::receiver();
Expand All @@ -577,6 +631,7 @@ fn handle_hotkeys() -> impl futures::Stream<Item = Message> {
})
}

/// This is the subscription function that handles the change in clipboard history
fn handle_clipboard_history() -> impl futures::Stream<Item = Message> {
stream::channel(100, async |mut output| {
let mut clipboard = Clipboard::new().unwrap();
Expand Down
137 changes: 74 additions & 63 deletions src/calculator.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
//! 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,
pub operation: Operation,
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,
Expand All @@ -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,
Expand All @@ -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<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;
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<Expression> {
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))
}
}
4 changes: 4 additions & 0 deletions src/clipboard.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
//! This has all the logic regarding the cliboard history
use arboard::ImageData;
use iced::{
Length::Fill,
Expand All @@ -8,13 +9,15 @@ 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),
Image(ImageData<'static>),
}

impl ClipBoardContentType {
/// Returns the iced element for rendering the clipboard item
pub fn render_clipboard_item(&self) -> impl Into<iced::Element<'_, Message>> {
let mut tile = Row::new().width(Fill).height(55);

Expand Down Expand Up @@ -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
Expand Down
Loading