Fixed status window
This commit is contained in:
@@ -136,6 +136,9 @@ pub fn handle_key(state: &mut AppState, runtime: &mut Runtime, key: KeyEvent) {
|
|||||||
KeyCode::Esc | KeyCode::Enter | KeyCode::Char('q') => {}
|
KeyCode::Esc | KeyCode::Enter | KeyCode::Char('q') => {}
|
||||||
_ => state.popup = Some(Popup::FedText { title, text }),
|
_ => state.popup = Some(Popup::FedText { title, text }),
|
||||||
},
|
},
|
||||||
|
Popup::FederationStatusDetails { scroll } => {
|
||||||
|
handle_federation_status_details(state, scroll, key);
|
||||||
|
}
|
||||||
Popup::DevicePairing {
|
Popup::DevicePairing {
|
||||||
request_id,
|
request_id,
|
||||||
device_id,
|
device_id,
|
||||||
@@ -163,6 +166,20 @@ pub fn handle_key(state: &mut AppState, runtime: &mut Runtime, key: KeyEvent) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn handle_federation_status_details(state: &mut AppState, scroll: usize, key: KeyEvent) {
|
||||||
|
let next_scroll = match key.code {
|
||||||
|
KeyCode::Esc | KeyCode::Enter | KeyCode::Char('q') => return,
|
||||||
|
KeyCode::Up | KeyCode::Char('k') => scroll.saturating_sub(1),
|
||||||
|
KeyCode::Down | KeyCode::Char('j') => scroll + 1,
|
||||||
|
KeyCode::PageUp => scroll.saturating_sub(8),
|
||||||
|
KeyCode::PageDown => scroll + 8,
|
||||||
|
_ => scroll,
|
||||||
|
};
|
||||||
|
state.popup = Some(Popup::FederationStatusDetails {
|
||||||
|
scroll: next_scroll,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
fn handle_connected_devices(
|
fn handle_connected_devices(
|
||||||
state: &mut AppState,
|
state: &mut AppState,
|
||||||
runtime: &mut Runtime,
|
runtime: &mut Runtime,
|
||||||
|
|||||||
@@ -544,6 +544,8 @@ pub enum Popup {
|
|||||||
ConfirmDeviceRevoke { device_id: String, name: String },
|
ConfirmDeviceRevoke { device_id: String, name: String },
|
||||||
/// Connected playback devices and their current role/status.
|
/// Connected playback devices and their current role/status.
|
||||||
ConnectedDevices { cursor: usize },
|
ConnectedDevices { cursor: usize },
|
||||||
|
/// Full federation, transport and device status details.
|
||||||
|
FederationStatusDetails { scroll: usize },
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
@@ -593,6 +595,7 @@ impl FedRow {
|
|||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
pub enum SettingsRow {
|
pub enum SettingsRow {
|
||||||
Federation(FedRow),
|
Federation(FedRow),
|
||||||
|
StatusDetails,
|
||||||
DeviceName,
|
DeviceName,
|
||||||
DeviceInvite,
|
DeviceInvite,
|
||||||
DeviceConnect,
|
DeviceConnect,
|
||||||
@@ -751,6 +754,7 @@ pub fn settings_rows(state: &AppState) -> Vec<SettingsRow> {
|
|||||||
if !state.visualizer.scripts.is_empty() {
|
if !state.visualizer.scripts.is_empty() {
|
||||||
rows.push(SettingsRow::VisualizationEdit);
|
rows.push(SettingsRow::VisualizationEdit);
|
||||||
}
|
}
|
||||||
|
rows.push(SettingsRow::StatusDetails);
|
||||||
rows
|
rows
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+32
-22
@@ -2452,6 +2452,10 @@ fn federation_select(state: &mut AppState) -> Option<Effect> {
|
|||||||
});
|
});
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
SettingsRow::StatusDetails => {
|
||||||
|
state.popup = Some(Popup::FederationStatusDetails { scroll: 0 });
|
||||||
|
None
|
||||||
|
}
|
||||||
SettingsRow::DeviceName => {
|
SettingsRow::DeviceName => {
|
||||||
if !require_connected_devices_enabled(state) {
|
if !require_connected_devices_enabled(state) {
|
||||||
return None;
|
return None;
|
||||||
@@ -2851,8 +2855,9 @@ mod tests {
|
|||||||
cover_path: None,
|
cover_path: None,
|
||||||
track_count: 1,
|
track_count: 1,
|
||||||
};
|
};
|
||||||
// columns = 3 in tests (no tty → 80 wide): albums rows [0,1,2],[3],
|
let columns = grid_columns();
|
||||||
// compilations row [4,5].
|
// The terminal size can be visible to tests. Build enough albums to
|
||||||
|
// force a short second album row for whichever width this run has.
|
||||||
let detail = ArtistDetail {
|
let detail = ArtistDetail {
|
||||||
id: 1,
|
id: 1,
|
||||||
name: "a".into(),
|
name: "a".into(),
|
||||||
@@ -2861,45 +2866,50 @@ mod tests {
|
|||||||
total_play_count: 0,
|
total_play_count: 0,
|
||||||
top_tracks: vec![],
|
top_tracks: vec![],
|
||||||
featured_tracks: vec![],
|
featured_tracks: vec![],
|
||||||
releases: vec![
|
releases: (0..=columns)
|
||||||
release(10, "album"),
|
.map(|index| release(10 + index as i64, "album"))
|
||||||
release(11, "album"),
|
.chain((0..2).map(|index| release(100 + index, "compilation")))
|
||||||
release(12, "album"),
|
.collect(),
|
||||||
release(13, "album"),
|
|
||||||
release(14, "compilation"),
|
|
||||||
release(15, "compilation"),
|
|
||||||
],
|
|
||||||
};
|
};
|
||||||
let mut state = AppState::default();
|
let mut state = AppState::default();
|
||||||
state.artist_views.insert(1, Loadable::Ready(detail));
|
state.artist_views.insert(1, Loadable::Ready(detail));
|
||||||
state
|
state.global.stack.push(GlobalView::Artist {
|
||||||
.global
|
id: 1,
|
||||||
.stack
|
cursor: columns + 1,
|
||||||
.push(GlobalView::Artist { id: 1, cursor: 4 });
|
});
|
||||||
|
|
||||||
// Up from the first compilation lands on the album row directly
|
// Up from the first compilation lands on the album row directly
|
||||||
// above (position 3), not three flat items back.
|
// above, not one flat grid-width jump back.
|
||||||
update(&mut state, Action::MoveUp);
|
update(&mut state, Action::MoveUp);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
state.global.stack.last(),
|
state.global.stack.last(),
|
||||||
Some(&GlobalView::Artist { id: 1, cursor: 3 })
|
Some(&GlobalView::Artist {
|
||||||
|
id: 1,
|
||||||
|
cursor: columns
|
||||||
|
})
|
||||||
);
|
);
|
||||||
// And back down returns to the compilation row, same column.
|
// And back down returns to the compilation row, same column.
|
||||||
update(&mut state, Action::MoveDown);
|
update(&mut state, Action::MoveDown);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
state.global.stack.last(),
|
state.global.stack.last(),
|
||||||
Some(&GlobalView::Artist { id: 1, cursor: 4 })
|
Some(&GlobalView::Artist {
|
||||||
|
id: 1,
|
||||||
|
cursor: columns + 1
|
||||||
|
})
|
||||||
);
|
);
|
||||||
// Up from the second compilation clamps to the single tile above.
|
// Up from the second compilation clamps to the single tile above.
|
||||||
state.global.stack.pop();
|
state.global.stack.pop();
|
||||||
state
|
state.global.stack.push(GlobalView::Artist {
|
||||||
.global
|
id: 1,
|
||||||
.stack
|
cursor: columns + 2,
|
||||||
.push(GlobalView::Artist { id: 1, cursor: 5 });
|
});
|
||||||
update(&mut state, Action::MoveUp);
|
update(&mut state, Action::MoveUp);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
state.global.stack.last(),
|
state.global.stack.last(),
|
||||||
Some(&GlobalView::Artist { id: 1, cursor: 3 })
|
Some(&GlobalView::Artist {
|
||||||
|
id: 1,
|
||||||
|
cursor: columns
|
||||||
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+344
-21
@@ -32,7 +32,7 @@ pub fn draw(frame: &mut Frame, area: Rect, state: &AppState) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let rows_height =
|
let rows_height =
|
||||||
(settings_rows(state).len() + 5 + device_presence_sections(state).len()) as u16;
|
(settings_rows(state).len() + 6 + device_presence_sections(state).len()) as u16;
|
||||||
let [rows_area, _, status_area] = Layout::vertical([
|
let [rows_area, _, status_area] = Layout::vertical([
|
||||||
Constraint::Length(rows_height.min(inner.height)),
|
Constraint::Length(rows_height.min(inner.height)),
|
||||||
Constraint::Length(1),
|
Constraint::Length(1),
|
||||||
@@ -285,7 +285,19 @@ fn draw_settings_rows(frame: &mut Frame, area: Rect, state: &AppState) {
|
|||||||
"Edit selected visualization",
|
"Edit selected visualization",
|
||||||
"↵".to_string(),
|
"↵".to_string(),
|
||||||
);
|
);
|
||||||
|
cursor += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
y = y.saturating_add(1);
|
||||||
|
draw_row(
|
||||||
|
frame,
|
||||||
|
area,
|
||||||
|
&mut y,
|
||||||
|
cursor,
|
||||||
|
state.settings_cursor,
|
||||||
|
"Full status details",
|
||||||
|
"enter".to_string(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn draw_section(frame: &mut Frame, area: Rect, y: &mut u16, title: &'static str) {
|
fn draw_section(frame: &mut Frame, area: Rect, y: &mut u16, title: &'static str) {
|
||||||
@@ -481,7 +493,7 @@ fn push_transport_status(lines: &mut Vec<Line<'static>>, status: &crate::federat
|
|||||||
"Last peer",
|
"Last peer",
|
||||||
format!(
|
format!(
|
||||||
"{} · paths d/r/c/open {}/{}/{}/{}",
|
"{} · paths d/r/c/open {}/{}/{}/{}",
|
||||||
short_id(&sample.peer_id),
|
sample.peer_id,
|
||||||
sample.direct_paths,
|
sample.direct_paths,
|
||||||
sample.relay_paths,
|
sample.relay_paths,
|
||||||
sample.custom_paths,
|
sample.custom_paths,
|
||||||
@@ -501,7 +513,7 @@ fn push_transport_status(lines: &mut Vec<Line<'static>>, status: &crate::federat
|
|||||||
),
|
),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
for sample in transport.last.iter().take(3) {
|
for sample in &transport.last {
|
||||||
lines.push(Line::from(vec![
|
lines.push(Line::from(vec![
|
||||||
Span::styled(format!("{:<14}", sample.at), theme::dim()),
|
Span::styled(format!("{:<14}", sample.at), theme::dim()),
|
||||||
Span::raw(format!(
|
Span::raw(format!(
|
||||||
@@ -518,6 +530,267 @@ fn push_transport_status(lines: &mut Vec<Line<'static>>, status: &crate::federat
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn draw_status(frame: &mut Frame, area: Rect, state: &AppState) {
|
fn draw_status(frame: &mut Frame, area: Rect, state: &AppState) {
|
||||||
|
if area.width == 0 || area.height == 0 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if area.height < 18 || area.width < 36 {
|
||||||
|
frame.render_widget(
|
||||||
|
Paragraph::new(compact_status_lines(state))
|
||||||
|
.wrap(ratatui::widgets::Wrap { trim: false }),
|
||||||
|
area,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let [node_area, _, transport_area, _, devices_area, _] = Layout::vertical([
|
||||||
|
Constraint::Length(5),
|
||||||
|
Constraint::Length(1),
|
||||||
|
Constraint::Length(5),
|
||||||
|
Constraint::Length(1),
|
||||||
|
Constraint::Length(5),
|
||||||
|
Constraint::Min(0),
|
||||||
|
])
|
||||||
|
.areas(area);
|
||||||
|
|
||||||
|
draw_summary_card(frame, node_area, " Status ", node_summary_lines(state));
|
||||||
|
draw_summary_card(
|
||||||
|
frame,
|
||||||
|
transport_area,
|
||||||
|
" Iroh Transport ",
|
||||||
|
transport_summary_lines(state),
|
||||||
|
);
|
||||||
|
draw_summary_card(
|
||||||
|
frame,
|
||||||
|
devices_area,
|
||||||
|
" Connected Devices ",
|
||||||
|
device_summary_lines(state),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn compact_status_lines(state: &AppState) -> Vec<Line<'static>> {
|
||||||
|
let mut lines = Vec::new();
|
||||||
|
lines.push(Line::styled("Status", theme::header()));
|
||||||
|
lines.extend(node_summary_lines(state).into_iter().take(2));
|
||||||
|
lines.push(Line::default());
|
||||||
|
lines.push(Line::styled("Iroh Transport", theme::header()));
|
||||||
|
lines.extend(transport_summary_lines(state).into_iter().take(2));
|
||||||
|
lines.push(Line::default());
|
||||||
|
lines.push(Line::styled("Connected Devices", theme::header()));
|
||||||
|
lines.extend(device_summary_lines(state).into_iter().take(2));
|
||||||
|
lines
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_summary_card(
|
||||||
|
frame: &mut Frame,
|
||||||
|
area: Rect,
|
||||||
|
title: &'static str,
|
||||||
|
lines: Vec<Line<'static>>,
|
||||||
|
) {
|
||||||
|
if area.width == 0 || area.height == 0 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let block = Block::bordered()
|
||||||
|
.title(title)
|
||||||
|
.title_style(theme::header())
|
||||||
|
.border_style(theme::dim());
|
||||||
|
let inner = block.inner(area);
|
||||||
|
frame.render_widget(block, area);
|
||||||
|
frame.render_widget(Paragraph::new(lines), inner);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn summary_line(label: &'static str, value: String) -> Line<'static> {
|
||||||
|
Line::from(vec![
|
||||||
|
Span::styled(format!("{label:<10}"), theme::dim()),
|
||||||
|
Span::raw(value),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
fn node_summary_lines(state: &AppState) -> Vec<Line<'static>> {
|
||||||
|
match &state.federation.status {
|
||||||
|
None => vec![
|
||||||
|
summary_line("Node", "loading".to_string()),
|
||||||
|
summary_line("Network", "unknown".to_string()),
|
||||||
|
summary_line("Peers", "waiting for status".to_string()),
|
||||||
|
],
|
||||||
|
Some(status) if !status.running => {
|
||||||
|
let network = if state.federation.settings.network_id.trim().is_empty() {
|
||||||
|
"network id not set".to_string()
|
||||||
|
} else {
|
||||||
|
state.federation.settings.network_id.clone()
|
||||||
|
};
|
||||||
|
vec![
|
||||||
|
summary_line("Node", "stopped".to_string()),
|
||||||
|
summary_line("Network", network),
|
||||||
|
summary_line(
|
||||||
|
"Problem",
|
||||||
|
status
|
||||||
|
.last_error
|
||||||
|
.as_deref()
|
||||||
|
.map(first_line)
|
||||||
|
.unwrap_or_else(|| "disabled".to_string()),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
Some(status) => vec![
|
||||||
|
summary_line("Node", format!("running on {}", status.network)),
|
||||||
|
summary_line(
|
||||||
|
"Peers",
|
||||||
|
format!(
|
||||||
|
"{} connected / {} contacts",
|
||||||
|
status.connected_peers.len(),
|
||||||
|
status.known_contacts
|
||||||
|
),
|
||||||
|
),
|
||||||
|
summary_line(
|
||||||
|
"Library",
|
||||||
|
format!(
|
||||||
|
"{} published / {} DHT records",
|
||||||
|
status.published_items,
|
||||||
|
status
|
||||||
|
.stored_dht_records
|
||||||
|
.map(|count| count.to_string())
|
||||||
|
.unwrap_or_else(|| "n/a".to_string())
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn transport_summary_lines(state: &AppState) -> Vec<Line<'static>> {
|
||||||
|
let Some(status) = &state.federation.status else {
|
||||||
|
return vec![
|
||||||
|
summary_line("Traffic", "loading".to_string()),
|
||||||
|
summary_line("Streams", "loading".to_string()),
|
||||||
|
summary_line("Path", "loading".to_string()),
|
||||||
|
];
|
||||||
|
};
|
||||||
|
let transport = &status.transport;
|
||||||
|
if transport.total_samples == 0 {
|
||||||
|
return vec![
|
||||||
|
summary_line("Traffic", "no samples yet".to_string()),
|
||||||
|
summary_line("Streams", format!("{} active", transport.active_streams)),
|
||||||
|
summary_line("Path", "waiting for a stream".to_string()),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
let runtime_total = transport
|
||||||
|
.runtime_tx_bytes
|
||||||
|
.saturating_add(transport.runtime_rx_bytes);
|
||||||
|
let last_path = transport
|
||||||
|
.last
|
||||||
|
.first()
|
||||||
|
.map(|sample| {
|
||||||
|
format!(
|
||||||
|
"{} / {} / {}",
|
||||||
|
sample.protocol,
|
||||||
|
sample.selected_path,
|
||||||
|
rtt_label(sample.selected_rtt_ms)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.unwrap_or_else(|| "no recent stream".to_string());
|
||||||
|
vec![
|
||||||
|
summary_line(
|
||||||
|
"Traffic",
|
||||||
|
format!(
|
||||||
|
"{} (tx {} / rx {})",
|
||||||
|
short_bytes_label(runtime_total),
|
||||||
|
short_bytes_label(transport.runtime_tx_bytes),
|
||||||
|
short_bytes_label(transport.runtime_rx_bytes)
|
||||||
|
),
|
||||||
|
),
|
||||||
|
summary_line(
|
||||||
|
"Streams",
|
||||||
|
format!(
|
||||||
|
"{} active / {} samples",
|
||||||
|
transport.active_streams, transport.total_samples
|
||||||
|
),
|
||||||
|
),
|
||||||
|
summary_line(
|
||||||
|
"Paths",
|
||||||
|
format!(
|
||||||
|
"direct {} / relay {} / custom {}",
|
||||||
|
transport.direct_samples, transport.relay_samples, transport.custom_samples
|
||||||
|
),
|
||||||
|
),
|
||||||
|
summary_line("Last", last_path),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn device_summary_lines(state: &AppState) -> Vec<Line<'static>> {
|
||||||
|
if !state.connected_devices_enabled() {
|
||||||
|
return vec![
|
||||||
|
summary_line("Sync", "disabled".to_string()),
|
||||||
|
summary_line("This", state.device_playback.self_device_name.clone()),
|
||||||
|
summary_line("Devices", "enable federation first".to_string()),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
let Some(status) = &state.federation.devices else {
|
||||||
|
return vec![
|
||||||
|
summary_line("Sync", "loading".to_string()),
|
||||||
|
summary_line("This", state.device_playback.self_device_name.clone()),
|
||||||
|
summary_line("Devices", "waiting for device status".to_string()),
|
||||||
|
];
|
||||||
|
};
|
||||||
|
let (online, offline, revoked) = device_presence_counts(state);
|
||||||
|
let this_name = if status.this_device_name.trim().is_empty() {
|
||||||
|
short_id(&status.this_device_id)
|
||||||
|
} else {
|
||||||
|
format!(
|
||||||
|
"{} / {}",
|
||||||
|
status.this_device_name,
|
||||||
|
short_id(&status.this_device_id)
|
||||||
|
)
|
||||||
|
};
|
||||||
|
vec![
|
||||||
|
summary_line("This", this_name),
|
||||||
|
summary_line(
|
||||||
|
"Devices",
|
||||||
|
format!(
|
||||||
|
"{} active / {} online / {} pending",
|
||||||
|
status.active_devices, online, status.pending_requests
|
||||||
|
),
|
||||||
|
),
|
||||||
|
summary_line(
|
||||||
|
"Sync",
|
||||||
|
format!(
|
||||||
|
"{} outbox / last {}",
|
||||||
|
status.outbox_ops,
|
||||||
|
status
|
||||||
|
.last_sync
|
||||||
|
.clone()
|
||||||
|
.unwrap_or_else(|| "not yet".to_string())
|
||||||
|
),
|
||||||
|
),
|
||||||
|
summary_line(
|
||||||
|
"Other",
|
||||||
|
format!("{} offline / {} revoked", offline, revoked),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn device_presence_counts(state: &AppState) -> (usize, usize, usize) {
|
||||||
|
let Some(status) = &state.federation.devices else {
|
||||||
|
return (0, 0, 0);
|
||||||
|
};
|
||||||
|
let now = crate::app::state::unix_time_ms();
|
||||||
|
let mut online = 0usize;
|
||||||
|
let mut offline = 0usize;
|
||||||
|
let mut revoked = 0usize;
|
||||||
|
for device in &status.devices {
|
||||||
|
match crate::app::state::device_presence_section(state, device, now) {
|
||||||
|
DevicePresenceSection::Online => online += 1,
|
||||||
|
DevicePresenceSection::Offline => offline += 1,
|
||||||
|
DevicePresenceSection::Revoked => revoked += 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(online, offline, revoked)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn first_line(value: &str) -> String {
|
||||||
|
value.lines().next().unwrap_or(value).to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn status_detail_lines(state: &AppState) -> Vec<Line<'static>> {
|
||||||
let mut lines: Vec<Line> = vec![Line::styled("Status", theme::header())];
|
let mut lines: Vec<Line> = vec![Line::styled("Status", theme::header())];
|
||||||
match &state.federation.status {
|
match &state.federation.status {
|
||||||
None => lines.push(Line::styled("loading…", theme::dim())),
|
None => lines.push(Line::styled("loading…", theme::dim())),
|
||||||
@@ -540,21 +813,12 @@ fn draw_status(frame: &mut Frame, area: Rect, state: &AppState) {
|
|||||||
lines.push(status_line("Node", format!("running · {}", status.network)));
|
lines.push(status_line("Node", format!("running · {}", status.network)));
|
||||||
lines.push(status_line(
|
lines.push(status_line(
|
||||||
"Endpoint",
|
"Endpoint",
|
||||||
format!(
|
format!("{} · dht {}", status.endpoint_id, status.dht_node_id),
|
||||||
"{} · dht {}",
|
|
||||||
short_id(&status.endpoint_id),
|
|
||||||
short_id(&status.dht_node_id)
|
|
||||||
),
|
|
||||||
));
|
));
|
||||||
let peers = if status.connected_peers.is_empty() {
|
let peers = if status.connected_peers.is_empty() {
|
||||||
format!("none · contacts {}", status.known_contacts)
|
format!("none · contacts {}", status.known_contacts)
|
||||||
} else {
|
} else {
|
||||||
let names: Vec<String> = status
|
let names: Vec<String> = status.connected_peers.iter().map(String::clone).collect();
|
||||||
.connected_peers
|
|
||||||
.iter()
|
|
||||||
.take(3)
|
|
||||||
.map(|p| short_id(p))
|
|
||||||
.collect();
|
|
||||||
let more = status.connected_peers.len().saturating_sub(names.len());
|
let more = status.connected_peers.len().saturating_sub(names.len());
|
||||||
let more = if more > 0 {
|
let more = if more > 0 {
|
||||||
format!(" +{more}")
|
format!(" +{more}")
|
||||||
@@ -605,13 +869,9 @@ fn draw_status(frame: &mut Frame, area: Rect, state: &AppState) {
|
|||||||
Some(status) => {
|
Some(status) => {
|
||||||
lines.push(status_line(
|
lines.push(status_line(
|
||||||
"This device",
|
"This device",
|
||||||
format!(
|
format!("{} · {}", status.this_device_name, status.this_device_id),
|
||||||
"{} · {}",
|
|
||||||
status.this_device_name,
|
|
||||||
short_id(&status.this_device_id)
|
|
||||||
),
|
|
||||||
));
|
));
|
||||||
lines.push(status_line("Sync group", short_id(&status.group_id)));
|
lines.push(status_line("Sync group", status.group_id.clone()));
|
||||||
lines.push(status_line(
|
lines.push(status_line(
|
||||||
"Devices",
|
"Devices",
|
||||||
format!(
|
format!(
|
||||||
@@ -647,7 +907,70 @@ fn draw_status(frame: &mut Frame, area: Rect, state: &AppState) {
|
|||||||
if let Some(last_error) = &status.last_error {
|
if let Some(last_error) = &status.last_error {
|
||||||
lines.push(status_line("Device error", last_error.clone()));
|
lines.push(status_line("Device error", last_error.clone()));
|
||||||
}
|
}
|
||||||
|
lines.push(Line::default());
|
||||||
|
lines.push(Line::styled("Device List", theme::header()));
|
||||||
|
if status.devices.is_empty() {
|
||||||
|
lines.push(status_line("Devices", "none recorded".to_string()));
|
||||||
|
} else {
|
||||||
|
let ordered = crate::app::state::device_status_order(state);
|
||||||
|
let now = crate::app::state::unix_time_ms();
|
||||||
|
for index in &ordered {
|
||||||
|
if let Some(device) = status.devices.get(*index) {
|
||||||
|
push_device_detail(&mut lines, state, device, now);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (index, device) in status.devices.iter().enumerate() {
|
||||||
|
if !ordered.contains(&index) {
|
||||||
|
push_device_detail(&mut lines, state, device, now);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
frame.render_widget(Paragraph::new(lines), area);
|
lines
|
||||||
|
}
|
||||||
|
|
||||||
|
fn push_device_detail(
|
||||||
|
lines: &mut Vec<Line<'static>>,
|
||||||
|
state: &AppState,
|
||||||
|
device: &crate::devices::DeviceStatusRow,
|
||||||
|
now_ms: i64,
|
||||||
|
) {
|
||||||
|
let mut name = crate::app::state::device_display_name(device);
|
||||||
|
if device.is_self {
|
||||||
|
name.push_str(" (this device)");
|
||||||
|
}
|
||||||
|
if device.revoked {
|
||||||
|
name.push_str(" (revoked)");
|
||||||
|
}
|
||||||
|
let presence = match crate::app::state::device_presence_section(state, device, now_ms) {
|
||||||
|
DevicePresenceSection::Online => "online",
|
||||||
|
DevicePresenceSection::Offline => "offline",
|
||||||
|
DevicePresenceSection::Revoked => "revoked",
|
||||||
|
};
|
||||||
|
lines.push(status_line("Device", name));
|
||||||
|
lines.push(status_line("Device ID", device.device_id.clone()));
|
||||||
|
lines.push(status_line(
|
||||||
|
"Endpoint ID",
|
||||||
|
if device.endpoint_id.trim().is_empty() {
|
||||||
|
"unavailable".to_string()
|
||||||
|
} else {
|
||||||
|
device.endpoint_id.clone()
|
||||||
|
},
|
||||||
|
));
|
||||||
|
lines.push(status_line(
|
||||||
|
"Version",
|
||||||
|
if device.client_version.trim().is_empty() {
|
||||||
|
"unknown".to_string()
|
||||||
|
} else {
|
||||||
|
device.client_version.clone()
|
||||||
|
},
|
||||||
|
));
|
||||||
|
lines.push(status_line(
|
||||||
|
"Presence",
|
||||||
|
match device.last_seen_ms {
|
||||||
|
Some(seen) => format!("{presence} · last seen {seen} ms"),
|
||||||
|
None => format!("{presence} · last seen unavailable"),
|
||||||
|
},
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,6 +39,9 @@ pub fn draw(frame: &mut Frame, state: &AppState) {
|
|||||||
Some(Popup::LogDetail(entry)) => draw_log_detail(frame, entry),
|
Some(Popup::LogDetail(entry)) => draw_log_detail(frame, entry),
|
||||||
Some(Popup::FedInput { field, input }) => draw_fed_input(frame, field.title(), input),
|
Some(Popup::FedInput { field, input }) => draw_fed_input(frame, field.title(), input),
|
||||||
Some(Popup::FedText { title, text }) => draw_fed_text(frame, title, text),
|
Some(Popup::FedText { title, text }) => draw_fed_text(frame, title, text),
|
||||||
|
Some(Popup::FederationStatusDetails { scroll }) => {
|
||||||
|
draw_federation_status_details(frame, state, *scroll)
|
||||||
|
}
|
||||||
Some(Popup::DevicePairing {
|
Some(Popup::DevicePairing {
|
||||||
device_id,
|
device_id,
|
||||||
name,
|
name,
|
||||||
@@ -62,6 +65,37 @@ pub fn draw(frame: &mut Frame, state: &AppState) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn draw_federation_status_details(frame: &mut Frame, state: &AppState, scroll: usize) {
|
||||||
|
let width = frame.area().width.saturating_sub(6).clamp(52, 104);
|
||||||
|
let height = frame.area().height.saturating_sub(4).clamp(10, 32);
|
||||||
|
let area = centered(frame.area(), width, height);
|
||||||
|
let block = Block::bordered()
|
||||||
|
.title(" Full status details ")
|
||||||
|
.title_style(theme::header())
|
||||||
|
.border_style(theme::accent());
|
||||||
|
let inner = block.inner(area);
|
||||||
|
frame.render_widget(Clear, area);
|
||||||
|
frame.render_widget(block, area);
|
||||||
|
|
||||||
|
let [body, footer] = Layout::vertical([Constraint::Min(1), Constraint::Length(1)]).areas(inner);
|
||||||
|
let lines = super::federation::status_detail_lines(state);
|
||||||
|
let max_scroll = lines.len().saturating_sub(usize::from(body.height));
|
||||||
|
frame.render_widget(
|
||||||
|
Paragraph::new(lines)
|
||||||
|
.wrap(Wrap { trim: false })
|
||||||
|
.scroll((scroll.min(max_scroll) as u16, 0)),
|
||||||
|
body,
|
||||||
|
);
|
||||||
|
frame.render_widget(
|
||||||
|
Paragraph::new(Line::styled(
|
||||||
|
"j/k scroll - pgup/pgdn page - esc close",
|
||||||
|
theme::dim(),
|
||||||
|
))
|
||||||
|
.alignment(Alignment::Center),
|
||||||
|
footer,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
fn draw_connected_devices(frame: &mut Frame, state: &AppState, cursor: usize) {
|
fn draw_connected_devices(frame: &mut Frame, state: &AppState, cursor: usize) {
|
||||||
let rows = crate::app::popup::connected_device_rows(state);
|
let rows = crate::app::popup::connected_device_rows(state);
|
||||||
let self_row = rows.iter().find(|row| row.is_self);
|
let self_row = rows.iter().find(|row| row.is_self);
|
||||||
|
|||||||
Reference in New Issue
Block a user