use std::cell::RefCell;
use std::rc::Rc;
use std::sync::mpsc::{self, Receiver, Sender};
use std::time::Duration;
use slint::{ComponentHandle, Timer};
use crate::shipyard_ui::config;
use crate::shipyard_ui::runtime::commands::{self, CommandIntent};
use crate::shipyard_ui::runtime::events::{RunStatus, StreamKind, TerminalEvent};
use crate::shipyard_ui::runtime::executor;
use crate::shipyard_ui::state;
use crate::shipyard_ui::ui;
#[derive(Clone, Copy)]
enum AccountSetupState {
Checking,
NeedsSetup,
SignedIn,
Unknown,
}
impl AccountSetupState {
fn status_text(self) -> &'static str {
match self {
Self::Checking => "Checking account status...",
Self::NeedsSetup => "Account setup required (register, confirm, then status).",
Self::SignedIn => "Authenticated. Account is ready.",
Self::Unknown => "Status uncertain. Run account status to verify current session.",
}
}
}
pub fn launch() -> Result<(), String> {
let window = ui::ShipyardWindow::new().map_err(|error| error.to_string())?;
let (event_tx, event_rx) = mpsc::channel();
let controller = Rc::new(RefCell::new(ShipyardController::new(
window.as_weak(),
event_tx,
event_rx,
)));
wire_callbacks(&window, &controller);
controller.borrow_mut().sync_ui();
controller
.borrow_mut()
.run_intent(CommandIntent::AccountStatus);
let poll_timer = Timer::default();
let poll_controller = controller.clone();
poll_timer.start(
slint::TimerMode::Repeated,
Duration::from_millis(config::REPAINT_INTERVAL_MS),
move || {
poll_controller.borrow_mut().flush_events();
},
);
window.run().map_err(|error| error.to_string())
}
fn wire_callbacks(window: &ui::ShipyardWindow, controller: &Rc<RefCell<ShipyardController>>) {
let status_controller = controller.clone();
window.on_run_status(move || {
status_controller
.borrow_mut()
.run_intent(CommandIntent::AccountStatus);
});
let register_controller = controller.clone();
window.on_run_register(move |email| {
let email = email.trim();
if email.is_empty() {
return;
}
register_controller
.borrow_mut()
.run_intent(CommandIntent::AccountRegister {
email: email.to_string(),
});
});
let confirm_controller = controller.clone();
window.on_run_confirm(move |code| {
let code = code.trim();
if code.is_empty() {
return;
}
confirm_controller
.borrow_mut()
.run_intent(CommandIntent::AccountConfirm {
code: code.to_string(),
});
});
let toggle_controller = controller.clone();
window.on_toggle_execution_view(move || {
toggle_controller.borrow_mut().toggle_execution_view();
});
}
struct ShipyardController {
window: slint::Weak<ui::ShipyardWindow>,
output_lines: Vec<String>,
current_run_output_lines: Vec<String>,
status: RunStatus,
last_command: String,
active_intent: Option<CommandIntent>,
account_setup_state: AccountSetupState,
execution_view_visible: bool,
event_tx: Sender<TerminalEvent>,
event_rx: Receiver<TerminalEvent>,
}
impl ShipyardController {
fn new(
window: slint::Weak<ui::ShipyardWindow>,
event_tx: Sender<TerminalEvent>,
event_rx: Receiver<TerminalEvent>,
) -> Self {
Self {
window,
output_lines: Vec::new(),
current_run_output_lines: Vec::new(),
status: RunStatus::Idle,
last_command: String::new(),
active_intent: None,
account_setup_state: AccountSetupState::Checking,
execution_view_visible: state::load_execution_view_visible()
.unwrap_or(config::EXECUTION_VIEW_DEFAULT_VISIBLE),
event_tx,
event_rx,
}
}
fn run_intent(&mut self, intent: CommandIntent) {
if self.status.is_running() {
return;
}
let plan = commands::command_plan(&intent);
self.last_command = plan.display.clone();
self.status = RunStatus::Running;
self.current_run_output_lines.clear();
self.active_intent = Some(intent);
self.push_output_line(format!("$ {}", self.last_command));
executor::spawn_command(plan, self.event_tx.clone());
self.sync_ui();
}
fn flush_events(&mut self) {
let mut saw_update = false;
while let Ok(event) = self.event_rx.try_recv() {
saw_update = true;
match event {
TerminalEvent::Output { stream, line } => {
let stream_name = match stream {
StreamKind::Stdout => "stdout",
StreamKind::Stderr => "stderr",
};
let formatted = format!("{stream_name} | {line}");
self.current_run_output_lines.push(formatted.clone());
self.push_output_line(formatted);
}
TerminalEvent::Finished { success, code } => {
self.status = if success {
RunStatus::Succeeded(code)
} else {
RunStatus::Failed(code)
};
if let Some(intent) = self.active_intent.take() {
self.handle_intent_finished(intent, success);
}
}
TerminalEvent::SpawnFailed(message) => {
self.status = RunStatus::SpawnError(message.clone());
let formatted = format!("stderr | {message}");
self.current_run_output_lines.push(formatted.clone());
self.push_output_line(formatted);
if let Some(intent) = self.active_intent.take() {
self.handle_intent_finished(intent, false);
}
}
}
}
if saw_update {
self.sync_ui();
}
}
fn handle_intent_finished(&mut self, intent: CommandIntent, success: bool) {
match intent {
CommandIntent::AccountStatus => {
self.account_setup_state =
derive_account_setup_state(&self.current_run_output_lines, success);
}
CommandIntent::AccountRegister { .. } => {
self.account_setup_state = AccountSetupState::NeedsSetup;
if success {
self.run_intent(CommandIntent::AccountStatus);
}
}
CommandIntent::AccountConfirm { .. } => {
if let Some(window) = self.window.upgrade() {
window.set_account_code("".into());
}
if success {
self.run_intent(CommandIntent::AccountStatus);
} else {
self.account_setup_state = AccountSetupState::Unknown;
}
}
}
}
fn toggle_execution_view(&mut self) {
self.execution_view_visible = !self.execution_view_visible;
let _ = state::save_execution_view_visible(self.execution_view_visible);
self.sync_ui();
}
fn sync_ui(&mut self) {
let Some(window) = self.window.upgrade() else {
return;
};
let (status_label, status_kind) = status_view(&self.status);
window.set_status_label(status_label.into());
window.set_status_kind(status_kind);
window.set_command_running(self.status.is_running());
window.set_last_command(self.last_command.clone().into());
window.set_account_status_text(self.account_setup_state.status_text().into());
window.set_account_ready(matches!(
self.account_setup_state,
AccountSetupState::SignedIn
));
window.set_terminal_output(self.output_lines.join("\n").into());
window.set_execution_view_visible(self.execution_view_visible);
}
fn push_output_line(&mut self, line: String) {
self.output_lines.push(line);
if self.output_lines.len() > config::MAX_TERMINAL_LINES {
let overflow = self.output_lines.len() - config::MAX_TERMINAL_LINES;
self.output_lines.drain(0..overflow);
}
}
}
fn status_view(status: &RunStatus) -> (String, i32) {
match status {
RunStatus::Idle => ("idle".to_string(), 0),
RunStatus::Running => ("running".to_string(), 1),
RunStatus::Succeeded(code) => {
if let Some(code) = code {
(format!("success (exit {code})"), 2)
} else {
("success".to_string(), 2)
}
}
RunStatus::Failed(code) => {
if let Some(code) = code {
(format!("failed (exit {code})"), 3)
} else {
("failed".to_string(), 3)
}
}
RunStatus::SpawnError(message) => (format!("spawn error: {message}"), 4),
}
}
fn derive_account_setup_state(lines: &[String], success: bool) -> AccountSetupState {
if !success {
return AccountSetupState::Unknown;
}
let output = lines.join("\n").to_lowercase();
let setup_missing_markers = [
"no local config file found",
"no account found in config",
"no access token found in config",
"you must confirm your account first",
"run `cargo ai account register <email>` first",
];
if setup_missing_markers
.iter()
.any(|marker| output.contains(marker))
{
return AccountSetupState::NeedsSetup;
}
if output.contains("request failed") || output.contains("spawn error") {
return AccountSetupState::Unknown;
}
AccountSetupState::SignedIn
}