feat: Add streaming pipe support for async loading in.
This commit is contained in:
@@ -53,6 +53,7 @@ pub enum Action {
|
||||
RemoveItems(Vec<usize>),
|
||||
ProcessHookResponse(HookResponse),
|
||||
CloseMenu,
|
||||
StreamingDone,
|
||||
}
|
||||
|
||||
/// Broadcast from the menu loop to all subscribers
|
||||
@@ -81,6 +82,8 @@ pub struct ViewState {
|
||||
pub mode: Mode,
|
||||
pub selection_count: usize,
|
||||
pub multi_enabled: bool,
|
||||
/// True while stdin is still streaming items in.
|
||||
pub streaming: bool,
|
||||
/// Column headers for table mode. None when not in
|
||||
/// table mode.
|
||||
pub columns: Option<Vec<ColumnHeader>>,
|
||||
|
||||
@@ -8,6 +8,15 @@ use tokio::io::AsyncBufReadExt;
|
||||
use crate::error::PiklError;
|
||||
use crate::item::Item;
|
||||
|
||||
/// Parse a single line into a JSON Value. Tries JSON first,
|
||||
/// falls back to wrapping as a string. Used by the streaming
|
||||
/// stdin reader in the CLI binary, which sends raw Values
|
||||
/// via `Action::AddItems`.
|
||||
pub fn parse_line_to_value(line: &str) -> serde_json::Value {
|
||||
serde_json::from_str::<serde_json::Value>(line)
|
||||
.unwrap_or_else(|_| serde_json::Value::String(line.to_string()))
|
||||
}
|
||||
|
||||
/// Try to parse a line as JSON. Falls back to wrapping
|
||||
/// it as a plain-text string. The `label_key` controls
|
||||
/// which JSON key is used as the display label for object
|
||||
@@ -116,4 +125,39 @@ mod tests {
|
||||
assert_eq!(items.len(), 1);
|
||||
assert_eq!(items[0].label(), "quoted string");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_line_to_value_plain_text() {
|
||||
let v = parse_line_to_value("hello world");
|
||||
assert_eq!(v, serde_json::Value::String("hello world".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_line_to_value_json_object() {
|
||||
let v = parse_line_to_value(r#"{"label": "foo", "count": 42}"#);
|
||||
assert!(v.is_object());
|
||||
assert_eq!(v["label"], "foo");
|
||||
assert_eq!(v["count"], 42);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_line_to_value_invalid_json() {
|
||||
let v = parse_line_to_value("{not valid json}");
|
||||
assert_eq!(
|
||||
v,
|
||||
serde_json::Value::String("{not valid json}".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_line_to_value_json_string() {
|
||||
let v = parse_line_to_value(r#""quoted""#);
|
||||
assert_eq!(v, serde_json::Value::String("quoted".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_line_to_value_json_number() {
|
||||
let v = parse_line_to_value("42");
|
||||
assert_eq!(v, serde_json::json!(42));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -154,6 +154,7 @@ pub struct MenuRunner<M: MutableMenu> {
|
||||
generation: u64,
|
||||
selection: SelectionState,
|
||||
visual_anchor: Option<usize>,
|
||||
streaming: bool,
|
||||
}
|
||||
|
||||
impl<M: MutableMenu> MenuRunner<M> {
|
||||
@@ -180,6 +181,7 @@ impl<M: MutableMenu> MenuRunner<M> {
|
||||
generation: 0,
|
||||
selection: SelectionState::new(),
|
||||
visual_anchor: None,
|
||||
streaming: false,
|
||||
};
|
||||
(runner, action_tx)
|
||||
}
|
||||
@@ -312,6 +314,7 @@ impl<M: MutableMenu> MenuRunner<M> {
|
||||
mode: self.mode,
|
||||
selection_count: self.selection.count(),
|
||||
multi_enabled: self.selection.multi_enabled,
|
||||
streaming: self.streaming,
|
||||
columns,
|
||||
generation: self.generation,
|
||||
}
|
||||
@@ -557,6 +560,11 @@ impl<M: MutableMenu> MenuRunner<M> {
|
||||
self.apply_action(action)
|
||||
}
|
||||
Action::CloseMenu => ActionOutcome::Closed,
|
||||
Action::StreamingDone => {
|
||||
debug!("streaming done");
|
||||
self.streaming = false;
|
||||
ActionOutcome::Broadcast
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -566,6 +574,11 @@ impl<M: MutableMenu> MenuRunner<M> {
|
||||
self.mode = mode;
|
||||
}
|
||||
|
||||
/// Mark the menu as actively streaming items from stdin.
|
||||
pub fn set_streaming(&mut self, v: bool) {
|
||||
self.streaming = v;
|
||||
}
|
||||
|
||||
/// Run the menu event loop. Consumes actions and
|
||||
/// broadcasts events.
|
||||
///
|
||||
@@ -1824,4 +1837,54 @@ mod tests {
|
||||
// City "toronto" (7) vs header "City" (4): width = 7
|
||||
assert_eq!(cols[2].width, 7);
|
||||
}
|
||||
|
||||
// -- Streaming tests --
|
||||
|
||||
#[test]
|
||||
fn streaming_done_sets_streaming_false() {
|
||||
let (mut m, _tx) = test_menu();
|
||||
m.set_streaming(true);
|
||||
m.run_filter();
|
||||
assert!(m.build_view_state().streaming);
|
||||
let outcome = m.apply_action(Action::StreamingDone);
|
||||
assert!(matches!(outcome, ActionOutcome::Broadcast));
|
||||
assert!(!m.build_view_state().streaming);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_items_on_empty_streaming_menu() {
|
||||
let (mut m, _tx) = MenuRunner::new(JsonMenu::new(vec![], "label".to_string()));
|
||||
m.set_streaming(true);
|
||||
m.run_filter();
|
||||
m.apply_action(Action::Resize { height: 20 });
|
||||
|
||||
assert_eq!(m.menu.total(), 0);
|
||||
assert!(m.build_view_state().streaming);
|
||||
|
||||
m.apply_action(Action::AddItems(vec![
|
||||
Value::String("one".into()),
|
||||
Value::String("two".into()),
|
||||
]));
|
||||
assert_eq!(m.menu.total(), 2);
|
||||
assert_eq!(m.menu.filtered_count(), 2);
|
||||
|
||||
// Confirm on non-empty streaming menu works
|
||||
let outcome = m.apply_action(Action::Confirm);
|
||||
assert!(matches!(outcome, ActionOutcome::Selected { .. }));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn confirm_on_empty_streaming_menu_is_noop() {
|
||||
let (mut m, _tx) = MenuRunner::new(JsonMenu::new(vec![], "label".to_string()));
|
||||
m.set_streaming(true);
|
||||
m.run_filter();
|
||||
let outcome = m.apply_action(Action::Confirm);
|
||||
assert!(matches!(outcome, ActionOutcome::NoOp));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn view_state_streaming_defaults_false() {
|
||||
let mut m = ready_menu();
|
||||
assert!(!m.build_view_state().streaming);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -161,6 +161,7 @@ pub fn parse_action(line_number: usize, line: &str) -> Result<ScriptAction, Scri
|
||||
"show-ui" => Ok(ScriptAction::ShowUi),
|
||||
"show-tui" => Ok(ScriptAction::ShowTui),
|
||||
"show-gui" => Ok(ScriptAction::ShowGui),
|
||||
"streaming-done" => Ok(ScriptAction::Core(Action::StreamingDone)),
|
||||
_ => Err(ScriptError {
|
||||
line: line_number,
|
||||
source_line: line.to_string(),
|
||||
@@ -678,6 +679,14 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_streaming_done() {
|
||||
assert_eq!(
|
||||
parse_action(1, "streaming-done").unwrap_or(ScriptAction::Comment),
|
||||
ScriptAction::Core(Action::StreamingDone)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_set_mode_wrong_case() {
|
||||
assert!(parse_action(1, "set-mode Insert").is_err());
|
||||
|
||||
Reference in New Issue
Block a user