diff --git a/src/app.rs b/src/app.rs index 16c7cab..49ce678 100644 --- a/src/app.rs +++ b/src/app.rs @@ -3,7 +3,7 @@ use std::collections::HashMap; use crate::app::apps::{App, AppCommand, ICNS_ICON}; use crate::commands::Function; -use crate::config::Config; +use crate::config::{Config, Shelly}; use crate::debounce::DebouncePolicy; use crate::utils::icns_data_to_handle; use crate::{app::tile::ExtSender, clipboard::ClipBoardContentType}; @@ -128,7 +128,8 @@ pub enum SetConfigFields { AutoSuggest(bool), Modes(Editable<(String, String)>), Aliases(Editable<(String, String)>), - SearchDirs(Editable>), + SearchDirs(Editable), + ShellCommands(Editable), DebounceDelay(u64), SetThemeFields(SetConfigThemeFields), SetBufferFields(SetConfigBufferFields), diff --git a/src/app/pages/settings.rs b/src/app/pages/settings.rs index 5186d54..a2dd755 100644 --- a/src/app/pages/settings.rs +++ b/src/app/pages/settings.rs @@ -3,6 +3,7 @@ use std::collections::HashMap; use iced::widget::Slider; +use iced::widget::Space; use iced::widget::TextInput; use iced::widget::checkbox; use iced::widget::text_input; @@ -10,6 +11,8 @@ use iced::widget::text_input; use crate::app::Editable; use crate::app::SetConfigBufferFields; use crate::app::SetConfigThemeFields; +use crate::commands::Function; +use crate::config::Shelly; use crate::styles::delete_button_style; use crate::styles::settings_add_button_style; use crate::styles::settings_checkbox_style; @@ -204,16 +207,19 @@ pub fn settings_page(config: Config) -> Element<'static, Message> { let theme_clone = theme.clone(); let font_family = settings_item_column([ settings_hint_text(theme.clone(), "Set Font family"), - text_input("Font family", &config.theme.font.unwrap_or("".to_string())) - .on_input(move |input: String| { - Message::SetConfig(SetConfigFields::SetThemeFields(SetConfigThemeFields::Font( - input, - ))) - }) - .on_submit(Message::WriteConfig(false)) - .width(Length::Fill) - .style(move |_, _| settings_text_input_item_style(&theme_clone)) - .into(), + text_input( + "Font family", + &config.theme.font.clone().unwrap_or("".to_string()), + ) + .on_input(move |input: String| { + Message::SetConfig(SetConfigFields::SetThemeFields(SetConfigThemeFields::Font( + input, + ))) + }) + .on_submit(Message::WriteConfig(false)) + .width(Length::Fill) + .style(move |_, _| settings_text_input_item_style(&theme_clone)) + .into(), notice_item(theme.clone(), "What font rustcast should use"), ]); @@ -372,12 +378,18 @@ pub fn settings_page(config: Config) -> Element<'static, Message> { text_clr.into(), bg_clr.into(), settings_hint_text(theme.clone(), "Aliases"), - aliases_item(config.aliases, &theme), + aliases_item(config.aliases.clone(), &theme), settings_hint_text(theme.clone(), "Modes"), - modes_item(config.modes, &theme), + modes_item(config.modes.clone(), &theme), + settings_hint_text(theme.clone(), "Search Directories"), + search_dirs_item(&theme, config.search_dirs.clone()), + Space::new().height(30).into(), + settings_hint_text(theme.clone(), "Shell commands"), + shell_commands_item(config.shells.clone(), theme.clone()), Row::from_iter([ savebutton(theme.clone()), default_button(theme.clone()), + copy_config_button(config), wiki_button(theme.clone()), ]) .spacing(5) @@ -423,18 +435,34 @@ fn default_button(theme: Theme) -> Element<'static, Message> { fn wiki_button(theme: Theme) -> Element<'static, Message> { Button::new( - Text::new("Open the wiki") + Text::new("Open file") .align_x(Alignment::Center) .width(Length::Fill) .font(theme.font()), ) .style(move |_, _| settings_save_button_style(&theme)) .width(Length::Fill) - .on_press(Message::RunFunction( - crate::commands::Function::OpenWebsite( - "https://github.com/RustCastLabs/rustcast/wiki".to_string(), + .on_press(Message::RunFunction(crate::commands::Function::OpenApp( + std::env::var("HOME").unwrap_or("".to_string()) + "/.config/rustcast/config.toml", + ))) + .into() +} + +fn copy_config_button(config: Box) -> Element<'static, Message> { + let theme = config.theme.clone(); + Button::new( + Text::new("Copy config") + .align_x(Alignment::Center) + .width(Length::Fill) + .font(theme.font()), + ) + .style(move |_, _| settings_save_button_style(&theme)) + .width(Length::Fill) + .on_press(Message::RunFunction(Function::CopyToClipboard( + crate::clipboard::ClipBoardContentType::Text( + toml::to_string(&config).unwrap_or("".to_string()), ), - )) + ))) .into() } @@ -540,6 +568,47 @@ fn aliases_item(aliases: HashMap, theme: &Theme) -> Element<'sta .into() } +fn search_dirs_item(theme: &Theme, search_dirs: Vec) -> Element<'static, Message> { + let theme_clone = theme.clone(); + let search_dirs = search_dirs.clone(); + Column::from_iter([ + container( + Column::from_iter(search_dirs.iter().map(|dir| { + let theme_clone_2 = theme.clone(); + let directory = dir.clone(); + container( + Row::from_iter([ + dir_picker_button(directory, dir, theme_clone.clone()).into(), + Button::new("Delete") + .on_press(Message::SetConfig(SetConfigFields::SearchDirs( + Editable::Delete(dir.clone()), + ))) + .style(move |_, _| delete_button_style(&theme_clone_2)) + .into(), + ]) + .spacing(10) + .align_y(Alignment::Center), + ) + .width(Length::Fill) + .align_x(Alignment::Center) + .into() + })) + .spacing(10), + ) + .height(Length::Fill) + .width(Length::Fill) + .align_x(Alignment::Center) + .align_y(Alignment::Center) + .into(), + dir_adder_button("+", theme.to_owned()).into(), + ]) + .spacing(10) + .height(Length::Fill) + .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()) @@ -610,3 +679,212 @@ fn modes_item(modes: HashMap, theme: &Theme) -> Element<'static, .align_x(Alignment::Center) .into() } + +fn dir_picker_button(directory: String, dir: &str, theme: Theme) -> Button<'static, Message> { + let home = std::env::var("HOME").unwrap_or("/".to_string()); + Button::new(Text::new(dir.to_owned().replace(&home, "~"))) + .on_press_with(move || { + rfd::FileDialog::new() + .set_directory(home.clone()) + .set_can_create_directories(false) + .pick_folder() + .map(|path| { + let new = path.to_str().unwrap_or("").to_string(); + Message::SetConfig(SetConfigFields::SearchDirs(Editable::Update { + old: directory.clone(), + new, + })) + }) + .unwrap_or(Message::SetConfig(SetConfigFields::SearchDirs( + Editable::Update { + old: directory.clone(), + new: directory.clone(), + }, + ))) + }) + .style(move |_, _| settings_add_button_style(&theme.clone())) +} + +fn dir_adder_button(dir: &str, theme: Theme) -> Button<'static, Message> { + Button::new(Text::new(dir.to_owned())) + .on_press_with(move || { + rfd::FileDialog::new() + .set_directory(std::env::var("HOME").unwrap_or("/".to_string())) + .set_can_create_directories(false) + .pick_folder() + .map(|path| { + let new = path.to_str().unwrap_or("").to_string(); + Message::SetConfig(SetConfigFields::SearchDirs(Editable::Create(new))) + }) + .unwrap_or(Message::SetConfig(SetConfigFields::SearchDirs( + Editable::Create(String::new()), + ))) + }) + .style(move |_, _| settings_add_button_style(&theme.clone())) +} + +fn shell_commands_item(shells: Vec, theme: Theme) -> Element<'static, Message> { + let mut col = + Column::from_iter(shells.iter().map(|x| x.editable_render(theme.clone()))).spacing(30); + + let theme_clone = theme.clone(); + + col = col + .push( + 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::ShellCommands( + Editable::Create(Shelly::default()), + ))), + ) + .width(Length::Fill) + .align_x(Alignment::Center); + + col.into() +} + +impl Shelly { + pub fn editable_render(&self, theme: Theme) -> Element<'static, Message> { + let shell = self.to_owned(); + Column::from_iter([ + tuple_row( + shellcommand_hint_text(theme.clone(), "Display name"), + text_input_cell(self.alias.clone(), &theme, "Display Name") + .on_input({ + let shell = shell.clone(); + move |input| { + let old = shell.clone(); + let mut new = old.clone(); + new.alias = input; + Message::SetConfig(SetConfigFields::ShellCommands(Editable::Update { + old, + new, + })) + } + }) + .into(), + ) + .into(), + tuple_row( + shellcommand_hint_text(theme.clone(), "Search name"), + text_input_cell(self.alias_lc.clone(), &theme, "Search Name") + .on_input({ + let shell = shell.clone(); + move |input| { + let old = shell.clone(); + let mut new = old.clone(); + new.alias_lc = input; + Message::SetConfig(SetConfigFields::ShellCommands(Editable::Update { + old, + new, + })) + } + }) + .into(), + ) + .into(), + tuple_row( + shellcommand_hint_text(theme.clone(), "Command"), + text_input_cell(self.command.clone(), &theme, "Command") + .on_input({ + let shell = shell.clone(); + move |input| { + let old = shell.clone(); + let mut new = old.clone(); + new.command = input; + Message::SetConfig(SetConfigFields::ShellCommands(Editable::Update { + old, + new, + })) + } + }) + .into(), + ) + .into(), + tuple_row( + shellcommand_hint_text(theme.clone(), "Icon File"), + text_input_cell( + self.icon_path.clone().unwrap_or("".to_string()), + &theme, + "Icon path", + ) + .on_input({ + let shell = shell.clone(); + move |input| { + let old = shell.clone(); + let mut new = old.clone(); + new.icon_path = if input.is_empty() { None } else { Some(input) }; + Message::SetConfig(SetConfigFields::ShellCommands(Editable::Update { + old, + new, + })) + } + }) + .into(), + ) + .into(), + tuple_row( + shellcommand_hint_text(theme.clone(), "Hotkey"), + text_input_cell( + self.hotkey.clone().unwrap_or("".to_string()), + &theme, + "Hotkey", + ) + .on_input({ + let shell = shell.clone(); + move |input| { + let old = shell.clone(); + let mut new = old.clone(); + new.hotkey = Some(input); + Message::SetConfig(SetConfigFields::ShellCommands(Editable::Update { + old, + new, + })) + } + }) + .into(), + ) + .into(), + tuple_row( + Button::new("Delete") + .on_press(Message::SetConfig(SetConfigFields::ShellCommands( + Editable::Delete(self.clone()), + ))) + .style({ + let theme = theme.clone(); + move |_, _| delete_button_style(&theme) + }) + .into(), + notice_item(theme.clone(), "Icon path and hotkey are optional"), + ) + .into(), + ]) + .spacing(10) + .height(Length::Fill) + .width(Length::Fill) + .into() + } +} + +fn tuple_row( + left: Element<'static, Message>, + right: Element<'static, Message>, +) -> Row<'static, Message> { + Row::from_iter([left, right]) + .spacing(10) + .width(Length::Fill) +} + +fn shellcommand_hint_text(theme: Theme, text: impl ToString) -> Element<'static, Message> { + let text = text.to_string(); + + Text::new(text) + .font(theme.font()) + .color(theme.text_color(0.7)) + .width(WINDOW_WIDTH * 0.3) + .into() +} diff --git a/src/app/tile/elm.rs b/src/app/tile/elm.rs index 5fc386d..17e9534 100644 --- a/src/app/tile/elm.rs +++ b/src/app/tile/elm.rs @@ -189,7 +189,7 @@ pub fn view(tile: &Tile, wid: window::Id) -> Element<'_, Message> { .height(height as u32); let text = if tile.query_lc.is_empty() { - if tile.page == Page::Main { + if tile.config.auto_suggest && tile.page == Page::Main { "Frequently used".to_string() } else { tile.page.to_string() diff --git a/src/app/tile/update.rs b/src/app/tile/update.rs index a5c1ace..ef73cec 100644 --- a/src/app/tile/update.rs +++ b/src/app/tile/update.rs @@ -261,6 +261,13 @@ pub fn handle_update(tile: &mut Tile, message: Message) -> Task { Err(_) => return Task::none(), }; + let update_apps_task = if tile.config.shells != new_config.shells { + info!("App Update required"); + Task::done(Message::UpdateApps) + } else { + Task::none() + }; + if let Some(icon) = tile.tray_icon.as_mut() { icon.set_visible(new_config.clone().show_trayicon) .unwrap_or(()); @@ -280,7 +287,7 @@ pub fn handle_update(tile: &mut Tile, message: Message) -> Task { tile.theme = new_config.theme.to_owned().into(); tile.config = new_config; - Task::done(Message::LoadRanking) + Task::batch([Task::done(Message::LoadRanking), update_apps_task]) } Message::KeyPressed(hk_id) => { @@ -367,6 +374,10 @@ pub fn handle_update(tile: &mut Tile, message: Message) -> Task { Message::RunFunction(command) => { command.execute(&tile.config); + let page_task = match tile.page { + Page::Settings => Task::done(Message::SwitchToPage(Page::Main)), + _ => Task::none(), + }; let return_focus_task = match &command { Function::OpenApp(_) | Function::GoogleSearch(_) => Task::none(), @@ -380,6 +391,7 @@ pub fn handle_update(tile: &mut Tile, message: Message) -> Task { window::latest() .map(|x| x.unwrap()) .map(Message::HideWindow) + .chain(page_task) .chain(Task::done(Message::ClearSearchQuery)) .chain(return_focus_task) } @@ -599,14 +611,16 @@ pub fn handle_update(tile: &mut Tile, message: Message) -> Task { final_config.aliases.insert(new.0, new.1); } SetConfigFields::SearchDirs(Editable::Create(dir)) => { - final_config.search_dirs = dir + if !final_config.search_dirs.contains(&dir) { + final_config.search_dirs.push(dir); + } } SetConfigFields::SearchDirs(Editable::Delete(dirs)) => { final_config.search_dirs = final_config .search_dirs .iter() .filter_map(|dir| { - if !dirs.contains(dir) { + if &dirs != dir { Some(dir.to_owned()) } else { None @@ -615,9 +629,53 @@ pub fn handle_update(tile: &mut Tile, message: Message) -> Task { .collect(); } SetConfigFields::SearchDirs(Editable::Update { old, new }) => { - let _ = old; - let _ = new; + final_config.search_dirs = final_config + .search_dirs + .iter() + .map(|dir| { + if dir == &old { + new.clone() + } else { + dir.to_owned() + } + }) + .collect(); + } + + SetConfigFields::ShellCommands(Editable::Create(shell_command)) => { + if !final_config.shells.contains(&shell_command) { + final_config.shells.push(shell_command); + } } + + SetConfigFields::ShellCommands(Editable::Delete(shell_command)) => { + final_config.shells = final_config + .shells + .iter() + .filter_map(|shell| { + if &shell_command != shell { + Some(shell.to_owned()) + } else { + None + } + }) + .collect(); + } + + SetConfigFields::ShellCommands(Editable::Update { old, new }) => { + final_config.shells = final_config + .shells + .iter() + .map(|shell| { + if shell == &old { + new.clone() + } else { + shell.to_owned() + } + }) + .collect(); + } + SetConfigFields::SearchUrl(url) => final_config.search_url = url, SetConfigFields::PlaceHolder(placeholder) => final_config.placeholder = placeholder, SetConfigFields::AutoSuggest(status) => final_config.auto_suggest = status, @@ -649,8 +707,6 @@ 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.search_dirs = tile.config.search_dirs.clone(); } }; diff --git a/src/config.rs b/src/config.rs index eb1fc05..dc93230 100644 --- a/src/config.rs +++ b/src/config.rs @@ -178,7 +178,7 @@ impl Default for Buffer { /// Command is the command it will run when the button is clicked /// Icon_path is the path to an icon, but this is optional /// Alias is the text that is used to call this command / search for it -#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)] +#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Default)] pub struct Shelly { pub command: String, pub icon_path: Option,