use ratatui::text::Line;
use super::nav::ListNav;
pub const OUTPUT_LOG_LIMIT: usize = 500;
pub const INPUT_HISTORY_LIMIT: usize = 50;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SessionEntry {
pub id: String,
pub short_id: String,
pub prefix: String,
pub status: String,
pub summary: String,
}
impl SessionEntry {
pub fn new(
short_id: impl Into<String>,
prefix: impl Into<String>,
status: impl Into<String>,
summary: impl Into<String>,
) -> Self {
let short_id = short_id.into();
Self::with_id(short_id.clone(), short_id, prefix, status, summary)
}
pub fn with_id(
id: impl Into<String>,
short_id: impl Into<String>,
prefix: impl Into<String>,
status: impl Into<String>,
summary: impl Into<String>,
) -> Self {
Self {
id: id.into(),
short_id: short_id.into(),
prefix: prefix.into(),
status: status.into(),
summary: summary.into(),
}
}
}
#[derive(Debug, Clone, Default)]
pub struct CoordinatorState {
pub input: String,
pub history: Vec<String>,
history_cursor: Option<usize>,
pub sessions: Vec<SessionEntry>,
pub nav: ListNav,
needs_repoll: bool,
pub daemon_reachable: bool,
pub catalog_stale: bool,
pub should_exit: bool,
pub output: Vec<Line<'static>>,
}
impl CoordinatorState {
pub fn skeleton() -> Self {
Self {
sessions: vec![
SessionEntry::new(
"4f9ca1b2",
"aipowerranking",
"Active",
"Running tests — 12 passed, fixing flaky timeout",
),
SessionEntry::new(
"7b2ec0d1",
"genealogy",
"Idle",
"Idle — last activity 6m ago",
),
],
..Self::default()
}
}
pub fn live() -> Self {
Self::default()
}
pub fn row_count(&self) -> usize {
1 + self.sessions.len()
}
pub fn selected(&self) -> usize {
self.nav.selected()
}
pub fn select_row(&mut self, row: usize) {
self.nav.sync_len(self.row_count());
self.nav.select(row);
}
pub fn clamp_selection(&mut self) {
self.nav.sync_len(self.row_count());
}
pub fn select_up(&mut self) {
self.nav.up();
}
pub fn select_down(&mut self) {
self.nav.sync_len(self.row_count());
self.nav.down();
}
pub fn page_up(&mut self) {
self.nav.page_up();
}
pub fn page_down(&mut self) {
self.nav.sync_len(self.row_count());
self.nav.page_down();
}
pub fn controller_selected(&self) -> bool {
self.selected() == 0
}
pub fn selected_session(&self) -> Option<&SessionEntry> {
self.selected()
.checked_sub(1)
.and_then(|idx| self.sessions.get(idx))
}
pub fn focused_target(&self) -> Option<&str> {
self.selected_session().map(|s| s.id.as_str())
}
pub fn push_output(&mut self, line: Line<'static>) {
self.output.push(line);
self.trim_output();
}
pub fn push_output_lines(&mut self, lines: impl IntoIterator<Item = Line<'static>>) {
self.output.extend(lines);
self.trim_output();
}
fn trim_output(&mut self) {
if self.output.len() > OUTPUT_LOG_LIMIT {
let overflow = self.output.len() - OUTPUT_LOG_LIMIT;
self.output.drain(0..overflow);
}
}
pub fn request_repoll(&mut self) {
self.needs_repoll = true;
}
pub fn take_repoll(&mut self) -> bool {
std::mem::take(&mut self.needs_repoll)
}
pub fn push_char(&mut self, c: char) {
self.input.push(c);
self.history_cursor = None;
}
pub fn backspace(&mut self) {
self.input.pop();
self.history_cursor = None;
}
pub fn clear_input(&mut self) {
self.input.clear();
self.history_cursor = None;
}
pub fn submit_input(&mut self) -> Option<String> {
let typed = std::mem::take(&mut self.input);
self.history_cursor = None;
let trimmed = typed.trim().to_string();
if trimmed.is_empty() {
return None;
}
self.history.push(trimmed.clone());
if self.history.len() > INPUT_HISTORY_LIMIT {
let overflow = self.history.len() - INPUT_HISTORY_LIMIT;
self.history.drain(0..overflow);
}
Some(trimmed)
}
pub fn history_prev(&mut self) {
if self.history.is_empty() {
return;
}
let next = match self.history_cursor {
None => self.history.len() - 1,
Some(0) => 0,
Some(i) => i - 1,
};
self.history_cursor = Some(next);
self.input = self.history[next].clone();
}
pub fn history_next(&mut self) {
let Some(i) = self.history_cursor else {
return;
};
if i + 1 >= self.history.len() {
self.history_cursor = None;
self.input.clear();
} else {
self.history_cursor = Some(i + 1);
self.input = self.history[i + 1].clone();
}
}
}