foundry_tui_app/controller/
mod.rs1use 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(§ion) {
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}