diff --git a/Cargo.lock b/Cargo.lock index 5c5dae7..30e02b2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -947,6 +947,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec" dependencies = [ "bitflags 2.11.0", + "block2 0.6.2", + "libc", "objc2 0.6.3", ] @@ -3386,6 +3388,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "pollster" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f3a9f18d041e6d0e102a0a46750538147e5e8992d3b4873aaafee2520b00ce3" + [[package]] name = "portable-atomic" version = "1.13.1" @@ -3743,6 +3751,33 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832" +[[package]] +name = "rfd" +version = "0.17.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20dafead71c16a34e1ff357ddefc8afc11e7d51d6d2b9fbd07eaa48e3e540220" +dependencies = [ + "block2 0.6.2", + "dispatch2", + "js-sys", + "libc", + "log", + "objc2 0.6.3", + "objc2-app-kit 0.3.2", + "objc2-core-foundation", + "objc2-foundation 0.3.2", + "percent-encoding", + "pollster", + "raw-window-handle", + "wasm-bindgen", + "wasm-bindgen-futures", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "web-sys", + "windows-sys 0.61.2", +] + [[package]] name = "rgb" version = "0.8.53" @@ -3813,6 +3848,7 @@ dependencies = [ "once_cell", "rand", "rayon", + "rfd", "serde", "serde_json", "tokio", diff --git a/Cargo.toml b/Cargo.toml index 4bcb2b0..65fbdbd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,6 +30,7 @@ objc2-foundation = { version = "0.3.2", features = ["NSString"] } once_cell = "1.21.3" rand = "0.9.2" rayon = "1.11.0" +rfd = "0.17.2" serde = { version = "1.0.228", features = ["derive"] } serde_json = "1.0.149" tokio = { version = "1.48.0", features = ["full"] } diff --git a/src/app.rs b/src/app.rs index 80eb31a..8c5600c 100644 --- a/src/app.rs +++ b/src/app.rs @@ -91,6 +91,7 @@ pub enum Message { RunFunction(Function), OpenFocused, SetConfig(SetConfigFields), + OpenFileDialogue(String), ReturnFocus, EscKeyPressed(Id), ClearSearchResults, @@ -111,6 +112,7 @@ pub enum Message { } #[derive(Debug, Clone)] +#[allow(unused)] pub enum SetConfigFields { ToDefault, ToggleHotkey(String), @@ -119,9 +121,9 @@ pub enum SetConfigFields { SearchUrl(String), HapticFeedback(bool), ShowMenubarIcon(bool), - // Modes(HashMap), - // Aliases(HashMap), - // SearchDirs(Vec), + Modes(Editable<(String, String)>), + Aliases(Editable<(String, String)>), + SearchDirs(Editable>), DebounceDelay(u64), SetThemeFields(SetConfigThemeFields), SetBufferFields(SetConfigBufferFields), diff --git a/src/app/pages/settings.rs b/src/app/pages/settings.rs index b2428f3..420410f 100644 --- a/src/app/pages/settings.rs +++ b/src/app/pages/settings.rs @@ -1,11 +1,17 @@ //! The settings page UI +use std::collections::HashMap; + use iced::widget::Slider; +use iced::widget::TextInput; use iced::widget::checkbox; use iced::widget::text_input; +use crate::app::Editable; use crate::app::SetConfigBufferFields; use crate::app::SetConfigThemeFields; +use crate::styles::delete_button_style; +use crate::styles::settings_add_button_style; use crate::styles::settings_checkbox_style; use crate::styles::settings_save_button_style; use crate::styles::settings_slider_style; @@ -351,6 +357,10 @@ pub fn settings_page(config: Config) -> Element<'static, Message> { font_family.into(), text_clr.into(), bg_clr.into(), + settings_hint_text(theme.clone(), "Aliases"), + aliases_item(config.aliases, &theme), + settings_hint_text(theme.clone(), "Rustcast modes"), + modes_item(config.modes, &theme), Row::from_iter([ savebutton(theme.clone()), default_button(theme.clone()), @@ -451,3 +461,138 @@ fn notice_item(theme: Theme, notice: impl ToString) -> Element<'static, Message> .align_x(Alignment::End) .into() } + +fn aliases_item(aliases: HashMap, theme: &Theme) -> Element<'static, Message> { + let theme_clone = theme.clone(); + let mut aliases = aliases + .iter() + .map(|(k, v)| (k.to_owned(), v.to_owned())) + .collect::>(); + aliases.sort_by_key(|x| x.0.len()); + Column::from_iter([ + container( + Column::from_iter(aliases.iter().map(|(key, value)| { + let key_clone = key.clone(); + let val_clone = value.clone(); + let key_clone_2 = key.clone(); + let val_clone_2 = value.clone(); + let theme_clone_2 = theme.clone(); + Row::from_iter([ + text_input_cell(key.to_owned(), &theme_clone, "Shorthand") + .on_input(move |input| { + Message::SetConfig(SetConfigFields::Aliases(Editable::Update { + old: (key_clone.clone(), val_clone.clone()), + new: (input.clone(), val_clone.clone()), + })) + }) + .into(), + text_input_cell(value.to_owned(), &theme_clone, "Term") + .on_input(move |input| { + Message::SetConfig(SetConfigFields::Aliases(Editable::Update { + old: (key_clone_2.clone(), val_clone_2.clone()), + new: (key_clone_2.clone(), input.clone()), + })) + }) + .into(), + Button::new("Delete") + .on_press(Message::SetConfig(SetConfigFields::Aliases( + Editable::Delete((key.clone(), value.clone())), + ))) + .style(move |_, _| delete_button_style(&theme_clone_2)) + .into(), + ]) + .spacing(10) + .into() + })) + .spacing(10), + ) + .height(Length::Fill) + .width(Length::Fill) + .into(), + Button::new( + Text::new("+") + .align_x(Alignment::Center) + .align_y(Alignment::Center), + ) + .style(move |_, _| settings_add_button_style(&theme_clone.clone())) + .on_press(Message::SetConfig(SetConfigFields::Aliases( + Editable::Create((String::new(), String::new())), + ))) + .into(), + ]) + .spacing(10) + .width(Length::Fill) + .align_x(Alignment::Center) + .into() +} + +fn text_input_cell(text: String, theme: &Theme, placeholder: &str) -> TextInput<'static, Message> { + text_input(placeholder, &text) + .font(theme.font()) + .padding(5) + .on_submit(Message::WriteConfig(false)) +} + +fn modes_item(modes: HashMap, theme: &Theme) -> Element<'static, Message> { + let theme_clone = theme.clone(); + let mut modes = modes + .iter() + .map(|(k, v)| (k.to_owned(), v.to_owned())) + .collect::>(); + modes.sort_by_key(|x| x.0.len()); + Column::from_iter([ + container( + Column::from_iter(modes.iter().map(|(key, value)| { + let theme_clone_1 = theme_clone.clone(); + let display_val = if value.is_empty() { + "Pick a file".to_string() + } else { + value.replace(&std::env::var("HOME").unwrap_or("".to_string()), "~") + }; + let key_clone = key.clone(); + let val_clone = value.clone(); + let theme_clone_2 = theme.clone(); + Row::from_iter([ + text_input_cell(key.to_owned(), &theme_clone, "Shorthand") + .on_input(move |input| { + Message::SetConfig(SetConfigFields::Modes(Editable::Update { + old: (key_clone.clone(), val_clone.clone()), + new: (input.clone(), val_clone.clone()), + })) + }) + .into(), + Button::new(Text::new(display_val)) + .on_press(Message::OpenFileDialogue(key.to_owned())) + .style(move |_, _| settings_add_button_style(&theme_clone_1.clone())) + .into(), + Button::new("Delete") + .on_press(Message::SetConfig(SetConfigFields::Modes( + Editable::Delete((key.clone(), value.clone())), + ))) + .style(move |_, _| delete_button_style(&theme_clone_2)) + .into(), + ]) + .spacing(10) + .into() + })) + .spacing(10), + ) + .height(Length::Fill) + .width(Length::Fill) + .into(), + Button::new( + Text::new("+") + .align_x(Alignment::Center) + .align_y(Alignment::Center), + ) + .on_press(Message::SetConfig(SetConfigFields::Modes( + Editable::Create((String::new(), String::new())), + ))) + .style(move |_, _| settings_add_button_style(&theme_clone.clone())) + .into(), + ]) + .spacing(10) + .width(Length::Fill) + .align_x(Alignment::Center) + .into() +} diff --git a/src/app/tile/update.rs b/src/app/tile/update.rs index f6e2177..46ab915 100644 --- a/src/app/tile/update.rs +++ b/src/app/tile/update.rs @@ -66,7 +66,7 @@ pub fn handle_update(tile: &mut Tile, message: Message) -> Task { } else { info!("Switching to default mode"); tile.current_mode = "default".to_string(); - window::latest().map(|x| Message::HideWindow(x.unwrap())) + Task::none() } } @@ -549,14 +549,69 @@ pub fn handle_update(tile: &mut Tile, message: Message) -> Task { } } + Message::OpenFileDialogue(mode_name) => rfd::FileDialog::new() + .add_filter("shell", &["sh", "bash", "zsh"]) + .set_directory( + std::env::var("HOME").unwrap_or("".to_string()) + "/.config/rustcast/config.toml", + ) + .pick_file() + .and_then(|x| { + x.to_str().map(|x| { + Task::batch([ + Task::done(Message::SetConfig(SetConfigFields::Modes( + Editable::Create((mode_name, x.to_string())), + ))), + Task::done(Message::WriteConfig(false)), + ]) + }) + }) + .unwrap_or(Task::none()), + Message::SetConfig(config) => { let mut final_config = tile.config.clone(); match config { SetConfigFields::ToggleHotkey(hk) => final_config.toggle_hotkey = hk, SetConfigFields::ClipboardHotkey(hk) => final_config.toggle_hotkey = hk, - // SetConfigFields::Modes(modes) => final_config.modes = modes, - // SetConfigFields::Aliases(aliases) => final_config.aliases = aliases, - // SetConfigFields::SearchDirs(dirs) => final_config.search_dirs = dirs, + SetConfigFields::Modes(Editable::Create((key, value))) => { + final_config.modes.entry(key).or_insert(value); + } + SetConfigFields::Modes(Editable::Delete((key, _))) => { + final_config.modes.remove(&key); + } + SetConfigFields::Modes(Editable::Update { old, new }) => { + final_config.modes.remove(&old.0); + final_config.modes.insert(new.0, new.1); + } + SetConfigFields::Aliases(Editable::Create((key, value))) => { + final_config.aliases.entry(key).or_insert(value); + } + SetConfigFields::Aliases(Editable::Delete((key, _))) => { + final_config.aliases.remove(&key); + } + SetConfigFields::Aliases(Editable::Update { old, new }) => { + final_config.aliases.remove(&old.0); + final_config.aliases.insert(new.0, new.1); + } + SetConfigFields::SearchDirs(Editable::Create(dir)) => { + final_config.search_dirs = dir + } + SetConfigFields::SearchDirs(Editable::Delete(dirs)) => { + final_config.search_dirs = final_config + .search_dirs + .iter() + .filter_map(|dir| { + if !dirs.contains(dir) { + Some(dir.to_owned()) + } else { + None + } + }) + .collect(); + } + SetConfigFields::SearchDirs(Editable::Update { old, new }) => { + let _ = old; + let _ = new; + } SetConfigFields::SearchUrl(url) => final_config.search_url = url, SetConfigFields::PlaceHolder(placeholder) => final_config.placeholder = placeholder, SetConfigFields::DebounceDelay(delay) => final_config.debounce_delay = delay, @@ -588,9 +643,7 @@ pub fn handle_update(tile: &mut Tile, message: Message) -> Task { SetConfigFields::ToDefault => { final_config = Config::default(); final_config.shells = tile.config.shells.clone(); - final_config.aliases = tile.config.aliases.clone(); final_config.search_dirs = tile.config.search_dirs.clone(); - final_config.modes = tile.config.modes.clone(); } }; diff --git a/src/styles.rs b/src/styles.rs index e11596d..6b5cfa3 100644 --- a/src/styles.rs +++ b/src/styles.rs @@ -198,6 +198,19 @@ pub fn settings_save_button_style(theme: &ConfigTheme) -> button::Style { } } +pub fn settings_add_button_style(theme: &ConfigTheme) -> button::Style { + button::Style { + background: None, + text_color: theme.text_color(1.), + border: Border { + color: theme.text_color(0.7), + width: 0.7, + radius: Radius::new(10), + }, + ..Default::default() + } +} + pub fn settings_checkbox_style(theme: &ConfigTheme) -> checkbox::Style { checkbox::Style { background: Background::Color(Color::TRANSPARENT),