foundry-tui-app 0.0.5

Application model and controller for foundry-tui
Documentation
use std::{collections::BTreeMap, path::PathBuf};

use chrono::{DateTime, Local};
use foundry_tui_config::{ActionId, CustomTemplate};

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Tab {
    Build,
    Test,
    Script,
    Anvil,
    Cast,
    Verify,
    Custom,
    Logs,
    History,
}

impl Tab {
    pub const ALL: [Tab; 9] = [
        Tab::Build,
        Tab::Test,
        Tab::Script,
        Tab::Anvil,
        Tab::Cast,
        Tab::Verify,
        Tab::Custom,
        Tab::Logs,
        Tab::History,
    ];

    pub fn title(self) -> &'static str {
        match self {
            Tab::Build => "Build",
            Tab::Test => "Test",
            Tab::Script => "Script",
            Tab::Anvil => "Anvil",
            Tab::Cast => "Cast",
            Tab::Verify => "Verify",
            Tab::Custom => "Builder",
            Tab::Logs => "Logs",
            Tab::History => "History",
        }
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SectionFocus {
    MainPanel,
    JobsPanel,
    LogsPanel,
    AnvilInstancesPanel,
    AnvilInstanceLogsPanel,
}

impl SectionFocus {
    pub fn label(self) -> &'static str {
        match self {
            SectionFocus::MainPanel => "Main",
            SectionFocus::JobsPanel => "Jobs",
            SectionFocus::LogsPanel => "Logs",
            SectionFocus::AnvilInstancesPanel => "Anvil Instances",
            SectionFocus::AnvilInstanceLogsPanel => "Anvil Logs",
        }
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum JobStatus {
    Running,
    Success,
    Failed,
    Cancelled,
}

impl JobStatus {
    pub fn label(self) -> &'static str {
        match self {
            JobStatus::Running => "running",
            JobStatus::Success => "success",
            JobStatus::Failed => "failed",
            JobStatus::Cancelled => "cancelled",
        }
    }
}

#[derive(Debug, Clone)]
pub struct JobRecord {
    pub id: u64,
    pub name: String,
    pub commandline: String,
    pub status: JobStatus,
    pub started_at: DateTime<Local>,
    pub finished_at: Option<DateTime<Local>>,
    pub duration_ms: Option<u128>,
    pub status_code: Option<i32>,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LogStream {
    System,
    Stdout,
    Stderr,
}

#[derive(Debug, Clone)]
pub struct LogLine {
    pub ts: DateTime<Local>,
    pub job_id: Option<u64>,
    pub stream: LogStream,
    pub message: String,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AnvilInstanceStatus {
    Starting,
    Running,
    Failed,
    Stopped,
}

impl AnvilInstanceStatus {
    pub fn label(self) -> &'static str {
        match self {
            AnvilInstanceStatus::Starting => "starting",
            AnvilInstanceStatus::Running => "running",
            AnvilInstanceStatus::Failed => "failed",
            AnvilInstanceStatus::Stopped => "stopped",
        }
    }
}

#[derive(Debug, Clone)]
pub struct AnvilInstance {
    pub job_id: u64,
    pub name: String,
    pub port: u16,
    pub fork_url: Option<String>,
    pub status: AnvilInstanceStatus,
    pub logs: Vec<LogLine>,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AnvilPromptField {
    Name,
    Port,
    ForkUrl,
    ExtraFlags,
}

impl AnvilPromptField {
    pub(crate) fn next(self) -> Self {
        match self {
            AnvilPromptField::Name => AnvilPromptField::Port,
            AnvilPromptField::Port => AnvilPromptField::ForkUrl,
            AnvilPromptField::ForkUrl => AnvilPromptField::ExtraFlags,
            AnvilPromptField::ExtraFlags => AnvilPromptField::Name,
        }
    }

    pub(crate) fn prev(self) -> Self {
        match self {
            AnvilPromptField::Name => AnvilPromptField::ExtraFlags,
            AnvilPromptField::Port => AnvilPromptField::Name,
            AnvilPromptField::ForkUrl => AnvilPromptField::Port,
            AnvilPromptField::ExtraFlags => AnvilPromptField::ForkUrl,
        }
    }
}

#[derive(Debug, Clone)]
pub struct AnvilLaunchPrompt {
    pub name: String,
    pub port: String,
    pub fork_url: String,
    pub extra_flags: String,
    pub focus: AnvilPromptField,
    pub error: Option<String>,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CustomModalStep {
    TemplatePicker,
    Editor,
    Preview,
}

#[derive(Debug, Clone)]
pub struct CustomCommandDraft {
    pub template: CustomTemplate,
    pub rpc_preset: String,
    pub raw_args: String,
    pub param_values: BTreeMap<String, String>,
    pub merged_args: Vec<String>,
    pub resolved_args: Vec<String>,
    pub display_args: Vec<String>,
    pub display_commandline: String,
    pub rpc_url: Option<String>,
}

#[derive(Debug, Clone)]
pub struct CustomCommandModal {
    pub step: CustomModalStep,
    pub picker_index: usize,
    pub paste_mode: bool,
    pub paste_input: String,
    pub editor_index: usize,
    pub draft: Option<CustomCommandDraft>,
    pub error: Option<String>,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LogTextMode {
    Horizontal,
    Wrapped,
}

impl LogTextMode {
    pub fn toggle(self) -> Self {
        match self {
            LogTextMode::Horizontal => LogTextMode::Wrapped,
            LogTextMode::Wrapped => LogTextMode::Horizontal,
        }
    }

    pub fn short_label(self) -> &'static str {
        match self {
            LogTextMode::Horizontal => "H",
            LogTextMode::Wrapped => "W",
        }
    }

    pub fn label(self) -> &'static str {
        match self {
            LogTextMode::Horizontal => "horizontal",
            LogTextMode::Wrapped => "wrapped",
        }
    }
}

#[derive(Debug, Clone)]
pub struct AppModel {
    pub active_tab: Tab,
    pub tabs: Vec<Tab>,
    pub jobs: BTreeMap<u64, JobRecord>,
    pub history: Vec<JobRecord>,
    pub logs: Vec<LogLine>,
    pub focused_section: SectionFocus,
    pub palette_open: bool,
    pub palette_index: usize,
    pub palette_actions: Vec<ActionId>,
    pub show_build_onboarding: bool,
    pub mouse_mode_enabled: bool,
    pub log_text_mode: LogTextMode,
    pub main_scroll: usize,
    pub jobs_scroll: usize,
    pub logs_scroll: usize,
    pub anvil_logs_scroll: usize,
    pub logs_hscroll: usize,
    pub anvil_logs_hscroll: usize,
    pub notification: Option<String>,
    pub key_hints: BTreeMap<ActionId, String>,
    pub project_root: PathBuf,
    pub config_path: PathBuf,
    pub project_sol_files: Vec<String>,
    pub project_has_foundry_toml: bool,
    pub project_has_remappings: bool,
    pub project_indexed_at: DateTime<Local>,
    pub active_rpc_preset: Option<String>,
    pub active_rpc_chain: Option<String>,
    pub active_rpc_url: Option<String>,
    pub custom_templates: Vec<CustomTemplate>,
    pub custom_template_index: usize,
    pub custom_templates_global_path: PathBuf,
    pub custom_templates_project_path: PathBuf,
    pub custom_modal: Option<CustomCommandModal>,
    pub anvil_instances: Vec<AnvilInstance>,
    pub selected_anvil_index: usize,
    pub anvil_prompt: Option<AnvilLaunchPrompt>,
    pub should_quit: bool,
    pub launched_at: DateTime<Local>,
}

impl AppModel {
    pub fn active_tab_index(&self) -> usize {
        self.tabs
            .iter()
            .position(|tab| *tab == self.active_tab)
            .unwrap_or(0)
    }
}