Skip to main content

foundry_tui_app/controller/
mod.rs

1use std::path::PathBuf;
2
3use chrono::Local;
4use foundry_tui_config::{
5    default_custom_templates, ActionId, AppConfig, TemplateLoadState, TemplateTool,
6};
7
8use crate::{
9    job_manager::JobManager,
10    model::{AppModel, LogLine, LogStream, LogTextMode, SectionFocus, Tab},
11    parsing::rpc_chain_label,
12    project_inventory::scan_project_inventory,
13};
14
15mod actions;
16mod anvil;
17mod custom;
18mod input;
19mod jobs;
20
21pub struct AppController {
22    pub model: AppModel,
23    config: AppConfig,
24    job_manager: JobManager,
25    ticks_since_index: u64,
26}
27
28impl AppController {
29    pub fn new(config: AppConfig, project_root: PathBuf, config_path: PathBuf) -> Self {
30        Self::new_with_templates(
31            config,
32            project_root,
33            config_path,
34            TemplateLoadState {
35                templates: Vec::new(),
36                global_path: PathBuf::new(),
37                project_path: PathBuf::new(),
38            },
39        )
40    }
41
42    pub fn new_with_templates(
43        config: AppConfig,
44        project_root: PathBuf,
45        config_path: PathBuf,
46        templates: TemplateLoadState,
47    ) -> Self {
48        let mut custom_templates = templates
49            .templates
50            .into_iter()
51            .filter(|template| matches!(template.tool, TemplateTool::Forge | TemplateTool::Cast))
52            .collect::<Vec<_>>();
53        if custom_templates.is_empty() {
54            custom_templates = default_custom_templates()
55                .into_iter()
56                .filter(|template| {
57                    matches!(template.tool, TemplateTool::Forge | TemplateTool::Cast)
58                })
59                .collect();
60        }
61
62        let inventory = scan_project_inventory(&project_root);
63        let (active_rpc_preset, active_rpc_url) = resolve_active_rpc_target(&config);
64        let active_rpc_chain = active_rpc_preset
65            .as_ref()
66            .map(|preset| rpc_chain_label(preset));
67
68        let mut model = AppModel {
69            active_tab: Tab::Build,
70            tabs: Tab::ALL.to_vec(),
71            jobs: std::collections::BTreeMap::new(),
72            history: Vec::new(),
73            logs: Vec::new(),
74            focused_section: SectionFocus::MainPanel,
75            palette_open: false,
76            palette_index: 0,
77            palette_actions: ActionId::palette_defaults(),
78            show_build_onboarding: true,
79            mouse_mode_enabled: false,
80            log_text_mode: LogTextMode::Horizontal,
81            main_scroll: 0,
82            jobs_scroll: 0,
83            logs_scroll: 0,
84            anvil_logs_scroll: 0,
85            logs_hscroll: 0,
86            anvil_logs_hscroll: 0,
87            notification: None,
88            key_hints: config.keys.bindings.clone(),
89            project_root,
90            config_path,
91            project_sol_files: inventory.sol_files,
92            project_has_foundry_toml: inventory.has_foundry_toml,
93            project_has_remappings: inventory.has_remappings,
94            project_indexed_at: Local::now(),
95            active_rpc_preset,
96            active_rpc_chain,
97            active_rpc_url,
98            custom_templates,
99            custom_template_index: 0,
100            custom_templates_global_path: templates.global_path,
101            custom_templates_project_path: templates.project_path,
102            custom_modal: None,
103            anvil_instances: Vec::new(),
104            selected_anvil_index: 0,
105            anvil_prompt: None,
106            should_quit: false,
107            launched_at: Local::now(),
108        };
109
110        model.logs.push(LogLine {
111            ts: Local::now(),
112            job_id: None,
113            stream: LogStream::System,
114            message: "Foundry TUI ready. Press Ctrl+P for command palette.".to_string(),
115        });
116
117        Self {
118            model,
119            job_manager: JobManager::new(config.jobs.max_concurrent),
120            config,
121            ticks_since_index: 0,
122        }
123    }
124
125    pub fn should_quit(&self) -> bool {
126        self.model.should_quit
127    }
128
129    pub fn on_tick(&mut self) {
130        if self.config.jobs.auto_scroll_logs
131            && self.model.focused_section != SectionFocus::LogsPanel
132        {
133            self.model.logs_scroll = 0;
134        }
135        if self.config.jobs.auto_scroll_logs
136            && self.model.focused_section != SectionFocus::AnvilInstanceLogsPanel
137        {
138            self.model.anvil_logs_scroll = 0;
139        }
140
141        self.ticks_since_index = self.ticks_since_index.saturating_add(1);
142        if self.ticks_since_index >= 20 {
143            self.refresh_project_inventory();
144            self.ticks_since_index = 0;
145        }
146    }
147
148    pub fn set_focused_section(&mut self, section: SectionFocus) {
149        if self.available_sections().contains(&section) {
150            self.model.focused_section = section;
151        }
152    }
153}
154
155fn resolve_active_rpc_target(config: &AppConfig) -> (Option<String>, Option<String>) {
156    if let Some(preset) = &config.foundry.default_rpc_preset {
157        if let Some(url) = config.rpc_presets.get(preset) {
158            return (Some(preset.clone()), Some(url.clone()));
159        }
160    }
161
162    if let Some((preset, url)) = config.rpc_presets.iter().next() {
163        return (Some(preset.clone()), Some(url.clone()));
164    }
165
166    (None, None)
167}