Implement adaptive client list

This commit is contained in:
2026-10-28 23:12:22 +02:00
parent d49721ff8e
commit 4960c65f65
8 changed files with 1794 additions and 256 deletions
+112 -29
View File
@@ -869,15 +869,7 @@ fn handle_navigation_key(code: KeyCode, state: &mut UiState) -> bool {
true
}
KeyCode::Enter if state.screen == Screen::ClientList => {
let peer_index = state.selected_peer_index.or_else(|| {
state
.view
.as_ref()
.and_then(|view| view.clients.first().map(|client| client.peer_index))
});
if let Some(peer_index) = peer_index {
let _ = state.select_peer_index(peer_index);
}
let _ = state.open_selected_record();
state.scroll_offset = 0;
true
}
@@ -891,31 +883,17 @@ fn handle_navigation_key(code: KeyCode, state: &mut UiState) -> bool {
fn move_selection(state: &mut UiState, down: bool) {
let Some(view) = &state.view else { return };
if view.clients.is_empty() {
let records = view.visible_records();
if records.is_empty() {
return;
}
let current = state
.selected_peer_index
.and_then(|selected| {
view.clients
.iter()
.position(|row| row.peer_index == selected)
})
.or_else(|| {
state.selected_identity.and_then(|identity| {
view.clients
.iter()
.position(|row| row.identity == Some(identity))
})
})
.unwrap_or(0);
let current = state.selected_visible_index(&records).unwrap_or(0);
let next = if down {
(current + 1) % view.clients.len()
(current + 1) % records.len()
} else {
current.checked_sub(1).unwrap_or(view.clients.len() - 1)
current.checked_sub(1).unwrap_or(records.len() - 1)
};
let peer_index = view.clients[next].peer_index;
let _ = state.highlight_peer_index(peer_index);
let _ = state.select_visible_index(next);
}
#[cfg(test)]
@@ -996,6 +974,7 @@ mod tests {
unsupported,
],
client_states: Default::default(),
saved_peer_count: Some(0),
document_issues: Vec::new(),
applied: AppliedState::Unknown,
reserved_addresses: Default::default(),
@@ -1064,6 +1043,109 @@ mod tests {
assert!(!state.selected_client_mutable());
}
#[test]
fn keyboard_navigation_reaches_draft_removed_rows_without_authorizing_them() {
let removed_key = crate::domain::PublicKey::from_bytes([19; 32]);
let mut view = navigation_view();
view.client_states.insert(
removed_key,
crate::tui::state::ClientStateRow {
saved: Some(
crate::domain::Client::new(
removed_key,
"removed",
crate::domain::ClientAddresses {
ipv4: "10.0.0.9".parse().unwrap(),
ipv6: None,
},
)
.unwrap(),
),
draft: None,
},
);
let mut state = UiState {
view: Some(view),
..UiState::default()
};
assert!(handle_navigation_key(KeyCode::Down, &mut state));
assert_eq!(state.selected_peer_index, Some(9));
assert!(handle_navigation_key(KeyCode::Down, &mut state));
assert_eq!(state.selected_peer_index, None);
assert_eq!(state.selected_identity, Some(removed_key));
assert!(!state.selected_client_mutable());
assert!(handle_navigation_key(KeyCode::Enter, &mut state));
assert_eq!(state.screen, Screen::ClientDetails);
assert!(matches!(
state.selected_record(),
Some(crate::tui::state::VisibleRecord::DraftRemoved { identity, .. })
if identity == removed_key
));
assert!(handle_navigation_key(KeyCode::Esc, &mut state));
assert_eq!(state.screen, Screen::ClientList);
assert_eq!(state.selected_identity, Some(removed_key));
let mut empty = UiState {
view: Some(SessionView {
generation: 0,
clients: Vec::new(),
client_states: Default::default(),
saved_peer_count: Some(0),
document_issues: vec![crate::wireguard::DocumentIssue::MalformedSectionHeader {
line: 1,
}],
applied: AppliedState::Unknown,
reserved_addresses: Default::default(),
statistics: RuntimeObservation::unknown(),
has_draft: false,
vault_initialized: true,
}),
..UiState::default()
};
assert!(handle_navigation_key(KeyCode::Enter, &mut empty));
assert_eq!(empty.screen, Screen::ClientList);
assert!(empty.selected_record().is_none());
}
#[test]
fn duplicate_public_keys_remain_inspectable_by_peer_position_but_not_mutable() {
let key = crate::domain::PublicKey::from_bytes([20; 32]);
let mut first = navigation_view().clients[0].clone();
first.peer_index = 3;
first.identity = Some(key);
first.mutable_identity = Some(key);
first.label = "first duplicate".to_owned();
let mut second = first.clone();
second.peer_index = 7;
second.label = "second duplicate".to_owned();
let mut state = UiState {
view: Some(SessionView {
generation: 0,
clients: vec![first, second],
client_states: Default::default(),
saved_peer_count: Some(2),
document_issues: Vec::new(),
applied: AppliedState::Unknown,
reserved_addresses: Default::default(),
statistics: RuntimeObservation::unknown(),
has_draft: false,
vault_initialized: true,
}),
..UiState::default()
};
assert!(handle_navigation_key(KeyCode::Down, &mut state));
assert_eq!(state.selected_peer_index, Some(7));
assert_eq!(state.selected_identity, Some(key));
assert!(!state.selected_client_mutable());
assert!(handle_navigation_key(KeyCode::Enter, &mut state));
assert!(matches!(
state.selected_record(),
Some(crate::tui::state::VisibleRecord::Peer(client))
if client.peer_index == 7 && client.label == "second duplicate"
));
assert!(!state.selected_client_mutable());
}
#[test]
fn issues_screen_hides_valid_address_shape_limits_but_keeps_genuine_errors() {
let mut view = navigation_view();
@@ -1275,6 +1357,7 @@ mod tests {
generation: 1,
clients: Vec::new(),
client_states: Default::default(),
saved_peer_count: Some(0),
document_issues: Vec::new(),
applied: AppliedState::Unknown,
reserved_addresses: Default::default(),