diff --git a/Cargo.lock b/Cargo.lock index 2028174..460d149 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -177,9 +177,9 @@ checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "chrono" -version = "0.4.43" +version = "0.4.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" dependencies = [ "iana-time-zone", "num-traits", @@ -866,9 +866,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.85" +version = "0.3.90" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3" +checksum = "14dc6f6450b3f6d4ed5b16327f38fed626d375a886159ca555bd7822c0c3a5a6" dependencies = [ "once_cell", "wasm-bindgen", @@ -898,9 +898,9 @@ dependencies = [ [[package]] name = "linux-raw-sys" -version = "0.11.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" [[package]] name = "litrs" @@ -1169,16 +1169,16 @@ dependencies = [ [[package]] name = "promkit-widgets" -version = "0.2.0" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7ef81079760b198d5dde773c78b94a72edc2ebd057be386382c379e0d854fb6" +checksum = "b665ae2f09cfe22290386f73e642922980cb6a5edd83188989dcb1e914dc4a7b" dependencies = [ "anyhow", "promkit-core", "rayon", "serde", "serde_json", - "tokio", + "termcfg", ] [[package]] @@ -1271,9 +1271,9 @@ dependencies = [ [[package]] name = "rustix" -version = "1.1.3" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ "bitflags", "errno", @@ -1437,9 +1437,9 @@ dependencies = [ [[package]] name = "termcfg" -version = "0.1.0" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b8c057661e9a795bc9cb18a7d2924b00d0aa3c5a9e61dfe7de32cea8a1d8fe7" +checksum = "6e444ec0de07571e26f31f6ac5006c03540b61d35bec105031e82c6d6cbb5fab" dependencies = [ "crossterm", "serde", @@ -1633,9 +1633,9 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasm-bindgen" -version = "0.2.108" +version = "0.2.113" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566" +checksum = "60722a937f594b7fde9adb894d7c092fc1bb6612897c46368d18e7a20208eff2" dependencies = [ "cfg-if", "once_cell", @@ -1646,9 +1646,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.108" +version = "0.2.113" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608" +checksum = "0fac8c6395094b6b91c4af293f4c79371c163f9a6f56184d2c9a85f5a95f3950" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -1656,9 +1656,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.108" +version = "0.2.113" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55" +checksum = "ab3fabce6159dc20728033842636887e4877688ae94382766e00b180abac9d60" dependencies = [ "bumpalo", "proc-macro2", @@ -1669,9 +1669,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.108" +version = "0.2.113" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12" +checksum = "de0e091bdb824da87dc01d967388880d017a0a9bc4f3bdc0d86ee9f9336e3bb5" dependencies = [ "unicode-ident", ] diff --git a/Cargo.toml b/Cargo.toml index 1111a6d..eef4861 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,9 +17,8 @@ duration-string = { version = "0.5.3", features = ["serde"] } derive_builder = "0.20.2" dirs = "6.0.0" futures = "0.3.32" -promkit-widgets = { version = "0.2.0", features = ["jsonstream", "listbox", "text", "texteditor"] } serde = "1.0.228" -termcfg = { version = "0.1.0", features = ["crossterm_0_29_0"] } +termcfg = { version = "0.2.0", features = ["crossterm_0_29_0"] } tokio = { version = "1.49.0", features = ["full"] } tokio-stream = "0.1.18" toml = "0.9.8" @@ -29,6 +28,11 @@ jaq-core = "2.2.1" jaq-json = { version = "1.1.3", features = ["serde_json"] } jaq-std = "2.1.2" +[dependencies.promkit-widgets] +version = "0.3.0" +features = ["jsonstream", "listbox", "serde", "text", "texteditor"] +default-features = false + # The profile that 'cargo dist' will build with [profile.dist] inherits = "release" diff --git a/README.md b/README.md index 3b9301a..bea138c 100644 --- a/README.md +++ b/README.md @@ -208,48 +208,61 @@ it will be automatically created on first run. > [!IMPORTANT] > The syntax in TOML configurations -> like [default.toml](./default.toml) was revamped in v0.8.0, +> like [default.toml](./default.toml) was revamped in v0.7.0, > and the configuration shown below reflects the new format. > A migration tool is not provided for this change. > Please manually replace/update your local > `config.toml` to match the new syntax. -The following settings are available in `config.toml`: +> [!WARNING] +> Depending on the type of terminal and environment, +> characters and styles may not be displayed properly. +> Specific key bindings and decorative characters may not +> display or function correctly in certain terminal emulators. + +
+The following settings are available in config.toml ```toml # Whether to hide hint messages no_hint = false # Editor settings -[editor] +# Uses promkit_widgets::text_editor::Config directly +[editor.on_focus] + # Editor mode # "Insert": Insert characters at the cursor position # "Overwrite": Replace characters at the cursor position with new ones -mode = "Insert" +edit_mode = "Insert" # Characters considered as word boundaries # These are used to define word movement and deletion behavior in the editor word_break_chars = [".", "|", "(", ")", "[", "]"] -# How to configure colors and text attributes -# -# Color specification methods: -# 1. By name: "black", "red", etc. -# 2. By RGB value: "rgb_(255,0,0)" or "#ff0000" -# 3. By ANSI value: "ansi_(16)" +# Style notation (termcfg) +# Format: "fg=,bg=,ul=,attr=" +# Examples: +# - "fg=blue" +# - "fg=#00FF00,bg=black,attr=bold|underlined" +# - "attr=dim" # -# Text attribute specification: -# attributes = ["Bold"], etc. +# Color tokens: +# - reset, black, red, green, yellow, blue, magenta, cyan, white +# - darkgrey, darkred, darkgreen, darkyellow, darkblue, darkmagenta, darkcyan, grey +# - #RRGGBB # -# Configuration example: -# style = { foreground = "blue", background = "magenta", attributes = ["Bold"] } +# Attribute tokens (examples): +# - bold, italic, underlined, dim, reverse, crossedout, nounderline, nobold # -# Detailed information: -# - Color: https://docs.rs/crossterm/0.28.1/crossterm/style/enum.Color.html -# - Attribute: https://docs.rs/crossterm/0.28.1/crossterm/style/enum.Attribute.html +# Notes: +# - ANSI 256-color index tokens (0..255, e.g. "200") are currently out of notation scope. +# - See termcfg notation reference for full token list. +# +# References: +# - https://github.com/ynqa/termcfg/blob/main/Notations.md +# - https://github.com/ynqa/termcfg -# Theme settings when the editor is focused -[editor.theme_on_focus] # Prefix shown before the cursor prefix = "❯❯ " # Style for the prefix @@ -260,7 +273,7 @@ active_char_style = "bg=magenta" inactive_char_style = "" # Theme settings when the editor is unfocused -[editor.theme_on_defocus] +[editor.on_defocus] # Prefix shown when focus is lost prefix = "▼ " # Style for the prefix when unfocused @@ -277,8 +290,9 @@ inactive_char_style = "attr=dim" # No limit if unset # max_streams = -# JSON display theme -[json.theme] +# JSON display settings +# Uses promkit_widgets::jsonstream::Config directly +[json.stream] # Number of spaces to use for indentation indent = 2 # Style for curly brackets {} @@ -295,18 +309,17 @@ number_value_style = "" boolean_value_style = "" # Style for null values null_value_style = "fg=grey" +# Attribute for the selected row and unselected rows +active_item_attribute = "bold" +# Attribute for unselected rows +inactive_item_attribute = "dim" +# Behavior when JSON content exceeds the available width +# "Wrap": Wrap content to the next line +# "Truncate": Truncate content with an ellipsis (...) +overflow_mode = "Wrap" # Completion feature settings [completion] -# Number of lines to display for completion candidates -lines = 3 -# Cursor character shown before the selected candidate -cursor = "❯ " -# Style for the selected candidate -active_item_style = "fg=grey,bg=yellow" -# Style for unselected candidates -inactive_item_style = "fg=grey" - # Settings for background loading of completion candidates # # Number of candidates loaded per chunk for search results @@ -317,6 +330,18 @@ search_result_chunk_size = 100 # A larger value finishes loading sooner but uses more memory temporarily search_load_chunk_size = 50000 +# Completion UI settings +# Uses promkit_widgets::listbox::Config directly +[completion.listbox] +# Number of lines to display for completion candidates +lines = 3 +# Cursor character shown before the selected candidate +cursor = "❯ " +# Style for the selected candidate +active_item_style = "fg=grey,bg=yellow" +# Style for unselected candidates +inactive_item_style = "fg=grey" + # Keybinding settings [keybinds] # Key to exit the application @@ -393,13 +418,8 @@ resize_debounce_duration = "200ms" spin_duration = "300ms" ``` -For more details on configuration, please refer to [default.toml](./default.toml) - -> [!WARNING] -> Depending on the type of terminal and environment, -> characters and styles may not be displayed properly. -> Specific key bindings and decorative characters may not -> display or function correctly in certain terminal emulators. +
## Stargazers over time + [![Stargazers over time](https://starchart.cc/ynqa/jnv.svg?variant=adaptive)](https://starchart.cc/ynqa/jnv) diff --git a/default.toml b/default.toml index 514838a..05f892e 100644 --- a/default.toml +++ b/default.toml @@ -2,35 +2,41 @@ no_hint = false # Editor settings -[editor] +# Uses promkit_widgets::text_editor::Config directly +[editor.on_focus] + # Editor mode # "Insert": Insert characters at the cursor position # "Overwrite": Replace characters at the cursor position with new ones -mode = "Insert" +edit_mode = "Insert" # Characters considered as word boundaries # These are used to define word movement and deletion behavior in the editor word_break_chars = [".", "|", "(", ")", "[", "]"] -# How to configure colors and text attributes -# -# Color specification methods: -# 1. By name: "black", "red", etc. -# 2. By RGB value: "rgb_(255,0,0)" or "#ff0000" -# 3. By ANSI value: "ansi_(16)" +# Style notation (termcfg) +# Format: "fg=,bg=,ul=,attr=" +# Examples: +# - "fg=blue" +# - "fg=#00FF00,bg=black,attr=bold|underlined" +# - "attr=dim" +# +# Color tokens: +# - reset, black, red, green, yellow, blue, magenta, cyan, white +# - darkgrey, darkred, darkgreen, darkyellow, darkblue, darkmagenta, darkcyan, grey +# - #RRGGBB # -# Text attribute specification: -# attributes = ["Bold"], etc. +# Attribute tokens (examples): +# - bold, italic, underlined, dim, reverse, crossedout, nounderline, nobold # -# Configuration example: -# style = { foreground = "blue", background = "magenta", attributes = ["Bold"] } +# Notes: +# - ANSI 256-color index tokens (0..255, e.g. "200") are currently out of notation scope. +# - See termcfg notation reference for full token list. # -# Detailed information: -# - Color: https://docs.rs/crossterm/0.28.1/crossterm/style/enum.Color.html -# - Attribute: https://docs.rs/crossterm/0.28.1/crossterm/style/enum.Attribute.html +# References: +# - https://github.com/ynqa/termcfg/blob/main/Notations.md +# - https://github.com/ynqa/termcfg -# Theme settings when the editor is focused -[editor.theme_on_focus] # Prefix shown before the cursor prefix = "❯❯ " # Style for the prefix @@ -41,7 +47,7 @@ active_char_style = "bg=magenta" inactive_char_style = "" # Theme settings when the editor is unfocused -[editor.theme_on_defocus] +[editor.on_defocus] # Prefix shown when focus is lost prefix = "▼ " # Style for the prefix when unfocused @@ -58,8 +64,9 @@ inactive_char_style = "attr=dim" # No limit if unset # max_streams = -# JSON display theme -[json.theme] +# JSON display settings +# Uses promkit_widgets::jsonstream::Config directly +[json.stream] # Number of spaces to use for indentation indent = 2 # Style for curly brackets {} @@ -76,18 +83,17 @@ number_value_style = "" boolean_value_style = "" # Style for null values null_value_style = "fg=grey" +# Attribute for the selected row and unselected rows +active_item_attribute = "bold" +# Attribute for unselected rows +inactive_item_attribute = "dim" +# Behavior when JSON content exceeds the available width +# "Wrap": Wrap content to the next line +# "Truncate": Truncate content with an ellipsis (...) +overflow_mode = "Wrap" # Completion feature settings [completion] -# Number of lines to display for completion candidates -lines = 3 -# Cursor character shown before the selected candidate -cursor = "❯ " -# Style for the selected candidate -active_item_style = "fg=grey,bg=yellow" -# Style for unselected candidates -inactive_item_style = "fg=grey" - # Settings for background loading of completion candidates # # Number of candidates loaded per chunk for search results @@ -98,6 +104,18 @@ search_result_chunk_size = 100 # A larger value finishes loading sooner but uses more memory temporarily search_load_chunk_size = 50000 +# Completion UI settings +# Uses promkit_widgets::listbox::Config directly +[completion.listbox] +# Number of lines to display for completion candidates +lines = 3 +# Cursor character shown before the selected candidate +cursor = "❯ " +# Style for the selected candidate +active_item_style = "fg=grey,bg=yellow" +# Style for unselected candidates +inactive_item_style = "fg=grey" + # Keybinding settings [keybinds] # Key to exit the application @@ -145,9 +163,9 @@ on_completion.down = ["Down", "Tab"] # Keybindings for JSON viewer operations [keybinds.on_json_viewer] # Move up in JSON viewer -up = ["Up", "Ctrl+K"] +up = ["Up", "Ctrl+K", "ScrollUp"] # Move down in JSON viewer -down = ["Down", "Ctrl+J"] +down = ["Down", "Ctrl+J", "ScrollDown"] # Move to the top of JSON viewer move_to_head = ["Ctrl+L"] # Move to the bottom of JSON viewer diff --git a/src/config.rs b/src/config.rs index 172cc7c..fb8ee63 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,85 +1,30 @@ use std::collections::HashSet; -use promkit_widgets::{ - core::crossterm::{event::Event, style::ContentStyle}, - text_editor::Mode, -}; +use promkit_widgets::{core::crossterm::event::Event, jsonstream, listbox, text_editor}; use serde::{Deserialize, Serialize}; -use termcfg::crossterm_config::{content_style_serde, event_set_serde}; +use termcfg::crossterm_config::event_set_serde; use tokio::time::Duration; mod duration; use duration::duration_serde; -mod text_editor; -use text_editor::text_editor_mode_serde; #[derive(Serialize, Deserialize)] pub struct EditorConfig { - pub theme_on_focus: EditorTheme, - pub theme_on_defocus: EditorTheme, - #[serde(with = "text_editor_mode_serde")] - pub mode: Mode, - pub word_break_chars: HashSet, -} - -#[derive(Serialize, Deserialize)] -pub struct EditorTheme { - pub prefix: String, - - #[serde(with = "content_style_serde")] - pub prefix_style: ContentStyle, - - #[serde(with = "content_style_serde")] - pub active_char_style: ContentStyle, - - #[serde(with = "content_style_serde")] - pub inactive_char_style: ContentStyle, + pub on_focus: text_editor::Config, + pub on_defocus: text_editor::Config, } #[derive(Serialize, Deserialize)] pub struct JsonConfig { pub max_streams: Option, - pub theme: JsonTheme, -} - -#[derive(Serialize, Deserialize)] -pub struct JsonTheme { - pub indent: usize, - - #[serde(with = "content_style_serde")] - pub curly_brackets_style: ContentStyle, - - #[serde(with = "content_style_serde")] - pub square_brackets_style: ContentStyle, - - #[serde(with = "content_style_serde")] - pub key_style: ContentStyle, - - #[serde(with = "content_style_serde")] - pub string_value_style: ContentStyle, - - #[serde(with = "content_style_serde")] - pub number_value_style: ContentStyle, - - #[serde(with = "content_style_serde")] - pub boolean_value_style: ContentStyle, - - #[serde(with = "content_style_serde")] - pub null_value_style: ContentStyle, + pub stream: jsonstream::Config, } #[derive(Serialize, Deserialize)] pub struct CompletionConfig { - pub lines: Option, - pub cursor: String, + pub listbox: listbox::Config, pub search_result_chunk_size: usize, pub search_load_chunk_size: usize, - - #[serde(with = "content_style_serde")] - pub active_item_style: ContentStyle, - - #[serde(with = "content_style_serde")] - pub inactive_item_style: ContentStyle, } // TODO: remove Clone derive diff --git a/src/config/text_editor.rs b/src/config/text_editor.rs deleted file mode 100644 index ff39cb0..0000000 --- a/src/config/text_editor.rs +++ /dev/null @@ -1,33 +0,0 @@ -use promkit_widgets::text_editor::Mode; -use serde::{Deserialize, Deserializer, Serialize, Serializer}; - -pub mod text_editor_mode_serde { - use super::*; - - pub fn serialize(mode: &Mode, serializer: S) -> Result - where - S: Serializer, - { - let mode_str = match mode { - Mode::Insert => "Insert", - Mode::Overwrite => "Overwrite", - // Add other variants if they exist - }; - mode_str.serialize(serializer) - } - - pub fn deserialize<'de, D>(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - let mode_str = String::deserialize(deserializer)?; - match mode_str.as_str() { - "Insert" => Ok(Mode::Insert), - "Overwrite" => Ok(Mode::Overwrite), - // Add other variants if they exist - _ => Err(serde::de::Error::custom(format!( - "Unknown Mode variant: {mode_str}", - ))), - } - } -} diff --git a/src/editor.rs b/src/editor.rs index 25a93be..792f3a7 100644 --- a/src/editor.rs +++ b/src/editor.rs @@ -12,16 +12,13 @@ use promkit_widgets::{ text_editor, }; -use crate::{ - config::{EditorKeybinds, EditorTheme}, - search::IncrementalSearcher, -}; +use crate::{config::EditorKeybinds, search::IncrementalSearcher}; pub struct Editor { handler: Handler, state: text_editor::State, - focus_theme: EditorTheme, - defocus_theme: EditorTheme, + focus_config: text_editor::Config, + defocus_config: text_editor::Config, guide: text::State, searcher: IncrementalSearcher, editor_keybinds: EditorKeybinds, @@ -31,15 +28,15 @@ impl Editor { pub fn new( state: text_editor::State, searcher: IncrementalSearcher, - focus_theme: EditorTheme, - defocus_theme: EditorTheme, + focus_config: text_editor::Config, + defocus_config: text_editor::Config, editor_keybinds: EditorKeybinds, ) -> Self { Self { handler: BOXED_EDITOR_HANDLER, state, - focus_theme, - defocus_theme, + focus_config, + defocus_config, guide: text::State::default(), searcher, editor_keybinds, @@ -47,17 +44,11 @@ impl Editor { } pub fn focus(&mut self) { - self.state.prefix = self.focus_theme.prefix.clone(); - self.state.prefix_style = self.focus_theme.prefix_style; - self.state.inactive_char_style = self.focus_theme.inactive_char_style; - self.state.active_char_style = self.focus_theme.active_char_style; + self.state.config = self.focus_config.clone(); } pub fn defocus(&mut self) { - self.state.prefix = self.defocus_theme.prefix.clone(); - self.state.prefix_style = self.defocus_theme.prefix_style; - self.state.inactive_char_style = self.defocus_theme.inactive_char_style; - self.state.active_char_style = self.defocus_theme.active_char_style; + self.state.config = self.defocus_config.clone(); self.searcher.leave_search(); self.handler = BOXED_EDITOR_HANDLER; @@ -114,19 +105,19 @@ pub async fn edit<'a>(event: &'a Event, editor: &'a mut Editor) -> anyhow::Resul "Loaded all ({}) suggestions", result.load_state.loaded_item_len )); - editor.guide.style = ContentStyle { + editor.guide.config.style = Some(ContentStyle { foreground_color: Some(Color::Green), ..Default::default() - }; + }); } else { editor.guide.text = Text::from(format!( "Loaded partially ({}) suggestions", result.load_state.loaded_item_len )); - editor.guide.style = ContentStyle { + editor.guide.config.style = Some(ContentStyle { foreground_color: Some(Color::Green), ..Default::default() - }; + }); } editor.state.texteditor.replace(&head); editor.handler = BOXED_SEARCHER_HANDLER; @@ -134,18 +125,18 @@ pub async fn edit<'a>(event: &'a Event, editor: &'a mut Editor) -> anyhow::Resul None => { editor.guide.text = Text::from(format!("No suggestion found for '{prefix}'")); - editor.guide.style = ContentStyle { + editor.guide.config.style = Some(ContentStyle { foreground_color: Some(Color::Yellow), ..Default::default() - }; + }); } }, Err(e) => { editor.guide.text = Text::from(format!("Failed to lookup suggestions: {e}")); - editor.guide.style = ContentStyle { + editor.guide.config.style = Some(ContentStyle { foreground_color: Some(Color::Yellow), ..Default::default() - }; + }); } } } @@ -173,13 +164,13 @@ pub async fn edit<'a>(event: &'a Event, editor: &'a mut Editor) -> anyhow::Resul editor .state .texteditor - .move_to_previous_nearest(&editor.state.word_break_chars); + .move_to_previous_nearest(&editor.state.config.word_break_chars); } key if editor.editor_keybinds.move_to_next_nearest.contains(key) => { editor .state .texteditor - .move_to_next_nearest(&editor.state.word_break_chars); + .move_to_next_nearest(&editor.state.config.word_break_chars); } // Erase char(s). @@ -199,13 +190,13 @@ pub async fn edit<'a>(event: &'a Event, editor: &'a mut Editor) -> anyhow::Resul editor .state .texteditor - .erase_to_previous_nearest(&editor.state.word_break_chars); + .erase_to_previous_nearest(&editor.state.config.word_break_chars); } key if editor.editor_keybinds.erase_to_next_nearest.contains(key) => { editor .state .texteditor - .erase_to_next_nearest(&editor.state.word_break_chars); + .erase_to_next_nearest(&editor.state.config.word_break_chars); } // Input char. @@ -220,7 +211,7 @@ pub async fn edit<'a>(event: &'a Event, editor: &'a mut Editor) -> anyhow::Resul modifiers: KeyModifiers::SHIFT, kind: KeyEventKind::Press, state: KeyEventState::NONE, - }) => match editor.state.edit_mode { + }) => match editor.state.config.edit_mode { text_editor::Mode::Insert => editor.state.texteditor.insert(*ch), text_editor::Mode::Overwrite => editor.state.texteditor.overwrite(*ch), }, diff --git a/src/json.rs b/src/json.rs index 5fcdd49..1bd93c2 100644 --- a/src/json.rs +++ b/src/json.rs @@ -13,7 +13,7 @@ use promkit_widgets::{ pane::Pane, PaneFactory, }, - jsonstream::{self, format::RowFormatter, jsonz, JsonStream}, + jsonstream::{self, config::Config as JsonStreamConfig, jsonz, JsonStream}, serde_json::{self, Deserializer, Value}, text::{self, Text}, }; @@ -33,19 +33,16 @@ pub struct Json { impl Json { pub fn new( - formatter: RowFormatter, + formatter: JsonStreamConfig, input_stream: &'static [serde_json::Value], keybinds: JsonViewerKeybinds, ) -> anyhow::Result { - let state = jsonstream::State { - stream: JsonStream::new(input_stream.iter()), - formatter, - lines: Default::default(), - }; - Ok(Self { json: input_stream, - state, + state: jsonstream::State { + stream: JsonStream::new(input_stream.iter()), + config: formatter, + }, keybinds, }) } @@ -93,9 +90,7 @@ impl Json { #[async_trait::async_trait] impl Visualizer for Json { async fn content_to_copy(&self) -> String { - self.state - .formatter - .format_raw_json(self.state.stream.rows()) + self.state.config.format_raw_json(self.state.stream.rows()) } async fn create_init_pane(&mut self, area: (u16, u16)) -> Pane { @@ -118,9 +113,12 @@ impl Visualizer for Json { if ret.iter().all(|val| *val == Value::Null) { guide = Some(text::State { text: Text::from(format!("jq returned 'null', which may indicate a typo or incorrect filter: `{input}`")), - style: ContentStyle { - foreground_color: Some(Color::Yellow), - attributes: Attributes::from(Attribute::Bold), + config: text::Config { + style: Some(ContentStyle { + foreground_color: Some(Color::Yellow), + attributes: Attributes::from(Attribute::Bold), + ..Default::default() + }), ..Default::default() }, ..Default::default() @@ -140,12 +138,14 @@ impl Visualizer for Json { Some( text::State { text: Text::from(format!("jq failed: `{e}`")), - style: ContentStyle { - foreground_color: Some(Color::Red), - attributes: Attributes::from(Attribute::Bold), + config: text::Config { + style: Some(ContentStyle { + foreground_color: Some(Color::Red), + attributes: Attributes::from(Attribute::Bold), + ..Default::default() + }), ..Default::default() }, - ..Default::default() } .create_pane(area.0, area.1), ), @@ -194,12 +194,12 @@ fn run_jaq( #[derive(Clone)] pub struct JsonStreamProvider { - formatter: RowFormatter, + formatter: JsonStreamConfig, max_streams: Option, } impl JsonStreamProvider { - pub fn new(formatter: RowFormatter, max_streams: Option) -> Self { + pub fn new(formatter: JsonStreamConfig, max_streams: Option) -> Self { Self { formatter, max_streams, diff --git a/src/main.rs b/src/main.rs index 34bbe4b..6d900b4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -8,8 +8,6 @@ use anyhow::anyhow; use clap::Parser; use config::Config; use promkit_widgets::{ - core::crossterm::style::Attribute, - jsonstream::format::RowFormatter, listbox::{self, Listbox}, text_editor::{self, TextEditor}, }; @@ -160,10 +158,7 @@ async fn main() -> anyhow::Result<()> { let listbox_state = listbox::State { listbox: Listbox::default(), - cursor: config.completion.cursor, - active_item_style: Some(config.completion.active_item_style), - inactive_item_style: Some(config.completion.inactive_item_style), - lines: config.completion.lines, + config: config.completion.listbox.clone(), }; let searcher = @@ -176,31 +171,11 @@ async fn main() -> anyhow::Result<()> { Default::default() }, history: Default::default(), - prefix: config.editor.theme_on_focus.prefix.clone(), - mask: Default::default(), - prefix_style: config.editor.theme_on_focus.prefix_style, - active_char_style: config.editor.theme_on_focus.active_char_style, - inactive_char_style: config.editor.theme_on_focus.inactive_char_style, - edit_mode: config.editor.mode, - word_break_chars: config.editor.word_break_chars, - lines: Default::default(), + config: config.editor.on_focus.clone(), }; - let provider = &mut JsonStreamProvider::new( - RowFormatter { - curly_brackets_style: config.json.theme.curly_brackets_style, - square_brackets_style: config.json.theme.square_brackets_style, - key_style: config.json.theme.key_style, - string_value_style: config.json.theme.string_value_style, - number_value_style: config.json.theme.number_value_style, - boolean_value_style: config.json.theme.boolean_value_style, - null_value_style: config.json.theme.null_value_style, - active_item_attribute: Attribute::Bold, - inactive_item_attribute: Attribute::Dim, - indent: config.json.theme.indent, - }, - config.json.max_streams, - ); + let provider = + &mut JsonStreamProvider::new(config.json.stream.clone(), config.json.max_streams); let item = Box::leak(input.into_boxed_str()); @@ -213,8 +188,8 @@ async fn main() -> anyhow::Result<()> { let editor = Editor::new( text_editor_state, searcher, - config.editor.theme_on_focus, - config.editor.theme_on_defocus, + config.editor.on_focus, + config.editor.on_defocus, // TODO: remove clones config.keybinds.on_editor.clone(), ); diff --git a/src/prompt.rs b/src/prompt.rs index 83d57fb..911d67d 100644 --- a/src/prompt.rs +++ b/src/prompt.rs @@ -7,7 +7,8 @@ use promkit_widgets::{ crossterm::{ cursor, event::{ - Event, EventStream, KeyCode, KeyEvent, KeyEventKind, KeyEventState, KeyModifiers, + DisableMouseCapture, EnableMouseCapture, Event, EventStream, MouseEvent, + MouseEventKind, }, execute, style::{Color, ContentStyle}, @@ -62,16 +63,22 @@ fn copy_to_clipboard(content: &str) -> text::State { Ok(mut clipboard) => match clipboard.set_text(content) { Ok(_) => text::State { text: Text::from("Copied to clipboard"), - style: ContentStyle { - foreground_color: Some(Color::Green), + config: text::Config { + style: Some(ContentStyle { + foreground_color: Some(Color::Green), + ..Default::default() + }), ..Default::default() }, ..Default::default() }, Err(e) => text::State { text: Text::from(format!("Failed to copy to clipboard: {e}")), - style: ContentStyle { - foreground_color: Some(Color::Red), + config: text::Config { + style: Some(ContentStyle { + foreground_color: Some(Color::Red), + ..Default::default() + }), ..Default::default() }, ..Default::default() @@ -82,8 +89,11 @@ fn copy_to_clipboard(content: &str) -> text::State { // https://github.com/1Password/arboard/issues/153 Err(e) => text::State { text: Text::from(format!("Failed to setup clipboard: {e}")), - style: ContentStyle { - foreground_color: Some(Color::Red), + config: text::Config { + style: Some(ContentStyle { + foreground_color: Some(Color::Red), + ..Default::default() + }), ..Default::default() }, ..Default::default() @@ -187,6 +197,25 @@ pub async fn run( 'main: loop { tokio::select! { Some(Ok(event)) = stream.next() => { + // Note: `HashSet::contains` compares full mouse events (including `column`/`row`), + // so wheel events are normalized to `(0, 0)` to match configured `ScrollUp`/`ScrollDown` bindings. + let event = match event { + Event::Mouse(mouse) + if matches!( + mouse.kind, + MouseEventKind::ScrollUp | MouseEventKind::ScrollDown + ) => + { + Event::Mouse(MouseEvent { + kind: mouse.kind, + column: 0, + row: 0, + modifiers: mouse.modifiers, + }) + } + other => other, + }; + match event { Event::Resize(width, height) => { debounce_resize_tx.send((width, height)).await?; @@ -194,20 +223,10 @@ pub async fn run( event if keybinds.exit.contains(&event) => { break 'main }, - Event::Key(KeyEvent { - code: KeyCode::Char('q'), - modifiers: KeyModifiers::CONTROL, - kind: KeyEventKind::Press, - state: KeyEventState::NONE, - }) => { + event if keybinds.copy_query.contains(&event) => { editor_copy_tx.send(()).await?; }, - Event::Key(KeyEvent { - code: KeyCode::Char('o'), - modifiers: KeyModifiers::CONTROL, - kind: KeyEventKind::Press, - state: KeyEventState::NONE, - }) => { + event if keybinds.copy_result.contains(&event) => { if context_monitor.is_idle().await { processor_copy_tx.send(()).await?; } else if !no_hint{ @@ -217,8 +236,11 @@ pub async fn run( Index::Guide, text::State { text: Text::from("Failed to copy while rendering is in progress.".to_string()), - style: ContentStyle { - foreground_color: Some(Color::Yellow), + config: text::Config { + style: Some(ContentStyle { + foreground_color: Some(Color::Yellow), + ..Default::default() + }), ..Default::default() }, ..Default::default() @@ -227,22 +249,17 @@ pub async fn run( ]).render().await?; } }, - Event::Key(KeyEvent { - code: KeyCode::Down, - modifiers: KeyModifiers::SHIFT, - kind: KeyEventKind::Press, - state: KeyEventState::NONE, - }) | Event::Key(KeyEvent { - code: KeyCode::Up, - modifiers: KeyModifiers::SHIFT, - kind: KeyEventKind::Press, - state: KeyEventState::NONE, - }) => { + event if keybinds.switch_mode.contains(&event) => { match focus { Focus::Editor => { if context_monitor.is_idle().await { focus = Focus::Processor; editor_focus_tx.send(false).await?; + execute!( + io::stdout(), + terminal::EnterAlternateScreen, + EnableMouseCapture, + )?; } else if !no_hint{ let size = terminal::size()?; shared_renderer.update([ @@ -250,8 +267,11 @@ pub async fn run( Index::Guide, text::State { text: Text::from("Failed to switch pane while rendering is in progress.".to_string()), - style: ContentStyle { - foreground_color: Some(Color::Yellow), + config: text::Config { + style: Some(ContentStyle { + foreground_color: Some(Color::Yellow), + ..Default::default() + }), ..Default::default() }, ..Default::default() @@ -262,6 +282,11 @@ pub async fn run( Focus::Processor => { focus = Focus::Editor; editor_focus_tx.send(true).await?; + execute!( + io::stdout(), + terminal::LeaveAlternateScreen, + DisableMouseCapture, + )?; }, } }, @@ -442,7 +467,7 @@ pub async fn run( editor_task.abort(); processor_task.abort(); - execute!(io::stdout(), cursor::Show)?; + execute!(io::stdout(), cursor::Show, DisableMouseCapture)?; disable_raw_mode()?; Ok(()) diff --git a/src/search.rs b/src/search.rs index 75004f0..93ce6a9 100644 --- a/src/search.rs +++ b/src/search.rs @@ -101,7 +101,7 @@ impl IncrementalSearcher { .listbox .len() .saturating_sub(self.state.listbox.position()) - < self.state.lines.unwrap_or(1) + < self.state.config.lines.unwrap_or(1) { self.load_more(); } @@ -116,7 +116,7 @@ impl IncrementalSearcher { } pub fn leave_search(&mut self) { - self.state.listbox = Listbox::from_displayable(Vec::::new()); + self.state.listbox = Listbox::from(Vec::::new()); self.search_chunk_remaining = Vec::::new(); } @@ -141,7 +141,7 @@ impl IncrementalSearcher { .drain(..self.search_result_chunk_size.min(items.len())) .collect::>(); self.search_chunk_remaining = items; - self.state.listbox = Listbox::from_displayable(used); + self.state.listbox = Listbox::from(used); Ok(StartSearchResult { head_item: Some(self.state.listbox.get().to_string()), load_state: state.clone(),