use std::io;
use std::sync::mpsc;
use std::sync::Arc;
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
use ratatui::crossterm::event::{
self, DisableBracketedPaste, DisableFocusChange, DisableMouseCapture, EnableBracketedPaste,
EnableFocusChange, EnableMouseCapture, Event, KeyEventKind, MouseEventKind,
};
use ratatui::crossterm::execute;
use crate::profile::{ConfigFile, Profile};
use crate::tui::effect::Effect;
use crate::tui::i18n::Lang;
use crate::tui::model::{Model, Options};
use crate::tui::msg::Msg;
use crate::tui::update::update;
use crate::tui::view::view;
use crate::tui::worker::{Backend, SwitchBackend, Workers, THREADS};
const TICK: Duration = Duration::from_millis(250);
const POLL: Duration = Duration::from_millis(40);
pub trait Host {
fn save_language(&mut self, lang: Lang) -> Result<(), String>;
fn write_file(&mut self, path: &str, contents: &str) -> Result<(), String>;
fn write_secret(&mut self, path: &str, contents: &str) -> Result<(), String>;
fn save_profiles(&mut self, file: &ConfigFile) -> Result<(), String>;
fn check_profile(&mut self, profile: &Profile) -> crate::tui::msg::CheckResult;
fn connect(&mut self, name: &str) -> Result<Connection, String>;
}
pub struct Connection {
pub backend: Arc<dyn Backend>,
pub url: String,
}
struct TerminalGuard {
mouse: bool,
}
impl TerminalGuard {
fn enter(mouse: bool) -> TerminalGuard {
let mut out = io::stdout();
if mouse {
let _ = execute!(out, EnableMouseCapture);
}
let _ = execute!(out, EnableBracketedPaste, EnableFocusChange);
TerminalGuard { mouse }
}
}
impl Drop for TerminalGuard {
fn drop(&mut self) {
let mut out = io::stdout();
if self.mouse {
let _ = execute!(out, DisableMouseCapture);
}
let _ = execute!(out, DisableBracketedPaste, DisableFocusChange);
}
}
fn wall_ms() -> i64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map_or(0, |d| i64::try_from(d.as_millis()).unwrap_or(0))
}
fn carry_out(
effects: Vec<Effect>,
model: &mut Model,
workers: &Workers,
switch: &SwitchBackend,
host: &mut dyn Host,
) -> bool {
let mut keep_going = true;
for effect in effects {
match effect {
Effect::Call {
id,
generation,
call,
} => workers.submit(id, generation, call),
Effect::Quit => keep_going = false,
Effect::WriteFile { path, contents } => {
let result = host.write_file(&path, &contents);
let more = update(model, Msg::FileWritten { path, result });
keep_going &= carry_out(more, model, workers, switch, host);
}
Effect::SaveLanguage(lang) => {
let result = host.save_language(lang);
let more = update(model, Msg::LanguageSaved(result));
keep_going &= carry_out(more, model, workers, switch, host);
}
Effect::WriteSecret { path, contents } => {
let result = host.write_secret(&path, &contents);
let more = update(model, Msg::FileWritten { path, result });
keep_going &= carry_out(more, model, workers, switch, host);
}
Effect::Copy(text) => copy_to_clipboard(&text),
Effect::SaveProfiles { file, select } => {
let result = host.save_profiles(&file);
let more = update(
model,
Msg::ProfilesSaved {
file,
select,
result,
},
);
keep_going &= carry_out(more, model, workers, switch, host);
}
Effect::CheckProfile(profile) => {
let result = host.check_profile(&profile);
let more = update(model, Msg::ProfileChecked(result));
keep_going &= carry_out(more, model, workers, switch, host);
}
Effect::Connect(name) => {
let (url, result) = match host.connect(&name) {
Ok(connection) => {
switch.switch(connection.backend);
(connection.url, Ok(()))
}
Err(e) => (String::new(), Err(e)),
};
let more = update(
model,
Msg::Connected {
profile: name,
url,
result,
},
);
keep_going &= carry_out(more, model, workers, switch, host);
}
}
}
keep_going
}
fn copy_to_clipboard(text: &str) {
use std::io::Write;
let mut out = io::stdout();
let _ = write!(
out,
"]52;c;{}",
taba_client::base64::encode(text.as_bytes())
);
let _ = out.flush();
}
pub fn run(
backend: Arc<dyn Backend>,
options: Options,
mouse: bool,
host: &mut dyn Host,
) -> io::Result<()> {
let mut terminal = ratatui::init();
let _guard = TerminalGuard::enter(mouse);
let result = event_loop(&mut terminal, backend, options, host);
ratatui::restore();
result
}
fn event_loop(
terminal: &mut ratatui::DefaultTerminal,
backend: Arc<dyn Backend>,
options: Options,
host: &mut dyn Host,
) -> io::Result<()> {
let (results, replies) = mpsc::channel::<Msg>();
let switch = Arc::new(SwitchBackend::new(backend));
let workers = Workers::start(Arc::clone(&switch) as Arc<dyn Backend>, results, THREADS);
let mut model = Model::new(options);
let size = terminal.size()?;
model.size = (size.width, size.height);
let started = Instant::now();
let mut last_tick: Option<Instant> = None;
let mut dirty = true;
loop {
let mut messages: Vec<Msg> = Vec::new();
if event::poll(POLL)? {
match event::read()? {
Event::Key(key) if key.kind != KeyEventKind::Release => {
messages.push(Msg::Key(key))
}
Event::Resize(width, height) => messages.push(Msg::Resize(width, height)),
Event::Paste(text) => messages.push(Msg::Paste(text)),
Event::FocusGained => messages.push(Msg::Focus(true)),
Event::FocusLost => messages.push(Msg::Focus(false)),
Event::Mouse(mouse) => match mouse.kind {
MouseEventKind::ScrollUp => messages.push(Msg::Scroll(-3)),
MouseEventKind::ScrollDown => messages.push(Msg::Scroll(3)),
_ => {}
},
_ => {}
}
}
while let Ok(reply) = replies.try_recv() {
messages.push(reply);
}
if last_tick.is_none_or(|t| t.elapsed() >= TICK) {
last_tick = Some(Instant::now());
messages.push(Msg::Tick {
now: u64::try_from(started.elapsed().as_millis()).unwrap_or(u64::MAX),
wall_ms: wall_ms(),
});
}
dirty |= !messages.is_empty();
for message in messages {
let effects = update(&mut model, message);
if !carry_out(effects, &mut model, &workers, &switch, host) {
return Ok(());
}
}
if dirty {
terminal.draw(|frame| view(frame, &model))?;
dirty = false;
}
}
}
#[cfg(test)]
mod tests;