Implement adaptive client list
This commit is contained in:
+112
-29
@@ -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(),
|
||||
|
||||
Reference in New Issue
Block a user