fix: align Backend worker picker columns
This commit is contained in:
@@ -12,6 +12,7 @@ use ratatui::layout::{Constraint, Layout};
|
|||||||
use ratatui::style::{Color, Modifier, Style};
|
use ratatui::style::{Color, Modifier, Style};
|
||||||
use ratatui::text::{Line, Span};
|
use ratatui::text::{Line, Span};
|
||||||
use ratatui::widgets::Paragraph;
|
use ratatui::widgets::Paragraph;
|
||||||
|
use unicode_width::UnicodeWidthStr;
|
||||||
|
|
||||||
use crate::backend_workspace_picker::select_backend_workspace;
|
use crate::backend_workspace_picker::select_backend_workspace;
|
||||||
use crate::console;
|
use crate::console;
|
||||||
@@ -235,9 +236,10 @@ fn draw(frame: &mut Frame<'_>, state: &BackendWorkerPickerState) {
|
|||||||
layout[0],
|
layout[0],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let column_widths = WorkerColumnWidths::from_workers(&state.workers);
|
||||||
for (i, worker) in state.workers.iter().enumerate() {
|
for (i, worker) in state.workers.iter().enumerate() {
|
||||||
frame.render_widget(
|
frame.render_widget(
|
||||||
Paragraph::new(row_line(worker, i == state.selected)),
|
Paragraph::new(row_line(worker, &column_widths, i == state.selected)),
|
||||||
layout[i + 1],
|
layout[i + 1],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -272,7 +274,28 @@ fn picker_title(target: &BackendRuntimeListTarget) -> String {
|
|||||||
format!("backend workers workspace: {workspace} runtime: {runtime}")
|
format!("backend workers workspace: {workspace} runtime: {runtime}")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn row_line(worker: &BackendWorkerSummary, selected: bool) -> Line<'static> {
|
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
|
||||||
|
struct WorkerColumnWidths {
|
||||||
|
identity: usize,
|
||||||
|
name: usize,
|
||||||
|
state: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WorkerColumnWidths {
|
||||||
|
fn from_workers(workers: &[BackendWorkerSummary]) -> Self {
|
||||||
|
workers.iter().fold(Self::default(), |widths, worker| Self {
|
||||||
|
identity: widths.identity.max(text_width(&short_worker_id(worker))),
|
||||||
|
name: widths.name.max(text_width(worker_name(worker))),
|
||||||
|
state: widths.state.max(text_width(&worker_state(worker))),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn row_line(
|
||||||
|
worker: &BackendWorkerSummary,
|
||||||
|
widths: &WorkerColumnWidths,
|
||||||
|
selected: bool,
|
||||||
|
) -> Line<'static> {
|
||||||
let marker = if selected { "▶ " } else { " " };
|
let marker = if selected { "▶ " } else { " " };
|
||||||
let id_style = if selected {
|
let id_style = if selected {
|
||||||
Style::default()
|
Style::default()
|
||||||
@@ -281,42 +304,58 @@ fn row_line(worker: &BackendWorkerSummary, selected: bool) -> Line<'static> {
|
|||||||
} else {
|
} else {
|
||||||
Style::default().fg(Color::Cyan)
|
Style::default().fg(Color::Cyan)
|
||||||
};
|
};
|
||||||
let preview_style = if selected {
|
let name_style = if selected {
|
||||||
Style::default().fg(Color::White)
|
Style::default().fg(Color::White)
|
||||||
} else {
|
} else {
|
||||||
Style::default().fg(Color::DarkGray)
|
Style::default().fg(Color::DarkGray)
|
||||||
};
|
};
|
||||||
|
|
||||||
let label = if worker.label.is_empty() {
|
|
||||||
worker.worker_id.as_str()
|
|
||||||
} else {
|
|
||||||
worker.label.as_str()
|
|
||||||
};
|
|
||||||
let profile = worker.profile.as_deref().unwrap_or("-");
|
|
||||||
|
|
||||||
Line::from(vec![
|
Line::from(vec![
|
||||||
Span::raw(marker),
|
Span::raw(marker),
|
||||||
Span::styled(short_worker_id(worker), id_style),
|
|
||||||
Span::raw(" "),
|
|
||||||
Span::styled(
|
Span::styled(
|
||||||
format!("[{}]", worker.state),
|
pad_column(&short_worker_id(worker), widths.identity),
|
||||||
state_style(worker.state.as_str()),
|
id_style,
|
||||||
),
|
),
|
||||||
Span::raw(" "),
|
Span::raw(" "),
|
||||||
|
Span::styled(pad_column(worker_name(worker), widths.name), name_style),
|
||||||
|
Span::raw(" "),
|
||||||
Span::styled(
|
Span::styled(
|
||||||
format!("profile:{profile}"),
|
pad_column(&worker_state(worker), widths.state),
|
||||||
Style::default().fg(Color::DarkGray),
|
state_style(worker.state.as_str()),
|
||||||
),
|
),
|
||||||
Span::raw(" "),
|
Span::raw(" "),
|
||||||
Span::styled(
|
Span::styled(
|
||||||
working_directory_text(worker),
|
working_directory_text(worker),
|
||||||
Style::default().fg(Color::DarkGray),
|
Style::default().fg(Color::DarkGray),
|
||||||
),
|
),
|
||||||
Span::raw(" "),
|
|
||||||
Span::styled(label.to_string(), preview_style),
|
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn worker_name(worker: &BackendWorkerSummary) -> &str {
|
||||||
|
if !worker.label.is_empty() {
|
||||||
|
worker.label.as_str()
|
||||||
|
} else if !worker.display_name.is_empty() {
|
||||||
|
worker.display_name.as_str()
|
||||||
|
} else {
|
||||||
|
worker.worker_id.as_str()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn worker_state(worker: &BackendWorkerSummary) -> String {
|
||||||
|
format!("[{}]", worker.state)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn text_width(value: &str) -> usize {
|
||||||
|
UnicodeWidthStr::width(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn pad_column(value: &str, width: usize) -> String {
|
||||||
|
format!(
|
||||||
|
"{value}{}",
|
||||||
|
" ".repeat(width.saturating_sub(text_width(value)))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
fn state_style(state: &str) -> Style {
|
fn state_style(state: &str) -> Style {
|
||||||
match state {
|
match state {
|
||||||
"running" | "idle" | "active" => Style::default()
|
"running" | "idle" | "active" => Style::default()
|
||||||
@@ -347,11 +386,7 @@ fn working_directory_text(worker: &BackendWorkerSummary) -> String {
|
|||||||
let Some(wd) = worker.working_directory.as_ref() else {
|
let Some(wd) = worker.working_directory.as_ref() else {
|
||||||
return "wd:—".to_string();
|
return "wd:—".to_string();
|
||||||
};
|
};
|
||||||
let cleanliness = wd.cleanliness.as_deref().unwrap_or("unknown");
|
format!("wd:{}・{}", wd.repository_key, wd.working_directory_id)
|
||||||
format!(
|
|
||||||
"wd:{}:{} {} {}",
|
|
||||||
wd.repository_key, wd.working_directory_id, wd.status, cleanliness
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@@ -395,18 +430,90 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
fn row_text(worker: &BackendWorkerSummary, widths: &WorkerColumnWidths) -> String {
|
||||||
fn worker_row_matches_inline_picker_shape() {
|
row_line(worker, widths, false)
|
||||||
let row = row_line(&worker("runtime-a", "worker-b", Some("default")), true);
|
|
||||||
let text = row
|
|
||||||
.spans
|
.spans
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|span| span.content)
|
.map(|span| span.content)
|
||||||
.collect::<String>();
|
.collect()
|
||||||
assert!(text.starts_with("▶ W-1"));
|
}
|
||||||
assert!(text.contains("[running]"));
|
|
||||||
assert!(text.contains("profile:default"));
|
fn display_column(text: &str, value: &str) -> usize {
|
||||||
assert!(text.contains("wd:—"));
|
let byte_offset = text.find(value).expect("value in rendered row");
|
||||||
|
text_width(&text[..byte_offset])
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn worker_row_orders_and_simplifies_columns() {
|
||||||
|
let mut worker = worker("runtime-a", "worker-b", Some("builtin:coder"));
|
||||||
|
worker.resource_key = "W-90".to_string();
|
||||||
|
worker.display_name = "Coder".to_string();
|
||||||
|
worker.label = "Coder · T-585".to_string();
|
||||||
|
worker.state = "stopped".to_string();
|
||||||
|
worker.working_directory = Some(
|
||||||
|
serde_json::from_value(serde_json::json!({
|
||||||
|
"working_directory_id": "001a06a9f0202000000",
|
||||||
|
"repository_key": "main",
|
||||||
|
"materializer_kind": "local_git_worktree",
|
||||||
|
"status": "active",
|
||||||
|
"cleanliness": "clean"
|
||||||
|
}))
|
||||||
|
.unwrap(),
|
||||||
|
);
|
||||||
|
let widths = WorkerColumnWidths::from_workers(std::slice::from_ref(&worker));
|
||||||
|
let text = row_text(&worker, &widths);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
text,
|
||||||
|
" W-90 Coder · T-585 [stopped] wd:main・001a06a9f0202000000"
|
||||||
|
);
|
||||||
|
assert!(!text.contains("profile:"));
|
||||||
|
assert!(!text.contains("active clean"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn worker_rows_align_identity_name_state_and_workdir_columns() {
|
||||||
|
let mut short = worker("runtime-a", "worker-a", None);
|
||||||
|
short.resource_key = "W-2".to_string();
|
||||||
|
short.label = "Coder".to_string();
|
||||||
|
short.display_name = short.label.clone();
|
||||||
|
short.state = "idle".to_string();
|
||||||
|
|
||||||
|
let mut long = worker("runtime-a", "worker-b", None);
|
||||||
|
long.resource_key = "W-100".to_string();
|
||||||
|
long.label = "Longer worker · T-9".to_string();
|
||||||
|
long.display_name = long.label.clone();
|
||||||
|
long.state = "stopped".to_string();
|
||||||
|
|
||||||
|
for worker in [&mut short, &mut long] {
|
||||||
|
worker.working_directory = Some(
|
||||||
|
serde_json::from_value(serde_json::json!({
|
||||||
|
"working_directory_id": "workdir-1",
|
||||||
|
"repository_key": "main",
|
||||||
|
"materializer_kind": "local_git_worktree",
|
||||||
|
"status": "active"
|
||||||
|
}))
|
||||||
|
.unwrap(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let workers = vec![short, long];
|
||||||
|
let widths = WorkerColumnWidths::from_workers(&workers);
|
||||||
|
let first = row_text(&workers[0], &widths);
|
||||||
|
let second = row_text(&workers[1], &widths);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
display_column(&first, "Coder"),
|
||||||
|
display_column(&second, "Longer")
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
display_column(&first, "[idle]"),
|
||||||
|
display_column(&second, "[stopped]")
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
display_column(&first, "wd:main"),
|
||||||
|
display_column(&second, "wd:main")
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
Reference in New Issue
Block a user