use ratatui::{
Frame,
layout::Rect,
style::{Modifier, Style},
text::Line,
widgets::Paragraph,
};
use crate::tui::project_ctl::state::{Pane, PendingConfirm, ProjectCtlState};
pub const PROJECTS_HINT: &str =
"[Enter] drill in [c] config [v] deliverables [Tab] switch pane [↑↓] select [q] quit";
pub const SESSIONS_HINT: &str = "[l] launch [k] kill [r] resume [d] decommission [a] attach-cmd [Tab] switch pane [↑↓] select [q] quit";
pub const ACTIVITY_HINT: &str = "[Tab] switch pane [q] quit";
pub const DAEMON_REACHABLE: &str = "daemon ●";
pub const DAEMON_UNREACHABLE: &str = "daemon ✗";
pub fn focus_hint(focus: Pane) -> &'static str {
match focus {
Pane::Projects => PROJECTS_HINT,
Pane::Sessions => SESSIONS_HINT,
Pane::Activity => ACTIVITY_HINT,
}
}
pub fn confirm_prompt_text(confirm: &PendingConfirm) -> String {
use crate::tui::project_ctl::state::ConfirmKind;
match confirm.kind {
ConfirmKind::Kill => format!(
"⚠ kill '{}' — session is Active. Confirm? [y] yes [n/Esc] cancel",
confirm.session_label
),
ConfirmKind::Decommission => format!(
"⚠ decommission '{}' — PERMANENT, deletes the workspace. Confirm? [y] yes [n/Esc] cancel",
confirm.session_label
),
}
}
pub fn bar_text(state: &ProjectCtlState) -> String {
let daemon = if state.daemon_reachable {
DAEMON_REACHABLE
} else {
DAEMON_UNREACHABLE
};
let left = if let Some(confirm) = &state.pending_confirm {
confirm_prompt_text(confirm)
} else if let Some(notice) = &state.notice {
notice.clone()
} else {
focus_hint(state.focus).to_string()
};
format!("{left} · {daemon}")
}
pub fn render(frame: &mut Frame, area: Rect, state: &ProjectCtlState) {
let paragraph = Paragraph::new(Line::from(bar_text(state))).style(
Style::default()
.add_modifier(Modifier::BOLD)
.add_modifier(Modifier::REVERSED),
);
frame.render_widget(paragraph, area);
}
#[cfg(test)]
mod tests {
use super::*;
use crate::tui::project_ctl::state::ConfirmKind;
#[test]
fn focus_hint_differs_per_pane() {
let projects = focus_hint(Pane::Projects);
let sessions = focus_hint(Pane::Sessions);
let activity = focus_hint(Pane::Activity);
assert_ne!(projects, sessions);
assert_ne!(sessions, activity);
assert_ne!(projects, activity);
assert!(sessions.contains("[k] kill"));
assert!(sessions.contains("[d] decommission"));
assert!(!projects.contains("[k] kill"));
assert!(!activity.contains("[k] kill"));
assert!(projects.contains("[c] config"));
assert!(!sessions.contains("[c] config"));
}
#[test]
fn bar_text_shows_focus_hint_by_default() {
let mut state = ProjectCtlState::default();
assert_eq!(state.focus, Pane::Projects);
assert!(bar_text(&state).contains(PROJECTS_HINT));
state.focus = Pane::Sessions;
assert!(bar_text(&state).contains(SESSIONS_HINT));
state.focus = Pane::Activity;
assert!(bar_text(&state).contains(ACTIVITY_HINT));
}
#[test]
fn bar_text_prefers_notice_over_hint() {
let mut state = ProjectCtlState::default();
state.set_notice("killed foo — now stopped");
let text = bar_text(&state);
assert!(text.contains("killed foo — now stopped"));
assert!(!text.contains(PROJECTS_HINT));
}
#[test]
fn bar_text_prefers_confirm_over_notice() {
let mut state = ProjectCtlState::default();
state.set_notice("this should be hidden");
state.request_confirm(ConfirmKind::Decommission, "sess-1", "my-session");
let text = bar_text(&state);
assert!(text.contains("decommission"));
assert!(text.contains("my-session"));
assert!(!text.contains("this should be hidden"));
}
#[test]
fn confirm_prompt_text_names_verb_and_target() {
let kill = PendingConfirm {
kind: ConfirmKind::Kill,
session_id: "id".to_string(),
session_label: "my-session".to_string(),
};
let kill_text = confirm_prompt_text(&kill);
assert!(kill_text.contains("kill"));
assert!(kill_text.contains("my-session"));
assert!(kill_text.contains("[y] yes"));
assert!(kill_text.contains("[n/Esc] cancel"));
let decommission = PendingConfirm {
kind: ConfirmKind::Decommission,
session_id: "id".to_string(),
session_label: "my-session".to_string(),
};
let decommission_text = confirm_prompt_text(&decommission);
assert!(decommission_text.contains("decommission"));
assert!(decommission_text.contains("PERMANENT"));
}
#[test]
fn bar_text_shows_daemon_indicator() {
let mut state = ProjectCtlState::default();
assert!(bar_text(&state).contains(DAEMON_UNREACHABLE));
state.daemon_reachable = true;
assert!(bar_text(&state).contains(DAEMON_REACHABLE));
}
}