From 826ce642d40f831e73c8175103c1ff88d8dc34b0 Mon Sep 17 00:00:00 2001 From: ynqa Date: Wed, 25 Feb 2026 00:25:34 +0900 Subject: [PATCH 01/11] debug: use local promkit-widgets --- Cargo.lock | 18 ++++++++++++------ Cargo.toml | 3 ++- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2028174..1730733 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -858,7 +858,7 @@ dependencies = [ "jaq-std", "promkit-widgets", "serde", - "termcfg", + "termcfg 0.1.0", "tokio", "tokio-stream", "toml", @@ -1157,8 +1157,6 @@ dependencies = [ [[package]] name = "promkit-core" version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed89f85398b2590095afe8fb4852c177d09f568836c206a9bed823bf1e70a051" dependencies = [ "anyhow", "crossbeam-skiplist", @@ -1169,15 +1167,14 @@ dependencies = [ [[package]] name = "promkit-widgets" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7ef81079760b198d5dde773c78b94a72edc2ebd057be386382c379e0d854fb6" +version = "0.3.0" dependencies = [ "anyhow", "promkit-core", "rayon", "serde", "serde_json", + "termcfg 0.2.0", "tokio", ] @@ -1446,6 +1443,15 @@ dependencies = [ "thiserror 2.0.18", ] +[[package]] +name = "termcfg" +version = "0.2.0" +dependencies = [ + "crossterm", + "serde", + "thiserror 2.0.18", +] + [[package]] name = "thiserror" version = "1.0.69" diff --git a/Cargo.toml b/Cargo.toml index 1111a6d..adccc6b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,7 +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"] } +# promkit-widgets = { version = "0.2.0", features = ["jsonstream", "listbox", "text", "texteditor"] } +promkit-widgets = { path = "../promkit/promkit-widgets", version = "0.3.0", features = ["jsonstream", "listbox", "text", "texteditor"] } serde = "1.0.228" termcfg = { version = "0.1.0", features = ["crossterm_0_29_0"] } tokio = { version = "1.49.0", features = ["full"] } From 043e40d86f3284f1c7f5da7a253a72b865fe705e Mon Sep 17 00:00:00 2001 From: ynqa Date: Wed, 25 Feb 2026 02:06:56 +0900 Subject: [PATCH 02/11] chore: use promkit-widgets v0.2.0 => v0.3.0 --- default.toml | 40 +++++++++++++---------- src/config.rs | 67 ++++----------------------------------- src/config/text_editor.rs | 33 ------------------- src/editor.rs | 53 +++++++++++++------------------ src/json.rs | 43 +++++++++++++------------ src/main.rs | 37 ++++----------------- src/prompt.rs | 35 ++++++++++++++------ src/search.rs | 6 ++-- 8 files changed, 108 insertions(+), 206 deletions(-) delete mode 100644 src/config/text_editor.rs diff --git a/default.toml b/default.toml index 514838a..7c8e13f 100644 --- a/default.toml +++ b/default.toml @@ -2,11 +2,13 @@ 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 @@ -29,8 +31,6 @@ word_break_chars = [".", "|", "(", ")", "[", "]"] # - 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 -# Theme settings when the editor is focused -[editor.theme_on_focus] # Prefix shown before the cursor prefix = "❯❯ " # Style for the prefix @@ -41,7 +41,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 +58,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 +77,13 @@ 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" # 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 +94,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 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..a332643 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,17 @@ 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 +91,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 +114,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 +139,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 +195,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..afdb8af 100644 --- a/src/prompt.rs +++ b/src/prompt.rs @@ -62,16 +62,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 +88,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() @@ -217,8 +226,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() @@ -250,8 +262,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() 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(), From 472dfded715017c28055bb342119aa37fe66ef54 Mon Sep 17 00:00:00 2001 From: ynqa Date: Wed, 25 Feb 2026 02:13:33 +0900 Subject: [PATCH 03/11] docs: how to set .toml configs --- README.md | 90 +++++++++++++++++++++++++++++++--------------------- default.toml | 32 +++++++++++-------- 2 files changed, 72 insertions(+), 50 deletions(-) diff --git a/README.md b/README.md index 3b9301a..48414ad 100644 --- a/README.md +++ b/README.md @@ -214,42 +214,55 @@ it will be automatically created on first run. > 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,13 @@ 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" # 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 +326,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 +414,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 7c8e13f..0ecd218 100644 --- a/default.toml +++ b/default.toml @@ -14,22 +14,28 @@ edit_mode = "Insert" # 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 # Prefix shown before the cursor prefix = "❯❯ " From 3be5b0641dda885f38233e5256e163bd17e9efca Mon Sep 17 00:00:00 2001 From: ynqa Date: Wed, 25 Feb 2026 21:16:14 +0900 Subject: [PATCH 04/11] feat: use wrap for multiple lines for one field --- README.md | 4 ++++ default.toml | 4 ++++ src/json.rs | 3 +-- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 48414ad..1a59532 100644 --- a/README.md +++ b/README.md @@ -313,6 +313,10 @@ null_value_style = "fg=grey" 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] diff --git a/default.toml b/default.toml index 0ecd218..db9aa08 100644 --- a/default.toml +++ b/default.toml @@ -87,6 +87,10 @@ null_value_style = "fg=grey" 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] diff --git a/src/json.rs b/src/json.rs index a332643..1bd93c2 100644 --- a/src/json.rs +++ b/src/json.rs @@ -37,7 +37,6 @@ impl Json { input_stream: &'static [serde_json::Value], keybinds: JsonViewerKeybinds, ) -> anyhow::Result { - Ok(Self { json: input_stream, state: jsonstream::State { @@ -146,7 +145,7 @@ impl Visualizer for Json { ..Default::default() }), ..Default::default() - } + }, } .create_pane(area.0, area.1), ), From 9ca58ea11f55d9b77b429a0cb78890f503f05263 Mon Sep 17 00:00:00 2001 From: ynqa Date: Wed, 25 Feb 2026 21:40:00 +0900 Subject: [PATCH 05/11] chore: use official v0.2.0 for termcfg --- Cargo.lock | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index 1730733..e6b6da4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1446,6 +1446,8 @@ dependencies = [ [[package]] name = "termcfg" version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e444ec0de07571e26f31f6ac5006c03540b61d35bec105031e82c6d6cbb5fab" dependencies = [ "crossterm", "serde", From da3c871fc36ad3a81a2398253331c053e1e3eb26 Mon Sep 17 00:00:00 2001 From: ynqa Date: Wed, 25 Feb 2026 21:49:31 +0900 Subject: [PATCH 06/11] docs: config.toml will be revamped in v0.7.0 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1a59532..bea138c 100644 --- a/README.md +++ b/README.md @@ -208,7 +208,7 @@ 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 From 310a843af0db559dabc7ac435327a003a3f749e7 Mon Sep 17 00:00:00 2001 From: ynqa Date: Wed, 25 Feb 2026 21:57:41 +0900 Subject: [PATCH 07/11] FIX: apply keybinds for copies and switch-mode --- src/prompt.rs | 30 ++++-------------------------- 1 file changed, 4 insertions(+), 26 deletions(-) diff --git a/src/prompt.rs b/src/prompt.rs index afdb8af..e92986c 100644 --- a/src/prompt.rs +++ b/src/prompt.rs @@ -6,9 +6,7 @@ use promkit_widgets::{ core::{ crossterm::{ cursor, - event::{ - Event, EventStream, KeyCode, KeyEvent, KeyEventKind, KeyEventState, KeyModifiers, - }, + event::{Event, EventStream}, execute, style::{Color, ContentStyle}, terminal::{self, disable_raw_mode, enable_raw_mode}, @@ -203,20 +201,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{ @@ -239,17 +227,7 @@ 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 { From c05b72db6b5baf68f52abc315182bad412483c46 Mon Sep 17 00:00:00 2001 From: ynqa Date: Wed, 25 Feb 2026 22:31:46 +0900 Subject: [PATCH 08/11] feat: enable to scroll using mouse --- default.toml | 4 ++-- src/prompt.rs | 36 ++++++++++++++++++++++++++++++++++-- 2 files changed, 36 insertions(+), 4 deletions(-) diff --git a/default.toml b/default.toml index db9aa08..05f892e 100644 --- a/default.toml +++ b/default.toml @@ -163,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/prompt.rs b/src/prompt.rs index e92986c..911d67d 100644 --- a/src/prompt.rs +++ b/src/prompt.rs @@ -6,7 +6,10 @@ use promkit_widgets::{ core::{ crossterm::{ cursor, - event::{Event, EventStream}, + event::{ + DisableMouseCapture, EnableMouseCapture, Event, EventStream, MouseEvent, + MouseEventKind, + }, execute, style::{Color, ContentStyle}, terminal::{self, disable_raw_mode, enable_raw_mode}, @@ -194,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?; @@ -233,6 +255,11 @@ pub async fn run( 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([ @@ -255,6 +282,11 @@ pub async fn run( Focus::Processor => { focus = Focus::Editor; editor_focus_tx.send(true).await?; + execute!( + io::stdout(), + terminal::LeaveAlternateScreen, + DisableMouseCapture, + )?; }, } }, @@ -435,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(()) From 3076255fafc188df22e2eba300d63591651250bc Mon Sep 17 00:00:00 2001 From: ynqa Date: Thu, 26 Feb 2026 00:16:27 +0900 Subject: [PATCH 09/11] chore: use official v0.3.0 for promkit-widgets --- Cargo.lock | 5 ++++- Cargo.toml | 3 +-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e6b6da4..a123b6c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1157,6 +1157,8 @@ dependencies = [ [[package]] name = "promkit-core" version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed89f85398b2590095afe8fb4852c177d09f568836c206a9bed823bf1e70a051" dependencies = [ "anyhow", "crossbeam-skiplist", @@ -1168,6 +1170,8 @@ dependencies = [ [[package]] name = "promkit-widgets" version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b665ae2f09cfe22290386f73e642922980cb6a5edd83188989dcb1e914dc4a7b" dependencies = [ "anyhow", "promkit-core", @@ -1175,7 +1179,6 @@ dependencies = [ "serde", "serde_json", "termcfg 0.2.0", - "tokio", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index adccc6b..1e65d47 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,8 +17,7 @@ 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"] } -promkit-widgets = { path = "../promkit/promkit-widgets", version = "0.3.0", features = ["jsonstream", "listbox", "text", "texteditor"] } +promkit-widgets = { version = "0.3.0", features = ["jsonstream", "listbox", "serde", "text", "texteditor"], default-features = false } serde = "1.0.228" termcfg = { version = "0.1.0", features = ["crossterm_0_29_0"] } tokio = { version = "1.49.0", features = ["full"] } From 986c14b05d223e41835c5f62b32b51ef3ec353e1 Mon Sep 17 00:00:00 2001 From: ynqa Date: Thu, 26 Feb 2026 00:19:35 +0900 Subject: [PATCH 10/11] fix: use dependencies.promkit-widgets instead --- Cargo.lock | 32 ++++++++++++++++---------------- Cargo.toml | 6 +++++- 2 files changed, 21 insertions(+), 17 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a123b6c..588c6d8 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" @@ -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", @@ -1644,9 +1644,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", @@ -1657,9 +1657,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", @@ -1667,9 +1667,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", @@ -1680,9 +1680,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 1e65d47..93f185a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,7 +17,6 @@ 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.3.0", features = ["jsonstream", "listbox", "serde", "text", "texteditor"], default-features = false } serde = "1.0.228" termcfg = { version = "0.1.0", features = ["crossterm_0_29_0"] } tokio = { version = "1.49.0", features = ["full"] } @@ -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" From 5cc580031b51d728ad068ff9673093947d34a399 Mon Sep 17 00:00:00 2001 From: ynqa Date: Thu, 26 Feb 2026 00:21:27 +0900 Subject: [PATCH 11/11] fix: use termcfg v0.2.0 --- Cargo.lock | 15 ++------------- Cargo.toml | 2 +- 2 files changed, 3 insertions(+), 14 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 588c6d8..460d149 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -858,7 +858,7 @@ dependencies = [ "jaq-std", "promkit-widgets", "serde", - "termcfg 0.1.0", + "termcfg", "tokio", "tokio-stream", "toml", @@ -1178,7 +1178,7 @@ dependencies = [ "rayon", "serde", "serde_json", - "termcfg 0.2.0", + "termcfg", ] [[package]] @@ -1435,17 +1435,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "termcfg" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b8c057661e9a795bc9cb18a7d2924b00d0aa3c5a9e61dfe7de32cea8a1d8fe7" -dependencies = [ - "crossterm", - "serde", - "thiserror 2.0.18", -] - [[package]] name = "termcfg" version = "0.2.0" diff --git a/Cargo.toml b/Cargo.toml index 93f185a..eef4861 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,7 +18,7 @@ derive_builder = "0.20.2" dirs = "6.0.0" futures = "0.3.32" 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"