feat(core): Add Quicklist function to return all visible via ctrl+Q.
Some checks failed
CI / Check (macos-latest) (push) Has been cancelled
CI / Check (ubuntu-latest) (push) Has been cancelled
CI / Clippy (strict) (push) Has been cancelled
CI / Format (push) Has been cancelled
CI / Test (macos-latest) (push) Has been cancelled
CI / Test (ubuntu-latest) (push) Has been cancelled
Some checks failed
CI / Check (macos-latest) (push) Has been cancelled
CI / Check (ubuntu-latest) (push) Has been cancelled
CI / Clippy (strict) (push) Has been cancelled
CI / Format (push) Has been cancelled
CI / Test (macos-latest) (push) Has been cancelled
CI / Test (ubuntu-latest) (push) Has been cancelled
This commit is contained in:
@@ -38,6 +38,7 @@ pub enum Action {
|
|||||||
HalfPageDown(usize),
|
HalfPageDown(usize),
|
||||||
SetMode(Mode),
|
SetMode(Mode),
|
||||||
Confirm,
|
Confirm,
|
||||||
|
Quicklist,
|
||||||
Cancel,
|
Cancel,
|
||||||
Resize { height: u16 },
|
Resize { height: u16 },
|
||||||
AddItems(Vec<Value>),
|
AddItems(Vec<Value>),
|
||||||
@@ -53,6 +54,7 @@ pub enum Action {
|
|||||||
pub enum MenuEvent {
|
pub enum MenuEvent {
|
||||||
StateChanged(ViewState),
|
StateChanged(ViewState),
|
||||||
Selected(Value),
|
Selected(Value),
|
||||||
|
Quicklist(Vec<Value>),
|
||||||
Cancelled,
|
Cancelled,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,5 +91,6 @@ pub struct VisibleItem {
|
|||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum MenuResult {
|
pub enum MenuResult {
|
||||||
Selected { value: Value, index: usize },
|
Selected { value: Value, index: usize },
|
||||||
|
Quicklist { items: Vec<(Value, usize)> },
|
||||||
Cancelled,
|
Cancelled,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ use serde_json::Value;
|
|||||||
#[serde(rename_all = "snake_case")]
|
#[serde(rename_all = "snake_case")]
|
||||||
pub enum OutputAction {
|
pub enum OutputAction {
|
||||||
Select,
|
Select,
|
||||||
|
Quicklist,
|
||||||
Cancel,
|
Cancel,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,6 +95,18 @@ mod tests {
|
|||||||
assert_eq!(json["action"], "cancel");
|
assert_eq!(json["action"], "cancel");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn output_item_quicklist_action() {
|
||||||
|
let item = OutputItem {
|
||||||
|
value: json!("test"),
|
||||||
|
action: OutputAction::Quicklist,
|
||||||
|
index: 2,
|
||||||
|
};
|
||||||
|
let json = serde_json::to_value(&item).unwrap_or_default();
|
||||||
|
assert_eq!(json["action"], "quicklist");
|
||||||
|
assert_eq!(json["index"], 2);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn output_item_string_contains_value_text() {
|
fn output_item_string_contains_value_text() {
|
||||||
let item = OutputItem {
|
let item = OutputItem {
|
||||||
|
|||||||
@@ -59,4 +59,17 @@ pub trait Menu: Send + 'static {
|
|||||||
fn formatted_label(&self, _filtered_index: usize) -> Option<String> {
|
fn formatted_label(&self, _filtered_index: usize) -> Option<String> {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Collect all filtered items as (value, original_index) pairs.
|
||||||
|
/// Default implementation iterates filtered results using
|
||||||
|
/// existing trait methods.
|
||||||
|
fn collect_filtered(&self) -> Vec<(&serde_json::Value, usize)> {
|
||||||
|
(0..self.filtered_count())
|
||||||
|
.filter_map(|i| {
|
||||||
|
let value = self.serialize_filtered(i)?;
|
||||||
|
let orig = self.original_index(i)?;
|
||||||
|
Some((value, orig))
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ pub enum HookEvent {
|
|||||||
Select { item: Value, index: usize },
|
Select { item: Value, index: usize },
|
||||||
Cancel,
|
Cancel,
|
||||||
Filter { text: String },
|
Filter { text: String },
|
||||||
|
Quicklist { items: Vec<Value>, count: usize },
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Discriminant for [`HookEvent`], used as a key for
|
/// Discriminant for [`HookEvent`], used as a key for
|
||||||
@@ -32,6 +33,7 @@ pub enum HookEventKind {
|
|||||||
Select,
|
Select,
|
||||||
Cancel,
|
Cancel,
|
||||||
Filter,
|
Filter,
|
||||||
|
Quicklist,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl HookEvent {
|
impl HookEvent {
|
||||||
@@ -44,6 +46,7 @@ impl HookEvent {
|
|||||||
HookEvent::Select { .. } => HookEventKind::Select,
|
HookEvent::Select { .. } => HookEventKind::Select,
|
||||||
HookEvent::Cancel => HookEventKind::Cancel,
|
HookEvent::Cancel => HookEventKind::Cancel,
|
||||||
HookEvent::Filter { .. } => HookEventKind::Filter,
|
HookEvent::Filter { .. } => HookEventKind::Filter,
|
||||||
|
HookEvent::Quicklist { .. } => HookEventKind::Quicklist,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -286,6 +289,27 @@ mod tests {
|
|||||||
assert!(resp.is_none());
|
assert!(resp.is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn event_quicklist_serializes() {
|
||||||
|
let event = HookEvent::Quicklist {
|
||||||
|
items: vec![json!("alpha"), json!("beta")],
|
||||||
|
count: 2,
|
||||||
|
};
|
||||||
|
let json = serde_json::to_value(&event).unwrap_or_default();
|
||||||
|
assert_eq!(json["event"], "quicklist");
|
||||||
|
assert_eq!(json["count"], 2);
|
||||||
|
assert_eq!(json["items"].as_array().map(|a| a.len()), Some(2));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn event_kind_quicklist() {
|
||||||
|
let event = HookEvent::Quicklist {
|
||||||
|
items: vec![],
|
||||||
|
count: 0,
|
||||||
|
};
|
||||||
|
assert_eq!(event.kind(), HookEventKind::Quicklist);
|
||||||
|
}
|
||||||
|
|
||||||
// -- Roundtrip: HookEvent serialize -> check shape --
|
// -- Roundtrip: HookEvent serialize -> check shape --
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -27,6 +27,8 @@ pub enum ActionOutcome {
|
|||||||
Cancelled,
|
Cancelled,
|
||||||
/// Menu closed by hook command.
|
/// Menu closed by hook command.
|
||||||
Closed,
|
Closed,
|
||||||
|
/// User quicklisted the filtered items.
|
||||||
|
Quicklist { items: Vec<(Value, usize)> },
|
||||||
/// Nothing happened (e.g. confirm on empty list).
|
/// Nothing happened (e.g. confirm on empty list).
|
||||||
NoOp,
|
NoOp,
|
||||||
}
|
}
|
||||||
@@ -218,6 +220,18 @@ impl<M: Menu> MenuRunner<M> {
|
|||||||
None => ActionOutcome::NoOp,
|
None => ActionOutcome::NoOp,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Action::Quicklist => {
|
||||||
|
if self.menu.filtered_count() == 0 {
|
||||||
|
return ActionOutcome::NoOp;
|
||||||
|
}
|
||||||
|
let items: Vec<(Value, usize)> = self
|
||||||
|
.menu
|
||||||
|
.collect_filtered()
|
||||||
|
.into_iter()
|
||||||
|
.map(|(v, idx)| (v.clone(), idx))
|
||||||
|
.collect();
|
||||||
|
ActionOutcome::Quicklist { items }
|
||||||
|
}
|
||||||
Action::Cancel => ActionOutcome::Cancelled,
|
Action::Cancel => ActionOutcome::Cancelled,
|
||||||
Action::Resize { height } => {
|
Action::Resize { height } => {
|
||||||
self.viewport.set_height(height as usize);
|
self.viewport.set_height(height as usize);
|
||||||
@@ -333,6 +347,21 @@ impl<M: Menu> MenuRunner<M> {
|
|||||||
let _ = self.event_tx.send(MenuEvent::Selected(value.clone()));
|
let _ = self.event_tx.send(MenuEvent::Selected(value.clone()));
|
||||||
return Ok(MenuResult::Selected { value, index });
|
return Ok(MenuResult::Selected { value, index });
|
||||||
}
|
}
|
||||||
|
ActionOutcome::Quicklist { items } => {
|
||||||
|
let values: Vec<Value> =
|
||||||
|
items.iter().map(|(v, _)| v.clone()).collect();
|
||||||
|
let count = values.len();
|
||||||
|
self.emit_hook(HookEvent::Quicklist {
|
||||||
|
items: values.clone(),
|
||||||
|
count,
|
||||||
|
});
|
||||||
|
self.emit_hook(HookEvent::Close);
|
||||||
|
|
||||||
|
let _ = self
|
||||||
|
.event_tx
|
||||||
|
.send(MenuEvent::Quicklist(values));
|
||||||
|
return Ok(MenuResult::Quicklist { items });
|
||||||
|
}
|
||||||
ActionOutcome::Cancelled => {
|
ActionOutcome::Cancelled => {
|
||||||
self.emit_hook(HookEvent::Cancel);
|
self.emit_hook(HookEvent::Cancel);
|
||||||
self.emit_hook(HookEvent::Close);
|
self.emit_hook(HookEvent::Close);
|
||||||
@@ -945,6 +974,127 @@ mod tests {
|
|||||||
assert!(matches!(outcome, ActionOutcome::Selected { index: 3, .. }));
|
assert!(matches!(outcome, ActionOutcome::Selected { index: 3, .. }));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// -- Quicklist tests --
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn apply_quicklist_returns_all_filtered() {
|
||||||
|
let mut m = ready_menu();
|
||||||
|
let outcome = m.apply_action(Action::Quicklist);
|
||||||
|
match outcome {
|
||||||
|
ActionOutcome::Quicklist { items } => {
|
||||||
|
assert_eq!(items.len(), 4);
|
||||||
|
let labels: Vec<&str> = items
|
||||||
|
.iter()
|
||||||
|
.filter_map(|(v, _)| v.as_str())
|
||||||
|
.collect();
|
||||||
|
assert_eq!(labels, vec!["alpha", "beta", "gamma", "delta"]);
|
||||||
|
}
|
||||||
|
other => panic!("expected Quicklist, got {other:?}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn apply_quicklist_respects_filter() {
|
||||||
|
let mut m = ready_menu();
|
||||||
|
m.apply_action(Action::UpdateFilter("a".to_string()));
|
||||||
|
let outcome = m.apply_action(Action::Quicklist);
|
||||||
|
match outcome {
|
||||||
|
ActionOutcome::Quicklist { items } => {
|
||||||
|
// "alpha", "gamma", "delta" all contain 'a'
|
||||||
|
assert!(items.len() >= 2);
|
||||||
|
for (v, _) in &items {
|
||||||
|
let label = v.as_str().unwrap_or("");
|
||||||
|
assert!(
|
||||||
|
label.contains('a'),
|
||||||
|
"filtered item should contain 'a': {label}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
other => panic!("expected Quicklist, got {other:?}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn apply_quicklist_on_empty_is_noop() {
|
||||||
|
let mut m = ready_menu();
|
||||||
|
m.apply_action(Action::UpdateFilter("zzzzz".to_string()));
|
||||||
|
assert_eq!(m.menu.filtered_count(), 0);
|
||||||
|
let outcome = m.apply_action(Action::Quicklist);
|
||||||
|
assert!(matches!(outcome, ActionOutcome::NoOp));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn apply_quicklist_preserves_original_indices() {
|
||||||
|
let mut m = ready_menu();
|
||||||
|
// Filter to "del" -> only "delta" at original index 3
|
||||||
|
m.apply_action(Action::UpdateFilter("del".to_string()));
|
||||||
|
let outcome = m.apply_action(Action::Quicklist);
|
||||||
|
match outcome {
|
||||||
|
ActionOutcome::Quicklist { items } => {
|
||||||
|
assert_eq!(items.len(), 1);
|
||||||
|
assert_eq!(items[0].0.as_str(), Some("delta"));
|
||||||
|
assert_eq!(items[0].1, 3);
|
||||||
|
}
|
||||||
|
other => panic!("expected Quicklist, got {other:?}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn quicklist_event_broadcast() {
|
||||||
|
let (menu, tx) = test_menu();
|
||||||
|
let mut rx = menu.subscribe();
|
||||||
|
|
||||||
|
let handle = tokio::spawn(async move { menu.run().await });
|
||||||
|
|
||||||
|
// Skip initial state
|
||||||
|
let _ = rx.recv().await;
|
||||||
|
let _ = tx.send(Action::Resize { height: 10 }).await;
|
||||||
|
let _ = rx.recv().await;
|
||||||
|
|
||||||
|
let _ = tx.send(Action::Quicklist).await;
|
||||||
|
|
||||||
|
if let Ok(MenuEvent::Quicklist(values)) = rx.recv().await {
|
||||||
|
assert_eq!(values.len(), 4);
|
||||||
|
} else {
|
||||||
|
panic!("expected Quicklist event");
|
||||||
|
}
|
||||||
|
|
||||||
|
let result = handle.await.unwrap_or(Ok(MenuResult::Cancelled));
|
||||||
|
assert!(matches!(result, Ok(MenuResult::Quicklist { .. })));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn quicklist_after_filter() {
|
||||||
|
let (menu, tx) = test_menu();
|
||||||
|
let mut rx = menu.subscribe();
|
||||||
|
|
||||||
|
let handle = tokio::spawn(async move { menu.run().await });
|
||||||
|
|
||||||
|
let _ = rx.recv().await; // initial
|
||||||
|
let _ = tx.send(Action::Resize { height: 10 }).await;
|
||||||
|
let _ = rx.recv().await;
|
||||||
|
|
||||||
|
// Filter then quicklist back-to-back
|
||||||
|
let _ = tx.send(Action::UpdateFilter("al".to_string())).await;
|
||||||
|
let _ = tx.send(Action::Quicklist).await;
|
||||||
|
|
||||||
|
let result = handle.await.unwrap_or(Ok(MenuResult::Cancelled));
|
||||||
|
match result {
|
||||||
|
Ok(MenuResult::Quicklist { items }) => {
|
||||||
|
// "alpha" matches "al"
|
||||||
|
assert!(!items.is_empty());
|
||||||
|
for (v, _) in &items {
|
||||||
|
let label = v.as_str().unwrap_or("");
|
||||||
|
assert!(
|
||||||
|
label.contains("al"),
|
||||||
|
"quicklist item should match filter: {label}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
other => panic!("expected Quicklist, got {other:?}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// -- Hook event tests --
|
// -- Hook event tests --
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
|||||||
@@ -132,7 +132,7 @@ async fn run_inner(
|
|||||||
}
|
}
|
||||||
view_state = Some(vs);
|
view_state = Some(vs);
|
||||||
}
|
}
|
||||||
Ok(MenuEvent::Selected(_) | MenuEvent::Cancelled) => {
|
Ok(MenuEvent::Selected(_) | MenuEvent::Quicklist(_) | MenuEvent::Cancelled) => {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
Err(broadcast::error::RecvError::Lagged(_)) => {}
|
Err(broadcast::error::RecvError::Lagged(_)) => {}
|
||||||
@@ -233,6 +233,7 @@ fn map_insert_mode(key: KeyEvent, filter_text: &mut String) -> Option<Action> {
|
|||||||
filter_text.pop();
|
filter_text.pop();
|
||||||
Some(Action::UpdateFilter(filter_text.clone()))
|
Some(Action::UpdateFilter(filter_text.clone()))
|
||||||
}
|
}
|
||||||
|
(KeyCode::Char('q'), KeyModifiers::CONTROL) => Some(Action::Quicklist),
|
||||||
(KeyCode::Char(c), mods) if !mods.intersects(KeyModifiers::CONTROL | KeyModifiers::ALT) => {
|
(KeyCode::Char(c), mods) if !mods.intersects(KeyModifiers::CONTROL | KeyModifiers::ALT) => {
|
||||||
filter_text.push(c);
|
filter_text.push(c);
|
||||||
Some(Action::UpdateFilter(filter_text.clone()))
|
Some(Action::UpdateFilter(filter_text.clone()))
|
||||||
@@ -276,6 +277,7 @@ fn map_normal_mode(key: KeyEvent, pending: &mut PendingKey) -> Option<Action> {
|
|||||||
(KeyCode::Char('/'), m) if !m.intersects(KeyModifiers::CONTROL | KeyModifiers::ALT) => {
|
(KeyCode::Char('/'), m) if !m.intersects(KeyModifiers::CONTROL | KeyModifiers::ALT) => {
|
||||||
Some(Action::SetMode(Mode::Insert))
|
Some(Action::SetMode(Mode::Insert))
|
||||||
}
|
}
|
||||||
|
(KeyCode::Char('q'), KeyModifiers::CONTROL) => Some(Action::Quicklist),
|
||||||
(KeyCode::Char('q'), m) if !m.intersects(KeyModifiers::CONTROL | KeyModifiers::ALT) => {
|
(KeyCode::Char('q'), m) if !m.intersects(KeyModifiers::CONTROL | KeyModifiers::ALT) => {
|
||||||
Some(Action::Cancel)
|
Some(Action::Cancel)
|
||||||
}
|
}
|
||||||
@@ -663,6 +665,28 @@ mod tests {
|
|||||||
assert_eq!(ft, "");
|
assert_eq!(ft, "");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ctrl_q_maps_to_quicklist_insert() {
|
||||||
|
let mut ft = String::new();
|
||||||
|
let mut pending = PendingKey::None;
|
||||||
|
let k = key_with_mods(KeyCode::Char('q'), KeyModifiers::CONTROL);
|
||||||
|
assert_eq!(
|
||||||
|
map_key_event(k, &mut ft, Mode::Insert, &mut pending),
|
||||||
|
Some(Action::Quicklist)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ctrl_q_maps_to_quicklist_normal() {
|
||||||
|
let mut ft = String::new();
|
||||||
|
let mut pending = PendingKey::None;
|
||||||
|
let k = key_with_mods(KeyCode::Char('q'), KeyModifiers::CONTROL);
|
||||||
|
assert_eq!(
|
||||||
|
map_key_event(k, &mut ft, Mode::Normal, &mut pending),
|
||||||
|
Some(Action::Quicklist)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// -- Rendering tests (TestBackend) --
|
// -- Rendering tests (TestBackend) --
|
||||||
|
|
||||||
fn render_to_backend(width: u16, height: u16, vs: &ViewState, filter: &str) -> TestBackend {
|
fn render_to_backend(width: u16, height: u16, vs: &ViewState, filter: &str) -> TestBackend {
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ use pikl_core::json_menu::JsonMenu;
|
|||||||
use pikl_core::menu::MenuRunner;
|
use pikl_core::menu::MenuRunner;
|
||||||
use pikl_core::output::{OutputAction, OutputItem};
|
use pikl_core::output::{OutputAction, OutputItem};
|
||||||
use pikl_core::script::action_fd::{self, ScriptAction, ShowAction};
|
use pikl_core::script::action_fd::{self, ScriptAction, ShowAction};
|
||||||
|
use serde_json::Value;
|
||||||
|
|
||||||
use handler::ShellHandlerHook;
|
use handler::ShellHandlerHook;
|
||||||
use hook::ShellExecHandler;
|
use hook::ShellExecHandler;
|
||||||
@@ -114,6 +115,10 @@ struct Cli {
|
|||||||
/// Format template for display text (e.g. "{label} - {sublabel}")
|
/// Format template for display text (e.g. "{label} - {sublabel}")
|
||||||
#[arg(long, value_name = "TEMPLATE")]
|
#[arg(long, value_name = "TEMPLATE")]
|
||||||
format: Option<String>,
|
format: Option<String>,
|
||||||
|
|
||||||
|
/// Wrap output in structured JSON with action metadata
|
||||||
|
#[arg(long)]
|
||||||
|
structured: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
@@ -397,19 +402,48 @@ async fn run_interactive(
|
|||||||
result
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Process the menu result: print selected item JSON to
|
/// Process the menu result: print output to stdout and
|
||||||
/// stdout, run hooks, or exit with the appropriate code.
|
/// exit with the appropriate code.
|
||||||
fn handle_result(result: Result<MenuResult, PiklError>, _cli: &Cli) {
|
fn handle_result(result: Result<MenuResult, PiklError>, cli: &Cli) {
|
||||||
|
let mut out = std::io::stdout().lock();
|
||||||
match result {
|
match result {
|
||||||
Ok(MenuResult::Selected { value, index }) => {
|
Ok(MenuResult::Selected { value, index }) => {
|
||||||
|
if cli.structured {
|
||||||
let output = OutputItem {
|
let output = OutputItem {
|
||||||
value: value.clone(),
|
value,
|
||||||
action: OutputAction::Select,
|
action: OutputAction::Select,
|
||||||
index,
|
index,
|
||||||
};
|
};
|
||||||
let _ = write_output_json(&mut std::io::stdout().lock(), &output);
|
let _ = write_output_json(&mut out, &output);
|
||||||
|
} else {
|
||||||
|
let _ = write_plain_value(&mut out, &value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(MenuResult::Quicklist { items }) => {
|
||||||
|
if cli.structured {
|
||||||
|
for (value, index) in items {
|
||||||
|
let output = OutputItem {
|
||||||
|
value,
|
||||||
|
action: OutputAction::Quicklist,
|
||||||
|
index,
|
||||||
|
};
|
||||||
|
let _ = write_output_json(&mut out, &output);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (value, _) in &items {
|
||||||
|
let _ = write_plain_value(&mut out, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Ok(MenuResult::Cancelled) => {
|
Ok(MenuResult::Cancelled) => {
|
||||||
|
if cli.structured {
|
||||||
|
let output = OutputItem {
|
||||||
|
value: Value::Null,
|
||||||
|
action: OutputAction::Cancel,
|
||||||
|
index: 0,
|
||||||
|
};
|
||||||
|
let _ = write_output_json(&mut out, &output);
|
||||||
|
}
|
||||||
std::process::exit(1);
|
std::process::exit(1);
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
@@ -425,6 +459,18 @@ fn write_output_json(writer: &mut impl Write, output: &OutputItem) -> Result<(),
|
|||||||
writeln!(writer, "{json}")
|
writeln!(writer, "{json}")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Write a plain value: strings without quotes, everything
|
||||||
|
/// else as JSON.
|
||||||
|
fn write_plain_value(writer: &mut impl Write, value: &Value) -> Result<(), std::io::Error> {
|
||||||
|
match value {
|
||||||
|
Value::String(s) => writeln!(writer, "{s}"),
|
||||||
|
_ => {
|
||||||
|
let json = serde_json::to_string(value).unwrap_or_default();
|
||||||
|
writeln!(writer, "{json}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Read items from stdin. If `timeout_secs` is non-zero,
|
/// Read items from stdin. If `timeout_secs` is non-zero,
|
||||||
/// spawn a thread and bail if it doesn't finish in time.
|
/// spawn a thread and bail if it doesn't finish in time.
|
||||||
/// A timeout of 0 means no timeout (blocking read).
|
/// A timeout of 0 means no timeout (blocking read).
|
||||||
|
|||||||
Reference in New Issue
Block a user