1use std::io;
5use std::sync::mpsc;
6use std::sync::Arc;
7use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
8
9use ratatui::crossterm::event::{
10 self, DisableBracketedPaste, DisableFocusChange, DisableMouseCapture, EnableBracketedPaste,
11 EnableFocusChange, EnableMouseCapture, Event, KeyEventKind, MouseEventKind,
12};
13use ratatui::crossterm::execute;
14
15use crate::profile::{ConfigFile, Profile};
16use crate::tui::effect::Effect;
17use crate::tui::i18n::Lang;
18use crate::tui::model::{Model, Options};
19use crate::tui::msg::Msg;
20use crate::tui::update::update;
21use crate::tui::view::view;
22use crate::tui::worker::{Backend, SwitchBackend, Workers, THREADS};
23
24const TICK: Duration = Duration::from_millis(250);
26const POLL: Duration = Duration::from_millis(40);
28
29pub trait Host {
31 fn save_language(&mut self, lang: Lang) -> Result<(), String>;
32 fn write_file(&mut self, path: &str, contents: &str) -> Result<(), String>;
34 fn write_secret(&mut self, path: &str, contents: &str) -> Result<(), String>;
36 fn save_profiles(&mut self, file: &ConfigFile) -> Result<(), String>;
38 fn check_profile(&mut self, profile: &Profile) -> crate::tui::msg::CheckResult;
40 fn connect(&mut self, name: &str) -> Result<Connection, String>;
42}
43
44pub struct Connection {
46 pub backend: Arc<dyn Backend>,
47 pub url: String,
48}
49
50struct TerminalGuard {
52 mouse: bool,
53}
54
55impl TerminalGuard {
56 fn enter(mouse: bool) -> TerminalGuard {
57 let mut out = io::stdout();
58 if mouse {
59 let _ = execute!(out, EnableMouseCapture);
60 }
61 let _ = execute!(out, EnableBracketedPaste, EnableFocusChange);
62 TerminalGuard { mouse }
63 }
64}
65
66impl Drop for TerminalGuard {
67 fn drop(&mut self) {
68 let mut out = io::stdout();
69 if self.mouse {
70 let _ = execute!(out, DisableMouseCapture);
71 }
72 let _ = execute!(out, DisableBracketedPaste, DisableFocusChange);
73 }
74}
75
76fn wall_ms() -> i64 {
77 SystemTime::now()
78 .duration_since(UNIX_EPOCH)
79 .map_or(0, |d| i64::try_from(d.as_millis()).unwrap_or(0))
80}
81
82fn carry_out(
84 effects: Vec<Effect>,
85 model: &mut Model,
86 workers: &Workers,
87 switch: &SwitchBackend,
88 host: &mut dyn Host,
89) -> bool {
90 let mut keep_going = true;
91 for effect in effects {
92 match effect {
93 Effect::Call {
94 id,
95 generation,
96 call,
97 } => workers.submit(id, generation, call),
98 Effect::Quit => keep_going = false,
99 Effect::WriteFile { path, contents } => {
100 let result = host.write_file(&path, &contents);
101 let more = update(model, Msg::FileWritten { path, result });
102 keep_going &= carry_out(more, model, workers, switch, host);
103 }
104 Effect::SaveLanguage(lang) => {
105 let result = host.save_language(lang);
106 let more = update(model, Msg::LanguageSaved(result));
107 keep_going &= carry_out(more, model, workers, switch, host);
108 }
109 Effect::WriteSecret { path, contents } => {
110 let result = host.write_secret(&path, &contents);
111 let more = update(model, Msg::FileWritten { path, result });
112 keep_going &= carry_out(more, model, workers, switch, host);
113 }
114 Effect::Copy(text) => copy_to_clipboard(&text),
115 Effect::SaveProfiles { file, select } => {
116 let result = host.save_profiles(&file);
117 let more = update(
118 model,
119 Msg::ProfilesSaved {
120 file,
121 select,
122 result,
123 },
124 );
125 keep_going &= carry_out(more, model, workers, switch, host);
126 }
127 Effect::CheckProfile(profile) => {
128 let result = host.check_profile(&profile);
129 let more = update(model, Msg::ProfileChecked(result));
130 keep_going &= carry_out(more, model, workers, switch, host);
131 }
132 Effect::Connect(name) => {
133 let (url, result) = match host.connect(&name) {
134 Ok(connection) => {
135 switch.switch(connection.backend);
136 (connection.url, Ok(()))
137 }
138 Err(e) => (String::new(), Err(e)),
139 };
140 let more = update(
141 model,
142 Msg::Connected {
143 profile: name,
144 url,
145 result,
146 },
147 );
148 keep_going &= carry_out(more, model, workers, switch, host);
149 }
150 }
151 }
152 keep_going
153}
154
155fn copy_to_clipboard(text: &str) {
157 use std::io::Write;
158 let mut out = io::stdout();
159 let _ = write!(
160 out,
161 "]52;c;{}",
162 taba_client::base64::encode(text.as_bytes())
163 );
164 let _ = out.flush();
165}
166
167pub fn run(
169 backend: Arc<dyn Backend>,
170 options: Options,
171 mouse: bool,
172 host: &mut dyn Host,
173) -> io::Result<()> {
174 let mut terminal = ratatui::init();
175 let _guard = TerminalGuard::enter(mouse);
176 let result = event_loop(&mut terminal, backend, options, host);
177 ratatui::restore();
178 result
179}
180
181fn event_loop(
182 terminal: &mut ratatui::DefaultTerminal,
183 backend: Arc<dyn Backend>,
184 options: Options,
185 host: &mut dyn Host,
186) -> io::Result<()> {
187 let (results, replies) = mpsc::channel::<Msg>();
188 let switch = Arc::new(SwitchBackend::new(backend));
189 let workers = Workers::start(Arc::clone(&switch) as Arc<dyn Backend>, results, THREADS);
190 let mut model = Model::new(options);
191 let size = terminal.size()?;
192 model.size = (size.width, size.height);
193 let started = Instant::now();
194 let mut last_tick: Option<Instant> = None;
195 let mut dirty = true;
196 loop {
197 let mut messages: Vec<Msg> = Vec::new();
198 if event::poll(POLL)? {
199 match event::read()? {
200 Event::Key(key) if key.kind != KeyEventKind::Release => {
201 messages.push(Msg::Key(key))
202 }
203 Event::Resize(width, height) => messages.push(Msg::Resize(width, height)),
204 Event::Paste(text) => messages.push(Msg::Paste(text)),
205 Event::FocusGained => messages.push(Msg::Focus(true)),
206 Event::FocusLost => messages.push(Msg::Focus(false)),
207 Event::Mouse(mouse) => match mouse.kind {
208 MouseEventKind::ScrollUp => messages.push(Msg::Scroll(-3)),
209 MouseEventKind::ScrollDown => messages.push(Msg::Scroll(3)),
210 _ => {}
211 },
212 _ => {}
213 }
214 }
215 while let Ok(reply) = replies.try_recv() {
216 messages.push(reply);
217 }
218 if last_tick.is_none_or(|t| t.elapsed() >= TICK) {
219 last_tick = Some(Instant::now());
220 messages.push(Msg::Tick {
221 now: u64::try_from(started.elapsed().as_millis()).unwrap_or(u64::MAX),
222 wall_ms: wall_ms(),
223 });
224 }
225 dirty |= !messages.is_empty();
226 for message in messages {
227 let effects = update(&mut model, message);
228 if !carry_out(effects, &mut model, &workers, &switch, host) {
229 return Ok(());
230 }
231 }
232 if dirty {
233 terminal.draw(|frame| view(frame, &model))?;
234 dirty = false;
235 }
236 }
237}
238
239#[cfg(test)]
240mod tests;