Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
197 changes: 100 additions & 97 deletions Cargo.lock

Large diffs are not rendered by default.

8 changes: 4 additions & 4 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,15 @@ readme = "README.md"
anyhow = "1.0.102"
arboard = { version = "3.6.1", features = ["wayland-data-control"] }
async-trait = "0.1.89"
clap = { version = "4.5.60", features = ["derive"] }
clap = { version = "4.6.0", features = ["derive"] }
duration-string = { version = "0.5.3", features = ["serde"] }
derive_builder = "0.20.2"
dirs = "6.0.0"
futures = "0.3.32"
rustix = { version = "1.1.4", features = ["stdio"] }
serde = "1.0.228"
termcfg = { version = "0.2.0", features = ["crossterm_0_29_0"] }
tokio = { version = "1.49.0", features = ["full"] }
tokio = { version = "1.50.0", features = ["full"] }
tokio-stream = "0.1.18"
toml = "0.9.8"

Expand All @@ -30,8 +30,8 @@ jaq-json = { version = "1.1.3", features = ["serde_json"] }
jaq-std = "2.1.2"

[dependencies.promkit-widgets]
version = "0.3.1"
features = ["jsonstream", "listbox", "serde", "text", "texteditor"]
version = "0.5.0"
features = ["jsonstream", "listbox", "serde", "spinner", "status", "texteditor"]
default-features = false

# The profile that 'cargo dist' will build with
Expand Down
77 changes: 35 additions & 42 deletions src/editor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,11 @@ use std::{future::Future, pin::Pin};

use promkit_widgets::{
core::{
crossterm::{
event::{Event, KeyCode, KeyEvent, KeyEventKind, KeyEventState, KeyModifiers},
style::{Color, ContentStyle},
},
Pane, PaneFactory,
crossterm::event::{Event, KeyCode, KeyEvent, KeyEventKind, KeyEventState, KeyModifiers},
grapheme::StyledGraphemes,
Widget,
},
text::{self, Text},
status::{self, Severity},
text_editor,
};

Expand All @@ -19,7 +17,7 @@ pub struct Editor {
state: text_editor::State,
focus_config: text_editor::Config,
defocus_config: text_editor::Config,
guide: text::State,
guide: status::State,
searcher: IncrementalSearcher,
editor_keybinds: EditorKeybinds,
}
Expand All @@ -37,7 +35,7 @@ impl Editor {
state,
focus_config,
defocus_config,
guide: text::State::default(),
guide: status::State::default(),
searcher,
editor_keybinds,
}
Expand All @@ -53,23 +51,23 @@ impl Editor {
self.searcher.leave_search();
self.handler = BOXED_EDITOR_HANDLER;

self.guide.text = Default::default();
self.guide = status::State::default();
}

pub fn text(&self) -> String {
self.state.texteditor.text_without_cursor().to_string()
}

pub fn create_editor_pane(&self, width: u16, height: u16) -> Pane {
self.state.create_pane(width, height)
pub fn create_editor_pane(&self, width: u16, height: u16) -> StyledGraphemes {
self.state.create_graphemes(width, height)
}

pub fn create_searcher_pane(&self, width: u16, height: u16) -> Pane {
pub fn create_searcher_pane(&self, width: u16, height: u16) -> StyledGraphemes {
self.searcher.create_pane(width, height)
}

pub fn create_guide_pane(&self, width: u16, height: u16) -> Pane {
self.guide.create_pane(width, height)
pub fn create_guide_pane(&self, width: u16, height: u16) -> StyledGraphemes {
self.guide.create_graphemes(width, height)
}

pub async fn operate(&mut self, event: &Event) -> anyhow::Result<()> {
Expand All @@ -92,7 +90,7 @@ const BOXED_SEARCHER_HANDLER: Handler =
};

pub async fn edit<'a>(event: &'a Event, editor: &'a mut Editor) -> anyhow::Result<()> {
editor.guide.text = Default::default();
editor.guide = status::State::default();

match event {
key if editor.editor_keybinds.completion.contains(key) => {
Expand All @@ -101,42 +99,37 @@ pub async fn edit<'a>(event: &'a Event, editor: &'a mut Editor) -> anyhow::Resul
Ok(result) => match result.head_item {
Some(head) => {
if result.load_state.loaded {
editor.guide.text = Text::from(format!(
"Loaded all ({}) suggestions",
result.load_state.loaded_item_len
));
editor.guide.config.style = Some(ContentStyle {
foreground_color: Some(Color::Green),
..Default::default()
});
editor.guide = status::State::new(
format!(
"Loaded all ({}) suggestions",
result.load_state.loaded_item_len
),
Severity::Success,
);
} else {
editor.guide.text = Text::from(format!(
"Loaded partially ({}) suggestions",
result.load_state.loaded_item_len
));
editor.guide.config.style = Some(ContentStyle {
foreground_color: Some(Color::Green),
..Default::default()
});
editor.guide = status::State::new(
format!(
"Loaded partially ({}) suggestions",
result.load_state.loaded_item_len
),
Severity::Success,
);
}
editor.state.texteditor.replace(&head);
editor.handler = BOXED_SEARCHER_HANDLER;
}
None => {
editor.guide.text =
Text::from(format!("No suggestion found for '{prefix}'"));
editor.guide.config.style = Some(ContentStyle {
foreground_color: Some(Color::Yellow),
..Default::default()
});
editor.guide = status::State::new(
format!("No suggestion found for '{prefix}'"),
Severity::Warning,
);
}
},
Err(e) => {
editor.guide.text = Text::from(format!("Failed to lookup suggestions: {e}"));
editor.guide.config.style = Some(ContentStyle {
foreground_color: Some(Color::Yellow),
..Default::default()
});
editor.guide = status::State::new(
format!("Failed to lookup suggestions: {e}"),
Severity::Warning,
);
}
}
}
Expand Down
60 changes: 20 additions & 40 deletions src/json.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,10 @@ use jaq_core::{
use jaq_json::Val;

use promkit_widgets::{
core::{
crossterm::{
event::Event,
style::{Attribute, Attributes, Color, ContentStyle},
},
pane::Pane,
PaneFactory,
},
core::{crossterm::event::Event, grapheme::StyledGraphemes, Widget},
jsonstream::{self, config::Config as JsonStreamConfig, jsonz, JsonStream},
serde_json::{self, Deserializer, Value},
text::{self, Text},
status::{self, Severity},
};

use crate::{
Expand Down Expand Up @@ -93,63 +86,50 @@ impl Visualizer for Json {
self.state.config.format_raw_json(self.state.stream.rows())
}

async fn create_init_pane(&mut self, area: (u16, u16)) -> Pane {
self.state.create_pane(area.0, area.1)
async fn create_init_pane(&mut self, area: (u16, u16)) -> StyledGraphemes {
self.state.create_graphemes(area.0, area.1)
}

async fn create_pane_from_event(&mut self, area: (u16, u16), event: &Event) -> Pane {
async fn create_pane_from_event(&mut self, area: (u16, u16), event: &Event) -> StyledGraphemes {
self.operate(event);
self.state.create_pane(area.0, area.1)
self.state.create_graphemes(area.0, area.1)
}

async fn create_panes_from_query(
&mut self,
area: (u16, u16),
input: String,
) -> (Option<Pane>, Option<Pane>) {
) -> (Option<StyledGraphemes>, Option<StyledGraphemes>) {
match run_jaq(&input, self.json) {
Ok(ret) => {
let mut guide = None;
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}`")),
config: text::Config {
style: Some(ContentStyle {
foreground_color: Some(Color::Yellow),
attributes: Attributes::from(Attribute::Bold),
..Default::default()
}),
..Default::default()
},
..Default::default()
}.create_pane(area.0, area.1));
guide = Some(
status::State::new(
format!(
"jq returned 'null', which may indicate a typo or incorrect filter: `{input}`"
),
Severity::Warning,
)
.create_graphemes(area.0, area.1),
);

self.state.stream = JsonStream::new(self.json.iter());
} else {
self.state.stream = JsonStream::new(ret.iter());
}

(guide, Some(self.state.create_pane(area.0, area.1)))
(guide, Some(self.state.create_graphemes(area.0, area.1)))
}
Err(e) => {
self.state.stream = JsonStream::new(self.json.iter());

(
Some(
text::State {
text: Text::from(format!("jq failed: `{e}`")),
config: text::Config {
style: Some(ContentStyle {
foreground_color: Some(Color::Red),
attributes: Attributes::from(Attribute::Bold),
..Default::default()
}),
..Default::default()
},
}
.create_pane(area.0, area.1),
status::State::new(format!("jq failed: `{e}`"), Severity::Error)
.create_graphemes(area.0, area.1),
),
Some(self.state.create_pane(area.0, area.1)),
Some(self.state.create_graphemes(area.0, area.1)),
)
}
}
Expand Down
3 changes: 1 addition & 2 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,7 @@ mod stdout_redirect;
use stdout_redirect::StdoutRedirect;
mod processor;
use processor::{
init::ViewInitializer, monitor::ContextMonitor, spinner::SpinnerSpawner, Context, Processor,
ViewProvider, Visualizer,
init::ViewInitializer, monitor::ContextMonitor, Context, Processor, ViewProvider, Visualizer,
};
mod prompt;
mod search;
Expand Down
22 changes: 10 additions & 12 deletions src/processor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,7 @@ use std::sync::Arc;

use async_trait::async_trait;
use promkit_widgets::core::{
crossterm::event::Event,
pane::{Pane, EMPTY_PANE},
render::SharedRenderer,
crossterm::event::Event, grapheme::StyledGraphemes, render::SharedRenderer,
};
use tokio::{sync::Mutex, task::JoinHandle};

Expand All @@ -13,7 +11,10 @@ pub use init::ViewProvider;

use crate::prompt::Index;
pub mod monitor;
pub mod spinner;

fn empty_pane() -> StyledGraphemes {
StyledGraphemes::default()
}

#[derive(PartialEq)]
enum State {
Expand All @@ -25,13 +26,13 @@ enum State {
#[async_trait]
pub trait Visualizer: Send + Sync + 'static {
async fn content_to_copy(&self) -> String;
async fn create_init_pane(&mut self, area: (u16, u16)) -> Pane;
async fn create_pane_from_event(&mut self, area: (u16, u16), event: &Event) -> Pane;
async fn create_init_pane(&mut self, area: (u16, u16)) -> StyledGraphemes;
async fn create_pane_from_event(&mut self, area: (u16, u16), event: &Event) -> StyledGraphemes;
async fn create_panes_from_query(
&mut self,
area: (u16, u16),
query: String,
) -> (Option<Pane>, Option<Pane>);
) -> (Option<StyledGraphemes>, Option<StyledGraphemes>);
}

pub struct Context {
Expand Down Expand Up @@ -90,11 +91,8 @@ impl Processor {
// TODO: error handling
let _ = shared_renderer
.update([
(Index::Guide, maybe_guide.unwrap_or(EMPTY_PANE.to_owned())),
(
Index::Processor,
maybe_resp.unwrap_or(EMPTY_PANE.to_owned()),
),
(Index::Guide, maybe_guide.unwrap_or_else(empty_pane)),
(Index::Processor, maybe_resp.unwrap_or_else(empty_pane)),
])
.render()
.await;
Expand Down
14 changes: 10 additions & 4 deletions src/processor/monitor.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use std::sync::Arc;
use std::{future::Future, sync::Arc};

use promkit_widgets::spinner;
use tokio::sync::Mutex;

use super::{Context, State};
Expand All @@ -12,9 +13,14 @@ impl ContextMonitor {
pub fn new(shared: Arc<Mutex<Context>>) -> Self {
Self { shared }
}
}

pub async fn is_idle(&self) -> bool {
let context = self.shared.lock().await;
context.state == State::Idle
impl spinner::State for ContextMonitor {
fn is_idle(&self) -> impl Future<Output = bool> + Send {
let shared = self.shared.clone();
async move {
let context = shared.lock().await;
context.state == State::Idle
}
}
}
Loading
Loading