[WIP] Daemon & CLI

This commit is contained in:
Alex-Emmet 2026-07-23 23:13:02 +02:00
commit 8b158108bb
100 changed files with 6519 additions and 1596 deletions

View file

@ -0,0 +1,15 @@
use iota_cli::controls::button::{button_minimum_width, horizontal_button_widths};
#[test]
fn width_allocation_handles_exact_spare_and_insufficient_space() {
assert_eq!(horizontal_button_widths(7, &[3, 4]), Some(vec![3, 4]));
assert_eq!(horizontal_button_widths(10, &[3, 4]), Some(vec![5, 5]));
assert_eq!(horizontal_button_widths(6, &[3, 4]), None);
assert_eq!(horizontal_button_widths(10, &[]), Some(Vec::new()));
}
#[test]
fn minimum_width_uses_terminal_columns() {
assert_eq!(button_minimum_width("é"), 3);
assert_eq!(button_minimum_width(""), 4);
}

View file

@ -0,0 +1,71 @@
use iota_cli::{
controls::choice::{ChoiceKind, ChoiceVisualState, render_choice_line},
theme::{ThemeName, resolve},
};
use ratatui::style::{Color, Modifier};
#[test]
fn ansi_checkbox_matches_the_existing_focused_and_disabled_styles() {
let theme = resolve(ThemeName::Ansi);
let line = render_choice_line(
"Terms",
ChoiceKind::Checkbox,
ChoiceVisualState {
selected: false,
focused: true,
enabled: true,
},
&theme,
);
assert_eq!(
line.spans
.iter()
.map(|span| span.content.as_ref())
.collect::<String>(),
"[ ] Terms"
);
assert_eq!(line.spans[1].style.fg, Some(Color::Yellow));
assert!(line.spans[1].style.add_modifier.contains(Modifier::BOLD));
let disabled = render_choice_line(
"Terms",
ChoiceKind::Checkbox,
ChoiceVisualState {
selected: false,
focused: true,
enabled: false,
},
&theme,
);
assert_eq!(disabled.spans[1].style.fg, Some(Color::DarkGray));
assert_eq!(disabled.spans[3].style.fg, Some(Color::Red));
}
#[test]
fn colourless_themes_keep_state_and_focus_visible() {
for name in [ThemeName::Monospace, ThemeName::Binary] {
let theme = resolve(name);
let line = render_choice_line(
"Mode",
ChoiceKind::Radio,
ChoiceVisualState {
selected: true,
focused: true,
enabled: true,
},
&theme,
);
assert_eq!(
line.spans
.iter()
.map(|span| span.content.as_ref())
.collect::<String>(),
"> (x) Mode <"
);
assert!(
line.spans
.iter()
.all(|span| span.style.fg.is_none() && span.style.bg.is_none())
);
}
}

View file

@ -0,0 +1,109 @@
use iota_cli::controls::{
checkbox_group::{CheckboxChange, CheckboxGroup, CheckboxItem},
navigation::DisabledFocusPolicy,
radio_group::{DisabledSelectionPolicy, RadioChange, RadioGroup, RadioGroupError, RadioItem},
};
fn checkbox(value: u8, enabled: bool) -> CheckboxItem<u8> {
CheckboxItem {
value,
label: value.to_string(),
description: None,
enabled,
disabled_reason: None,
}
}
fn radio(value: u8, enabled: bool) -> RadioItem<u8> {
RadioItem {
value,
label: value.to_string(),
description: None,
enabled,
disabled_reason: None,
}
}
#[test]
fn checkbox_selection_and_disabled_focus_are_independent() {
let mut group = CheckboxGroup::new(
vec![checkbox(1, true), checkbox(2, false), checkbox(3, true)],
[1, 99],
)
.unwrap();
assert_eq!(
group.selected().iter().copied().collect::<Vec<_>>(),
vec![1]
);
assert_eq!(group.toggle_focused(), CheckboxChange::Deselected(1));
group.focus_next();
assert_eq!(group.focused_item().unwrap().value, 3);
group.set_focus_policy(DisabledFocusPolicy::Include);
group.focus_previous();
assert_eq!(group.focused_item().unwrap().value, 2);
assert_eq!(group.toggle_focused(), CheckboxChange::IgnoredDisabled(2));
}
#[test]
fn checkbox_non_wrapping_navigation_stops_at_the_edge() {
let mut group = CheckboxGroup::new(vec![checkbox(1, true), checkbox(2, true)], []).unwrap();
group.set_wrap_navigation(false);
group.focus_previous();
assert_eq!(group.focused_item().unwrap().value, 1);
}
#[test]
fn radio_validates_default_and_preserves_one_selection() {
assert!(matches!(
RadioGroup::new(Vec::<RadioItem<u8>>::new(), None, 1),
Err(RadioGroupError::Empty)
));
assert!(matches!(
RadioGroup::new(vec![radio(1, true)], None, 2),
Err(RadioGroupError::DefaultMissing)
));
assert!(matches!(
RadioGroup::new(vec![radio(1, false)], None, 1),
Err(RadioGroupError::DefaultDisabled)
));
let mut group = RadioGroup::new(vec![radio(1, true), radio(2, true)], Some(2), 1).unwrap();
assert_eq!(group.selected(), &2);
group.focus_next();
assert_eq!(group.selected(), &2);
assert_eq!(group.select_focused(), RadioChange::Unchanged(2));
group.focus_previous();
assert_eq!(
group.select_focused(),
RadioChange::Changed {
previous: 2,
selected: 1
}
);
assert_eq!(group.selected(), &1);
}
#[test]
fn groups_initially_focus_the_first_enabled_item() {
let checkboxes = CheckboxGroup::new(vec![checkbox(1, false), checkbox(2, true)], []).unwrap();
assert_eq!(checkboxes.focused_item().unwrap().value, 2);
let radios = RadioGroup::new(vec![radio(1, false), radio(2, true)], None, 2).unwrap();
assert_eq!(radios.focused_item().value, 2);
}
#[test]
fn disabling_a_selected_radio_obeys_the_configured_policy() {
let mut group = RadioGroup::new(vec![radio(1, true), radio(2, true)], Some(2), 1).unwrap();
group.set_enabled(&2, false).unwrap();
assert_eq!(group.selected(), &1);
group.set_enabled(&2, true).unwrap();
group.focus_next();
group.select_focused();
group.set_disabled_selection_policy(DisabledSelectionPolicy::ReturnError);
assert_eq!(
group.set_enabled(&2, false),
Err(RadioGroupError::SelectedItemDisabled)
);
assert_eq!(group.selected(), &2);
}

View file

@ -0,0 +1,45 @@
use iota_cli::layout::fit::{
FitLevel, RequiredSize, centered_rect, inset_checked, reserve_vertical, select_fit_level,
};
use ratatui::layout::Rect;
#[test]
fn selects_fit_by_both_dimensions() {
let preferred = RequiredSize {
width: 80,
height: 20,
};
let compact = RequiredSize {
width: 50,
height: 12,
};
assert_eq!(
select_fit_level(Rect::new(0, 0, 80, 20), preferred, compact),
FitLevel::Preferred
);
assert_eq!(
select_fit_level(Rect::new(0, 0, 50, 12), preferred, compact),
FitLevel::Compact
);
assert_eq!(
select_fit_level(Rect::new(0, 0, 80, 11), preferred, compact),
FitLevel::Fallback
);
}
#[test]
fn rectangle_helpers_do_not_underflow() {
let zero = Rect::new(4, 5, 0, 0);
assert_eq!(
centered_rect(
zero,
RequiredSize {
width: 10,
height: 10
}
),
zero
);
assert_eq!(reserve_vertical(zero, 1, 0), None);
assert_eq!(inset_checked(zero, 1, 1), None);
}