[Add] Proper User managment
This commit is contained in:
parent
430c12e139
commit
b38b68ad96
38 changed files with 4331 additions and 1065 deletions
|
|
@ -11,6 +11,36 @@ use ratatui::{
|
|||
widgets::Paragraph,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct HeaderItem {
|
||||
pub label: &'static str,
|
||||
pub intent: ButtonIntent,
|
||||
pub action: AppAction,
|
||||
}
|
||||
|
||||
pub const HEADER_ITEMS: &[HeaderItem] = &[
|
||||
HeaderItem {
|
||||
label: "Overview",
|
||||
intent: ButtonIntent::Primary,
|
||||
action: AppAction::OpenOverview,
|
||||
},
|
||||
HeaderItem {
|
||||
label: "Users",
|
||||
intent: ButtonIntent::Neutral,
|
||||
action: AppAction::OpenUsers,
|
||||
},
|
||||
HeaderItem {
|
||||
label: "Settings",
|
||||
intent: ButtonIntent::Neutral,
|
||||
action: AppAction::OpenSettings,
|
||||
},
|
||||
HeaderItem {
|
||||
label: "Quit",
|
||||
intent: ButtonIntent::Destructive,
|
||||
action: AppAction::Quit,
|
||||
},
|
||||
];
|
||||
|
||||
fn connection_badge(
|
||||
state: &IpcConnectionState,
|
||||
theme: &ResolvedTheme,
|
||||
|
|
@ -59,22 +89,14 @@ pub fn render_header(
|
|||
|
||||
let rows =
|
||||
Layout::vertical([Constraint::Percentage(50), Constraint::Percentage(50)]).split(area);
|
||||
let cells = Layout::horizontal([
|
||||
Constraint::Min(28),
|
||||
Constraint::Length(12),
|
||||
Constraint::Length(12),
|
||||
Constraint::Length(12),
|
||||
Constraint::Length(8),
|
||||
])
|
||||
.split(rows[0]);
|
||||
let cells2 = Layout::horizontal([
|
||||
Constraint::Min(28),
|
||||
Constraint::Length(12),
|
||||
Constraint::Length(12),
|
||||
Constraint::Length(12),
|
||||
Constraint::Length(8),
|
||||
])
|
||||
.split(rows[1]);
|
||||
let constraints = std::iter::once(Constraint::Min(28))
|
||||
.chain(
|
||||
HEADER_ITEMS
|
||||
.iter()
|
||||
.map(|item| Constraint::Length(if item.label == "Quit" { 8 } else { 12 })),
|
||||
)
|
||||
.collect::<Vec<_>>();
|
||||
let cells = Layout::horizontal(constraints.clone()).split(rows[0]);
|
||||
|
||||
let (ipc_label, ipc_style) = connection_badge(connection, theme);
|
||||
let (omikron_text, omikron_style) = omikron_badge(daemon, theme);
|
||||
|
|
@ -99,46 +121,15 @@ pub fn render_header(
|
|||
);
|
||||
hits.register(brand_area, AppAction::OpenMain);
|
||||
|
||||
for (index, (top, _bottom, label, intent, action)) in [
|
||||
(
|
||||
cells[1],
|
||||
cells2[1],
|
||||
"Overview",
|
||||
ButtonIntent::Primary,
|
||||
AppAction::OpenOverview,
|
||||
),
|
||||
(
|
||||
cells[2],
|
||||
cells2[2],
|
||||
"Users",
|
||||
ButtonIntent::Neutral,
|
||||
AppAction::OpenUsers,
|
||||
),
|
||||
(
|
||||
cells[3],
|
||||
cells2[3],
|
||||
"Settings",
|
||||
ButtonIntent::Neutral,
|
||||
AppAction::OpenSettings,
|
||||
),
|
||||
(
|
||||
cells[4],
|
||||
cells2[4],
|
||||
"Quit",
|
||||
ButtonIntent::Destructive,
|
||||
AppAction::Quit,
|
||||
),
|
||||
]
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
{
|
||||
for (index, item) in HEADER_ITEMS.iter().enumerate() {
|
||||
let top = cells[index + 1];
|
||||
let button_area = Rect {
|
||||
x: top.x,
|
||||
y: top.y,
|
||||
width: top.width,
|
||||
height: area.height,
|
||||
};
|
||||
let style = match (intent, focused_action == Some(index)) {
|
||||
let style = match (item.intent, focused_action == Some(index)) {
|
||||
(ButtonIntent::Primary, true) => theme.buttons.primary_focused,
|
||||
(ButtonIntent::Primary, false) => theme.buttons.primary,
|
||||
(ButtonIntent::Neutral, true) => theme.buttons.neutral_focused,
|
||||
|
|
@ -148,9 +139,9 @@ pub fn render_header(
|
|||
(ButtonIntent::Destructive, _) => theme.buttons.destructive,
|
||||
};
|
||||
let display_label = if focused_action == Some(index) {
|
||||
format!("› {label}")
|
||||
format!("› {}", item.label)
|
||||
} else {
|
||||
label.to_owned()
|
||||
item.label.to_owned()
|
||||
};
|
||||
frame.render_widget(
|
||||
Paragraph::new(vec![
|
||||
|
|
@ -159,6 +150,6 @@ pub fn render_header(
|
|||
]),
|
||||
button_area,
|
||||
);
|
||||
hits.register(button_area, action);
|
||||
hits.register(button_area, item.action);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
119
iota-cli/src/controls/menu.rs
Normal file
119
iota-cli/src/controls/menu.rs
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
use crossterm::event::KeyCode;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct MenuItem<T> {
|
||||
pub label: String,
|
||||
pub description: Option<String>,
|
||||
pub value: T,
|
||||
pub enabled: bool,
|
||||
pub disabled_reason: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct MenuState<T> {
|
||||
items: Vec<MenuItem<T>>,
|
||||
selected: Option<usize>,
|
||||
}
|
||||
|
||||
impl<T> MenuState<T> {
|
||||
pub fn new(items: Vec<MenuItem<T>>) -> Self {
|
||||
let selected = items.iter().position(|item| item.enabled);
|
||||
Self { items, selected }
|
||||
}
|
||||
|
||||
pub fn items(&self) -> &[MenuItem<T>] {
|
||||
&self.items
|
||||
}
|
||||
pub fn selected_index(&self) -> Option<usize> {
|
||||
self.selected
|
||||
}
|
||||
pub fn selected_item(&self) -> Option<&MenuItem<T>> {
|
||||
self.selected.and_then(|index| self.items.get(index))
|
||||
}
|
||||
|
||||
pub fn move_next(&mut self) {
|
||||
self.move_by(1);
|
||||
}
|
||||
pub fn move_previous(&mut self) {
|
||||
self.move_by(-1);
|
||||
}
|
||||
pub fn first(&mut self) {
|
||||
self.selected = self.items.iter().position(|item| item.enabled);
|
||||
}
|
||||
pub fn last(&mut self) {
|
||||
self.selected = self.items.iter().rposition(|item| item.enabled);
|
||||
}
|
||||
|
||||
pub fn handle_key(&mut self, key: KeyCode) -> bool {
|
||||
match key {
|
||||
KeyCode::Up => self.move_previous(),
|
||||
KeyCode::Down => self.move_next(),
|
||||
KeyCode::Home => self.first(),
|
||||
KeyCode::End => self.last(),
|
||||
_ => return false,
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
fn move_by(&mut self, delta: isize) {
|
||||
let Some(current) = self.selected else {
|
||||
return;
|
||||
};
|
||||
for offset in 1..=self.items.len() {
|
||||
let next = (current as isize + delta * offset as isize)
|
||||
.rem_euclid(self.items.len() as isize) as usize;
|
||||
if self.items[next].enabled {
|
||||
self.selected = Some(next);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn navigation_skips_disabled_items() {
|
||||
let mut menu = MenuState::new(vec![
|
||||
MenuItem {
|
||||
label: "A".into(),
|
||||
description: None,
|
||||
value: 1,
|
||||
enabled: true,
|
||||
disabled_reason: None,
|
||||
},
|
||||
MenuItem {
|
||||
label: "B".into(),
|
||||
description: None,
|
||||
value: 2,
|
||||
enabled: false,
|
||||
disabled_reason: Some("Unavailable".into()),
|
||||
},
|
||||
MenuItem {
|
||||
label: "C".into(),
|
||||
description: None,
|
||||
value: 3,
|
||||
enabled: true,
|
||||
disabled_reason: None,
|
||||
},
|
||||
]);
|
||||
menu.move_next();
|
||||
assert_eq!(menu.selected_index(), Some(2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_enabled_set_is_safe() {
|
||||
let mut menu = MenuState::new(vec![MenuItem {
|
||||
label: "A".into(),
|
||||
description: None,
|
||||
value: (),
|
||||
enabled: false,
|
||||
disabled_reason: None,
|
||||
}]);
|
||||
menu.move_next();
|
||||
menu.last();
|
||||
assert_eq!(menu.selected_index(), None);
|
||||
}
|
||||
}
|
||||
|
|
@ -4,7 +4,9 @@ pub mod checkbox_group;
|
|||
pub mod choice;
|
||||
pub mod dialog;
|
||||
pub mod header;
|
||||
pub mod menu;
|
||||
pub mod navigation;
|
||||
pub mod panel;
|
||||
pub mod radio_group;
|
||||
pub mod scroll;
|
||||
pub mod text_input;
|
||||
|
|
|
|||
99
iota-cli/src/controls/text_input.rs
Normal file
99
iota-cli/src/controls/text_input.rs
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
use crossterm::event::KeyCode;
|
||||
|
||||
#[derive(Debug, Clone, Default, PartialEq, Eq)]
|
||||
pub struct TextInput {
|
||||
value: String,
|
||||
cursor: usize,
|
||||
label: Option<String>,
|
||||
placeholder: Option<String>,
|
||||
validation: Option<String>,
|
||||
secret: bool,
|
||||
}
|
||||
|
||||
impl TextInput {
|
||||
pub fn new(label: impl Into<String>) -> Self {
|
||||
Self {
|
||||
label: Some(label.into()),
|
||||
..Self::default()
|
||||
}
|
||||
}
|
||||
pub fn value(&self) -> &str {
|
||||
&self.value
|
||||
}
|
||||
pub fn display_value(&self) -> String {
|
||||
if self.secret {
|
||||
"•".repeat(self.value.chars().count())
|
||||
} else {
|
||||
self.value.clone()
|
||||
}
|
||||
}
|
||||
pub fn set_placeholder(&mut self, value: impl Into<String>) {
|
||||
self.placeholder = Some(value.into());
|
||||
}
|
||||
pub fn placeholder(&self) -> Option<&str> {
|
||||
self.placeholder.as_deref()
|
||||
}
|
||||
pub fn label(&self) -> Option<&str> {
|
||||
self.label.as_deref()
|
||||
}
|
||||
pub fn validation(&self) -> Option<&str> {
|
||||
self.validation.as_deref()
|
||||
}
|
||||
pub fn set_validation(&mut self, value: Option<String>) {
|
||||
self.validation = value;
|
||||
}
|
||||
pub fn set_secret(&mut self, secret: bool) {
|
||||
self.secret = secret;
|
||||
}
|
||||
pub fn handle_key(&mut self, key: KeyCode) -> bool {
|
||||
match key {
|
||||
KeyCode::Backspace => {
|
||||
if self.cursor > 0 {
|
||||
let start = self.value[..self.cursor]
|
||||
.char_indices()
|
||||
.last()
|
||||
.map(|(index, _)| index)
|
||||
.unwrap_or(0);
|
||||
self.value.drain(start..self.cursor);
|
||||
self.cursor = start;
|
||||
}
|
||||
}
|
||||
KeyCode::Delete => {
|
||||
if self.cursor < self.value.len() {
|
||||
let end = self.value[self.cursor..]
|
||||
.char_indices()
|
||||
.nth(1)
|
||||
.map(|(index, _)| self.cursor + index)
|
||||
.unwrap_or(self.value.len());
|
||||
self.value.drain(self.cursor..end);
|
||||
}
|
||||
}
|
||||
KeyCode::Left => {
|
||||
if self.cursor > 0 {
|
||||
self.cursor = self.value[..self.cursor]
|
||||
.char_indices()
|
||||
.last()
|
||||
.map(|(index, _)| index)
|
||||
.unwrap_or(0);
|
||||
}
|
||||
}
|
||||
KeyCode::Right => {
|
||||
if self.cursor < self.value.len() {
|
||||
self.cursor = self.value[self.cursor..]
|
||||
.char_indices()
|
||||
.nth(1)
|
||||
.map(|(index, _)| self.cursor + index)
|
||||
.unwrap_or(self.value.len());
|
||||
}
|
||||
}
|
||||
KeyCode::Home => self.cursor = 0,
|
||||
KeyCode::End => self.cursor = self.value.len(),
|
||||
KeyCode::Char(character) => {
|
||||
self.value.insert(self.cursor, character);
|
||||
self.cursor += character.len_utf8();
|
||||
}
|
||||
_ => return false,
|
||||
}
|
||||
true
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue