diff --git a/Cargo.lock b/Cargo.lock index bd642dc..f62edeb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -111,6 +111,9 @@ name = "bitflags" version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" +dependencies = [ + "serde", +] [[package]] name = "bstr" @@ -221,6 +224,7 @@ dependencies = [ "itoa", "rustversion", "ryu", + "serde", "static_assertions", ] @@ -813,6 +817,7 @@ dependencies = [ "itertools", "lru", "paste", + "serde", "strum", "strum_macros", "unicode-segmentation", diff --git a/Cargo.toml b/Cargo.toml index 14c077d..8daedf8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,7 +29,7 @@ config = { version = "0.14", default-features = false, features = ["toml"] } toml = "0.8" dirs = "5.0.1" nucleo = "0.5.0" -ratatui = "0.28" +ratatui = { version = "0.28", features = ["serde"] } crossterm = "0.28" clap_complete = "4.5" diff --git a/src/cli.rs b/src/cli.rs index 7129123..f99302d 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -18,6 +18,7 @@ use clap::{Args, Command, CommandFactory, Parser, Subcommand}; use clap_complete::{generate, Generator, Shell}; use error_stack::ResultExt; use git2::{build::RepoBuilder, FetchOptions, RemoteCallbacks, Repository}; +use ratatui::style::Color; #[derive(Debug, Parser)] #[command(author, version)] @@ -89,19 +90,19 @@ pub struct ConfigCommand { max_depths: Option>, #[arg(long, value_name = "#rrggbb")] /// Background color of the highlighted item in the picker - picker_highlight_color: Option, + picker_highlight_color: Option, #[arg(long, value_name = "#rrggbb")] /// Text color of the hightlighted item in the picker - picker_highlight_text_color: Option, + picker_highlight_text_color: Option, #[arg(long, value_name = "#rrggbb")] /// Color of the borders between widgets in the picker - picker_border_color: Option, + picker_border_color: Option, #[arg(long, value_name = "#rrggbb")] /// Color of the item count in the picker - picker_info_color: Option, + picker_info_color: Option, #[arg(long, value_name = "#rrggbb")] /// Color of the prompt in the picker - picker_prompt_color: Option, + picker_prompt_color: Option, #[arg(long, value_name = "Alphabetical | LastAttached")] /// Set the sort order of the sessions in the switch command session_sort_order: Option, @@ -392,27 +393,27 @@ fn config_command(args: &ConfigCommand, mut config: Config) -> Result<()> { if let Some(color) = &args.picker_highlight_color { let mut picker_colors = config.picker_colors.unwrap_or_default(); - picker_colors.highlight_color = Some(color.to_string()); + picker_colors.highlight_color = Some(*color); config.picker_colors = Some(picker_colors); } if let Some(color) = &args.picker_highlight_text_color { let mut picker_colors = config.picker_colors.unwrap_or_default(); - picker_colors.highlight_text_color = Some(color.to_string()); + picker_colors.highlight_text_color = Some(*color); config.picker_colors = Some(picker_colors); } if let Some(color) = &args.picker_border_color { let mut picker_colors = config.picker_colors.unwrap_or_default(); - picker_colors.border_color = Some(color.to_string()); + picker_colors.border_color = Some(*color); config.picker_colors = Some(picker_colors); } if let Some(color) = &args.picker_info_color { let mut picker_colors = config.picker_colors.unwrap_or_default(); - picker_colors.info_color = Some(color.to_string()); + picker_colors.info_color = Some(*color); config.picker_colors = Some(picker_colors); } if let Some(color) = &args.picker_prompt_color { let mut picker_colors = config.picker_colors.unwrap_or_default(); - picker_colors.prompt_color = Some(color.to_string()); + picker_colors.prompt_color = Some(*color); config.picker_colors = Some(picker_colors); } diff --git a/src/configs.rs b/src/configs.rs index 7c76f2d..05c5676 100644 --- a/src/configs.rs +++ b/src/configs.rs @@ -3,7 +3,7 @@ use error_stack::ResultExt; use serde_derive::{Deserialize, Serialize}; use std::{collections::HashMap, env, fmt::Display, fs::canonicalize, io::Write, path::PathBuf}; -use ratatui::style::{Color, Style}; +use ratatui::style::{Color, Style, Stylize}; use crate::{error::Suggestion, keymap::Keymap}; @@ -142,8 +142,7 @@ impl Config { .change_context(ConfigError::IoError)? .to_string(); - let path = canonicalize(expanded_path) - .change_context(ConfigError::IoError)?; + let path = canonicalize(expanded_path).change_context(ConfigError::IoError)?; Ok(SearchDirectory::new(path, search_dir.depth)) }) @@ -255,70 +254,74 @@ pub struct Window { #[derive(Default, Debug, Serialize, Deserialize, PartialEq, Eq)] pub struct Pane {} -#[derive(Default, Debug, Serialize, Deserialize, PartialEq, Eq)] +#[derive(Clone, Default, Debug, Serialize, Deserialize, PartialEq, Eq)] pub struct PickerColorConfig { - pub highlight_color: Option, - pub highlight_text_color: Option, - pub border_color: Option, - pub info_color: Option, - pub prompt_color: Option, + pub highlight_color: Option, + pub highlight_text_color: Option, + pub border_color: Option, + pub info_color: Option, + pub prompt_color: Option, } +const HIGHLIGHT_COLOR_DEFAULT: Color = Color::LightBlue; +const HIGHLIGHT_TEXT_COLOR_DEFAULT: Color = Color::Black; +const BORDER_COLOR_DEFAULT: Color = Color::DarkGray; +const INFO_COLOR_DEFAULT: Color = Color::LightYellow; +const PROMPT_COLOR_DEFAULT: Color = Color::LightGreen; + impl PickerColorConfig { + pub fn default_colors() -> Self { + PickerColorConfig { + highlight_color: Some(HIGHLIGHT_COLOR_DEFAULT), + highlight_text_color: Some(HIGHLIGHT_TEXT_COLOR_DEFAULT), + border_color: Some(BORDER_COLOR_DEFAULT), + info_color: Some(INFO_COLOR_DEFAULT), + prompt_color: Some(PROMPT_COLOR_DEFAULT), + } + } + pub fn highlight_style(&self) -> Style { - let mut style = Style::default().bg(Color::LightBlue).fg(Color::Black); + let mut style = Style::default() + .bg(HIGHLIGHT_COLOR_DEFAULT) + .fg(HIGHLIGHT_TEXT_COLOR_DEFAULT) + .bold(); - if let Some(color) = &self.highlight_color { - if let Some(color) = rgb_to_color(color) { - style = style.bg(color); - } + if let Some(color) = self.highlight_color { + style = style.bg(color); } - if let Some(color) = &self.highlight_text_color { - if let Some(color) = rgb_to_color(color) { - style = style.fg(color); - } + if let Some(color) = self.highlight_text_color { + style = style.fg(color); } style } - pub fn border_color(&self) -> Option { - if let Some(color) = &self.border_color { - rgb_to_color(color) + pub fn border_color(&self) -> Color { + if let Some(color) = self.border_color { + color } else { - None + BORDER_COLOR_DEFAULT } } - pub fn info_color(&self) -> Option { - if let Some(color) = &self.info_color { - rgb_to_color(color) + pub fn info_color(&self) -> Color { + if let Some(color) = self.info_color { + color } else { - None + INFO_COLOR_DEFAULT } } - pub fn prompt_color(&self) -> Option { - if let Some(color) = &self.prompt_color { - rgb_to_color(color) + pub fn prompt_color(&self) -> Color { + if let Some(color) = self.prompt_color { + color } else { - None + PROMPT_COLOR_DEFAULT } } } -fn rgb_to_color(color: &str) -> Option { - if color.len() == 7 && color.starts_with('#') { - let red = u8::from_str_radix(&color[1..3], 16).ok()?; - let green = u8::from_str_radix(&color[3..5], 16).ok()?; - let blue = u8::from_str_radix(&color[5..7], 16).ok()?; - Some(Color::Rgb(red, green, blue)) - } else { - None - } -} - #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] pub enum SessionSortOrderConfig { Alphabetical, diff --git a/src/keymap.rs b/src/keymap.rs index 4068c8a..f11d83e 100644 --- a/src/keymap.rs +++ b/src/keymap.rs @@ -4,7 +4,6 @@ use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; use serde::de::Error as DeError; use serde::ser::Error as SerError; use serde::{Deserialize, Serialize}; -use serde_derive::{Deserialize, Serialize}; use crate::error::TmsError; diff --git a/src/picker.rs b/src/picker.rs index 3ede331..88fd5ad 100644 --- a/src/picker.rs +++ b/src/picker.rs @@ -13,7 +13,7 @@ use crossterm::{ }; use nucleo::{ pattern::{CaseMatching, Normalization}, - Nucleo, Snapshot, + Nucleo, }; use ratatui::{ backend::CrosstermBackend, @@ -116,6 +116,8 @@ impl<'a> Picker<'a> { terminal: &mut Terminal>, ) -> Result> { loop { + self.matcher.tick(10); + self.update_selection(); terminal .draw(|f| self.render(f)) .map_err(|e| TmsError::TuiError(e.to_string()))?; @@ -126,7 +128,7 @@ impl<'a> Picker<'a> { Some(PickerAction::Cancel) => return Ok(None), Some(PickerAction::Confirm) => { if let Some(selected) = self.get_selected() { - return Ok(Some(selected)); + return Ok(Some(selected.to_owned())); } } Some(PickerAction::Backspace) => self.remove_filter(), @@ -152,6 +154,20 @@ impl<'a> Picker<'a> { } } + fn update_selection(&mut self) { + let snapshot = self.matcher.snapshot(); + if let Some(selected) = self.selection.selected() { + if snapshot.matched_item_count() == 0 { + self.selection.select(None); + } else if selected > snapshot.matched_item_count() as usize { + self.selection + .select(Some(snapshot.matched_item_count() as usize - 1)); + } + } else if snapshot.matched_item_count() > 0 { + self.selection.select(Some(0)); + } + } + fn render(&mut self, f: &mut Frame) { let preview_direction; let picker_pane; @@ -188,57 +204,27 @@ impl<'a> Picker<'a> { ) .split(preview_split[picker_pane]); - self.matcher.tick(10); let snapshot = self.matcher.snapshot(); let matches = snapshot .matched_items(..snapshot.matched_item_count()) .map(|item| ListItem::new(item.data.as_str())); - if let Some(selected) = self.selection.selected() { - if snapshot.matched_item_count() == 0 { - self.selection.select(None); - } else if selected > snapshot.matched_item_count() as usize { - self.selection - .select(Some(snapshot.matched_item_count() as usize - 1)); - } - } else if snapshot.matched_item_count() > 0 { - self.selection.select(Some(0)); - } - - let mut selected_style = Style::default() - .bg(Color::LightBlue) - .fg(Color::Black) - .bold(); - let mut border_color = Color::DarkGray; - let mut info_color = Color::LightYellow; - let mut prompt_color = Color::LightGreen; - - if let Some(colors) = &self.colors { - selected_style = colors.highlight_style().bold(); - - if let Some(color) = colors.border_color() { - border_color = color; - } - - if let Some(color) = colors.info_color() { - info_color = color; - } - - if let Some(color) = colors.prompt_color() { - prompt_color = color; - } - } + let colors = if let Some(colors) = self.colors { + colors.to_owned() + } else { + PickerColorConfig::default_colors() + }; let table = List::new(matches) - .highlight_style(selected_style) + .highlight_style(colors.highlight_style()) .direction(ListDirection::BottomToTop) .highlight_spacing(HighlightSpacing::Always) .highlight_symbol("> ") .block( Block::default() .borders(Borders::BOTTOM) - .border_style(Style::default().fg(border_color)) - .title_style(Style::default().fg(info_color)) + .border_style(Style::default().fg(colors.border_color())) + .title_style(Style::default().fg(colors.info_color())) .title_position(Position::Bottom) .title(format!( "{}/{}", @@ -248,7 +234,7 @@ impl<'a> Picker<'a> { ); f.render_stateful_widget(table, layout[0], &mut self.selection); - let prompt = Span::styled("> ", Style::default().fg(prompt_color)); + let prompt = Span::styled("> ", Style::default().fg(colors.prompt_color())); let input_text = Span::raw(&self.filter); let input_line = Line::from(vec![prompt, input_text]); let input = Paragraph::new(vec![input_line]); @@ -260,9 +246,8 @@ impl<'a> Picker<'a> { if !matches!(self.preview, Preview::None) { self.render_preview( - snapshot, f, - &border_color, + &colors.border_color(), &preview_direction, preview_split[preview_pane], ); @@ -271,36 +256,31 @@ impl<'a> Picker<'a> { fn render_preview( &self, - snapshot: &Snapshot, f: &mut Frame, border_color: &Color, direction: &Direction, rect: Rect, ) { - let text = if let Some(index) = self.selection.selected() { - if let Some(item) = snapshot.get_matched_item(index as u32) { - let output = match self.preview { - Preview::SessionPane => self.tmux.capture_pane(item.data), - Preview::WindowPane => self.tmux.capture_pane( - item.data - .split_once(' ') - .map(|val| val.0) - .unwrap_or_default(), - ), - Preview::Directory => process::Command::new("ls") - .args(["-1", item.data]) - .output() - .unwrap_or_else(|_| { - panic!("Failed to execute the command for directory: {}", item.data) - }), - Preview::None => panic!("preview rendering should not have occured"), - }; - - if output.status.success() { - String::from_utf8(output.stdout).unwrap() - } else { - "".to_string() - } + let text = if let Some(item_data) = self.get_selected() { + let output = match self.preview { + Preview::SessionPane => self.tmux.capture_pane(item_data), + Preview::WindowPane => self.tmux.capture_pane( + item_data + .split_once(' ') + .map(|val| val.0) + .unwrap_or_default(), + ), + Preview::Directory => process::Command::new("ls") + .args(["-1", item_data]) + .output() + .unwrap_or_else(|_| { + panic!("Failed to execute the command for directory: {}", item_data) + }), + Preview::None => panic!("preview rendering should not have occured"), + }; + + if output.status.success() { + String::from_utf8(output.stdout).unwrap() } else { "".to_string() } @@ -323,13 +303,13 @@ impl<'a> Picker<'a> { f.render_widget(preview, rect); } - fn get_selected(&self) -> Option { + fn get_selected(&self) -> Option<&String> { if let Some(index) = self.selection.selected() { return self .matcher .snapshot() .get_matched_item(index as u32) - .map(|item| item.data.to_owned()); + .map(|item| item.data); } None diff --git a/tests/cli.rs b/tests/cli.rs index 28680e6..fde8dd7 100644 --- a/tests/cli.rs +++ b/tests/cli.rs @@ -1,6 +1,7 @@ use assert_cmd::Command; use pretty_assertions::assert_eq; -use std::fs; +use ratatui::style::Color; +use std::{fs, str::FromStr}; use tempfile::tempdir; use tms::configs::{Config, PickerColorConfig, SearchDirectory, SessionSortOrderConfig}; @@ -32,11 +33,11 @@ fn tms_config() -> anyhow::Result<()> { let depth = 1; let default_session = String::from("my_default_session"); let excluded_dir = String::from("/exclude/this/directory"); - let picker_highlight_color = String::from("#aaaaaa"); - let picker_highlight_text_color = String::from("#bbbbbb"); - let picker_border_color = String::from("#cccccc"); - let picker_info_color = String::from("#dddddd"); - let picker_prompt_color = String::from("#eeeeee"); + let picker_highlight_color = Color::from_str("#aaaaaa")?; + let picker_highlight_text_color = Color::from_str("#bbbbbb")?; + let picker_border_color = Color::from_str("#cccccc")?; + let picker_info_color = Color::from_str("green")?; + let picker_prompt_color = Color::from_str("#eeeeee")?; let expected_config = Config { default_session: Some(default_session.clone()), @@ -53,11 +54,11 @@ fn tms_config() -> anyhow::Result<()> { )]), sessions: None, picker_colors: Some(PickerColorConfig { - highlight_color: Some(picker_highlight_color.clone()), - highlight_text_color: Some(picker_highlight_text_color.clone()), - border_color: Some(picker_border_color.clone()), - info_color: Some(picker_info_color.clone()), - prompt_color: Some(picker_prompt_color.clone()), + highlight_color: Some(picker_highlight_color), + highlight_text_color: Some(picker_highlight_text_color), + border_color: Some(picker_border_color), + info_color: Some(picker_info_color), + prompt_color: Some(picker_prompt_color), }), shortcuts: None, bookmarks: None, @@ -88,15 +89,15 @@ fn tms_config() -> anyhow::Result<()> { "--excluded", &excluded_dir, "--picker-highlight-color", - &picker_highlight_color, + &picker_highlight_color.to_string(), "--picker-highlight-text-color", - &picker_highlight_text_color, + &picker_highlight_text_color.to_string(), "--picker-border-color", - &picker_border_color, + &picker_border_color.to_string(), "--picker-info-color", - &picker_info_color, + &picker_info_color.to_string(), "--picker-prompt-color", - &picker_prompt_color, + &picker_prompt_color.to_string(), ]); tms.assert().success().code(0);