Skip to content

Commit e0de7e1

Browse files
committed
Refactor code to be few LoC per file
1 parent 7a64e31 commit e0de7e1

8 files changed

Lines changed: 672 additions & 612 deletions

File tree

src/app.rs

Lines changed: 3 additions & 607 deletions
Large diffs are not rendered by default.

src/app/apps.rs

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
use iced::{
2+
Alignment, Background,
3+
Length::Fill,
4+
alignment::Vertical,
5+
widget::{Button, Row, Text, container, image::Viewer, space},
6+
};
7+
8+
use crate::{
9+
app::{Message, RUSTCAST_DESC_NAME},
10+
commands::Function,
11+
};
12+
13+
/// The main app struct, that represents an "App"
14+
///
15+
/// This struct represents a command that rustcast can perform, providing the rustcast
16+
/// the data needed to search for the app, to display the app in search results, and to actually
17+
/// "run" the app.
18+
#[derive(Debug, Clone)]
19+
pub struct App {
20+
pub open_command: Function,
21+
pub desc: String,
22+
pub icons: Option<iced::widget::image::Handle>,
23+
pub name: String,
24+
pub name_lc: String,
25+
}
26+
27+
impl App {
28+
/// This returns the basic apps that rustcast has, such as quiting rustcast and opening preferences
29+
pub fn basic_apps() -> Vec<App> {
30+
vec![
31+
App {
32+
open_command: Function::Quit,
33+
desc: RUSTCAST_DESC_NAME.to_string(),
34+
icons: None,
35+
name: "Quit RustCast".to_string(),
36+
name_lc: "quit".to_string(),
37+
},
38+
App {
39+
open_command: Function::OpenPrefPane,
40+
desc: RUSTCAST_DESC_NAME.to_string(),
41+
icons: None,
42+
name: "Open RustCast Preferences".to_string(),
43+
name_lc: "settings".to_string(),
44+
},
45+
]
46+
}
47+
48+
/// This renders the app into an iced element, allowing it to be displayed in the search results
49+
pub fn render<'a>(
50+
&'a self,
51+
theme: &'a crate::config::Theme,
52+
) -> impl Into<iced::Element<'a, Message>> {
53+
let mut tile = Row::new().width(Fill).height(55);
54+
55+
if theme.show_icons {
56+
if let Some(icon) = &self.icons {
57+
tile = tile
58+
.push(Viewer::new(icon).height(35).width(35))
59+
.align_y(Alignment::Center);
60+
} else {
61+
tile = tile
62+
.push(space().height(Fill))
63+
.width(55)
64+
.height(55)
65+
.align_y(Alignment::Center);
66+
}
67+
}
68+
69+
tile = tile.push(
70+
Button::new(
71+
Text::new(&self.name)
72+
.height(Fill)
73+
.width(Fill)
74+
.color(theme.text_color(1.))
75+
.align_y(Vertical::Center),
76+
)
77+
.on_press(Message::RunFunction(self.open_command.clone()))
78+
.style(|_, _| iced::widget::button::Style {
79+
background: Some(Background::Color(theme.bg_color())),
80+
text_color: theme.text_color(1.),
81+
..Default::default()
82+
})
83+
.width(Fill)
84+
.height(55),
85+
);
86+
87+
tile = tile
88+
.push(container(Text::new(&self.desc).color(theme.text_color(0.4))).padding(15))
89+
.width(Fill);
90+
91+
container(tile)
92+
.style(|_| iced::widget::container::Style {
93+
text_color: Some(theme.text_color(1.)),
94+
background: Some(Background::Color(theme.bg_color())),
95+
..Default::default()
96+
})
97+
.width(Fill)
98+
.height(Fill)
99+
}
100+
}

src/app/tile.rs

Lines changed: 247 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,247 @@
1+
//! This module handles the logic for the tile, AKA rustcast's main window
2+
mod elm;
3+
mod update;
4+
5+
use crate::app::apps::App;
6+
use crate::app::{Message, Page};
7+
use crate::clipboard::ClipBoardContentType;
8+
use crate::commands::Function;
9+
use crate::config::Config;
10+
11+
use arboard::Clipboard;
12+
use global_hotkey::{GlobalHotKeyEvent, HotKeyState};
13+
14+
use iced::futures::SinkExt;
15+
use iced::window;
16+
use iced::{
17+
Element, Subscription, Task, Theme, futures,
18+
keyboard::{self, key::Named},
19+
stream,
20+
};
21+
22+
use objc2::rc::Retained;
23+
use objc2_app_kit::NSRunningApplication;
24+
use rayon::iter::{IntoParallelRefIterator, ParallelIterator};
25+
26+
use std::fs;
27+
use std::time::Duration;
28+
29+
/// This is the base window, and its a "Tile"
30+
/// Its fields are:
31+
/// - Theme ([`iced::Theme`])
32+
/// - Query (String)
33+
/// - Query Lowercase (String, but lowercase)
34+
/// - Previous Query Lowercase (String)
35+
/// - Results (Vec<[`App`]>) the results of the search
36+
/// - Options (Vec<[`App`]>) the options to search through
37+
/// - Visible (bool) whether the window is visible or not
38+
/// - Focused (bool) whether the window is focused or not
39+
/// - Frontmost ([`Option<Retained<NSRunningApplication>>`]) the frontmost application before the window was opened
40+
/// - Config ([`Config`]) the app's config
41+
/// - Open Hotkey ID (`u32`) the id of the hotkey that opens the window
42+
/// - Clipboard Content (`Vec<`[`ClipBoardContentType`]`>`) all of the cliboard contents
43+
/// - Page ([`Page`]) the current page of the window (main or clipboard history)
44+
#[derive(Debug, Clone)]
45+
pub struct Tile {
46+
theme: iced::Theme,
47+
query: String,
48+
query_lc: String,
49+
prev_query_lc: String,
50+
results: Vec<App>,
51+
options: Vec<App>,
52+
visible: bool,
53+
focused: bool,
54+
frontmost: Option<Retained<NSRunningApplication>>,
55+
config: Config,
56+
open_hotkey_id: u32,
57+
clipboard_content: Vec<ClipBoardContentType>,
58+
page: Page,
59+
}
60+
61+
impl Tile {
62+
/// Initialise the base window
63+
pub fn new(keybind_id: u32, config: &Config) -> (Self, Task<Message>) {
64+
elm::new(keybind_id, config)
65+
}
66+
67+
/// This handles the iced's updates, which have all the variants of [Message]
68+
pub fn update(&mut self, message: Message) -> Task<Message> {
69+
update::handle_update(self, message)
70+
}
71+
72+
/// This is the view of the window. It handles the rendering of the window
73+
///
74+
/// The rendering of the window size (the resizing of the window) is handled by the
75+
/// [`Tile::update`] function.
76+
pub fn view(&self, wid: window::Id) -> Element<'_, Message> {
77+
elm::view(self, wid)
78+
}
79+
80+
/// This returns the theme of the window
81+
pub fn theme(&self, _: window::Id) -> Option<Theme> {
82+
Some(self.theme.clone())
83+
}
84+
85+
/// This handles the subscriptions of the window
86+
///
87+
/// The subscriptions are:
88+
/// - Hotkeys
89+
/// - Hot reloading
90+
/// - Clipboard history
91+
/// - Window close events
92+
/// - Keypresses (escape to close the window)
93+
/// - Window focus changes
94+
pub fn subscription(&self) -> Subscription<Message> {
95+
Subscription::batch([
96+
Subscription::run(handle_hotkeys),
97+
Subscription::run(handle_hot_reloading),
98+
Subscription::run(handle_clipboard_history),
99+
window::close_events().map(Message::HideWindow),
100+
keyboard::listen().filter_map(|event| {
101+
if let keyboard::Event::KeyPressed { key, .. } = event {
102+
match key {
103+
keyboard::Key::Named(Named::Escape) => Some(Message::KeyPressed(65598)),
104+
_ => None,
105+
}
106+
} else {
107+
None
108+
}
109+
}),
110+
window::events()
111+
.with(self.focused)
112+
.filter_map(|(focused, (wid, event))| match event {
113+
window::Event::Unfocused => {
114+
if focused {
115+
Some(Message::WindowFocusChanged(wid, false))
116+
} else {
117+
None
118+
}
119+
}
120+
window::Event::Focused => Some(Message::WindowFocusChanged(wid, true)),
121+
_ => None,
122+
}),
123+
])
124+
}
125+
126+
/// Handles the search query changed event.
127+
///
128+
/// This is separate from the `update` function because it has a decent amount of logic, and
129+
/// should be separated out to make it easier to test. This function is called by the `update`
130+
/// function to handle the search query changed event.
131+
pub fn handle_search_query_changed(&mut self) {
132+
let filter_vec: &Vec<App> = if self.query_lc.starts_with(&self.prev_query_lc) {
133+
self.prev_query_lc = self.query_lc.to_owned();
134+
&self.results
135+
} else {
136+
&self.options
137+
};
138+
139+
let query = self.query_lc.clone();
140+
141+
let mut exact: Vec<App> = filter_vec
142+
.par_iter()
143+
.filter(|x| match &x.open_command {
144+
Function::RunShellCommand(_, _) => x
145+
.name_lc
146+
.starts_with(query.split_once(" ").unwrap_or((&query, "")).0),
147+
_ => x.name_lc == query,
148+
})
149+
.cloned()
150+
.collect();
151+
152+
let mut prefix: Vec<App> = filter_vec
153+
.par_iter()
154+
.filter(|x| match x.open_command {
155+
Function::RunShellCommand(_, _) => false,
156+
_ => x.name_lc != query && x.name_lc.starts_with(&query),
157+
})
158+
.cloned()
159+
.collect();
160+
161+
exact.append(&mut prefix);
162+
self.results = exact;
163+
}
164+
165+
/// Gets the frontmost application to focus later.
166+
pub fn capture_frontmost(&mut self) {
167+
use objc2_app_kit::NSWorkspace;
168+
169+
let ws = NSWorkspace::sharedWorkspace();
170+
self.frontmost = ws.frontmostApplication();
171+
}
172+
173+
/// Restores the frontmost application.
174+
#[allow(deprecated)]
175+
pub fn restore_frontmost(&mut self) {
176+
use objc2_app_kit::NSApplicationActivationOptions;
177+
178+
if let Some(app) = self.frontmost.take() {
179+
app.activateWithOptions(NSApplicationActivationOptions::ActivateIgnoringOtherApps);
180+
}
181+
}
182+
}
183+
184+
/// This is the subscription function that handles hot reloading of the config
185+
fn handle_hot_reloading() -> impl futures::Stream<Item = Message> {
186+
stream::channel(100, async |mut output| {
187+
let content = fs::read_to_string(
188+
std::env::var("HOME").unwrap_or("".to_owned()) + "/.config/rustcast/config.toml",
189+
)
190+
.unwrap_or("".to_string());
191+
loop {
192+
let current_content = fs::read_to_string(
193+
std::env::var("HOME").unwrap_or("".to_owned()) + "/.config/rustcast/config.toml",
194+
)
195+
.unwrap_or("".to_string());
196+
197+
if current_content != content {
198+
output.send(Message::ReloadConfig).await.unwrap();
199+
}
200+
tokio::time::sleep(Duration::from_millis(10)).await;
201+
}
202+
})
203+
}
204+
205+
/// This is the subscription function that handles hotkeys for hiding / showing the window
206+
fn handle_hotkeys() -> impl futures::Stream<Item = Message> {
207+
stream::channel(100, async |mut output| {
208+
let receiver = GlobalHotKeyEvent::receiver();
209+
loop {
210+
if let Ok(event) = receiver.recv()
211+
&& event.state == HotKeyState::Pressed
212+
{
213+
output.try_send(Message::KeyPressed(event.id)).unwrap();
214+
}
215+
tokio::time::sleep(Duration::from_millis(10)).await;
216+
}
217+
})
218+
}
219+
220+
/// This is the subscription function that handles the change in clipboard history
221+
fn handle_clipboard_history() -> impl futures::Stream<Item = Message> {
222+
stream::channel(100, async |mut output| {
223+
let mut clipboard = Clipboard::new().unwrap();
224+
let mut prev_byte_rep: Option<ClipBoardContentType> = None;
225+
226+
loop {
227+
let byte_rep = if let Ok(a) = clipboard.get_image() {
228+
Some(ClipBoardContentType::Image(a))
229+
} else if let Ok(a) = clipboard.get_text() {
230+
Some(ClipBoardContentType::Text(a))
231+
} else {
232+
None
233+
};
234+
235+
if byte_rep != prev_byte_rep
236+
&& let Some(content) = &byte_rep
237+
{
238+
output
239+
.send(Message::ClipboardHistory(content.to_owned()))
240+
.await
241+
.ok();
242+
prev_byte_rep = byte_rep;
243+
}
244+
tokio::time::sleep(Duration::from_millis(10)).await;
245+
}
246+
})
247+
}

0 commit comments

Comments
 (0)