use std::time::{Duration, Instant};
use eframe::egui::{self, FontId, Vec2};
use ratatui::Terminal;
use ratatui::crossterm::event::KeyEvent;
use rcmd_tui::app::{App, Exec};
use rcmd_tui::{state, ui};
use crate::exec;
use crate::grid::{EguiBackend, Metrics, Palette};
use crate::keys::{self, Input};
use crate::term::TerminalPane;
const IDLE_FRAME: Duration = Duration::from_secs(2);
pub struct Gui {
app: App,
terminal: Terminal<EguiBackend>,
font: FontId,
metrics: Metrics,
palette: Palette,
size: (u16, u16),
last_frame: Instant,
startup_keys: Vec<KeyEvent>,
pane: TerminalPane,
saved: bool,
}
impl Gui {
pub fn new(
cc: &eframe::CreationContext<'_>,
app: App,
font_size: f32,
startup_keys: Vec<KeyEvent>,
) -> anyhow::Result<Self> {
crate::font::install(&cc.egui_ctx);
let font = FontId::monospace(font_size);
let metrics = Metrics::estimate(font_size);
let palette = palette_from_theme();
let terminal = Terminal::new(EguiBackend::new(80, 25, palette))?;
Ok(Self {
app,
terminal,
font,
metrics,
palette,
size: (80, 25),
last_frame: Instant::now(),
startup_keys,
pane: TerminalPane::new(80, 25),
saved: false,
})
}
fn run_exec(&mut self, cmd: Exec) {
let quiet = matches!(cmd, Exec::Quiet(_));
if !quiet && self.app.subshell_alive() {
let (cols, rows) = self.size;
self.pane.open(&mut self.app, cmd, cols, rows);
self.app.set_dirty();
return;
}
let cwd = self.app.panels[self.app.active].local_cwd();
match exec::run(&cmd, &cwd) {
Ok(Some(note)) => self.app.status = Some(format!(" {note} ")),
Ok(None) => {}
Err(err) => self.app.status = Some(format!(" {err} ")),
}
self.app.finish_remote_edit();
self.app.set_dirty();
}
fn pane_frame(&mut self, ui: &mut egui::Ui, origin: egui::Pos2, input: Vec<Input>) -> Duration {
let pane = &mut self.pane;
let (cols, rows) = self.size;
pane.resize(&mut self.app, cols, rows);
let open = pane.feed(&mut self.app, &input) & pane.step(&mut self.app);
pane.paint(ui.painter(), origin, self.metrics, &self.font, self.palette);
let wait = pane.repaint_after();
if !open {
pane.close();
self.app.end_subshell();
self.app.finish_remote_edit();
self.app.set_dirty();
return Duration::ZERO;
}
wait
}
fn save_once(&mut self) {
if self.saved {
return;
}
self.saved = true;
self.app.cancel_background();
if let Err(err) = state::save_session(&self.app) {
eprintln!("rcmd-egui: could not save state: {err}");
}
}
}
impl eframe::App for Gui {
fn clear_color(&self, _visuals: &egui::Visuals) -> [f32; 4] {
let bg = self.palette.bg;
[
bg.r() as f32 / 255.0,
bg.g() as f32 / 255.0,
bg.b() as f32 / 255.0,
1.0,
]
}
fn ui(&mut self, ui: &mut egui::Ui, _frame: &mut eframe::Frame) {
let ctx = ui.ctx().clone();
self.metrics = Metrics::measure(&ctx, &self.font);
let available = ui.max_rect();
let origin = available.min;
let (cols, rows) = self.metrics.cells(available.size());
if (cols, rows) != self.size {
self.size = (cols, rows);
self.terminal.backend_mut().set_size(cols, rows);
self.app.resize_subshell(cols, rows);
self.app.set_dirty();
}
let mut input = ctx.input(|i| keys::collect(i, origin, self.metrics));
if !self.startup_keys.is_empty() {
let key = self.startup_keys.remove(0);
input.insert(0, Input::Key(key));
ctx.request_repaint();
}
if self.pane.is_open() {
let wait = self.pane_frame(ui, origin, input);
ctx.request_repaint_after(wait);
return;
}
if !input.is_empty() {
self.app.set_dirty();
for event in input {
match event {
Input::Key(key) => self.app.on_key(key),
Input::Mouse(mouse) => self.app.on_mouse(mouse),
}
}
}
let busy = self.app.tick();
if self.app.dirty() || busy || self.last_frame.elapsed() >= IDLE_FRAME {
if self.app.take_repaint() {
let _ = self.terminal.clear();
}
let Self { app, terminal, .. } = self;
if let Err(err) = terminal.draw(|frame| ui::draw(frame, app)) {
eprintln!("rcmd-egui: draw failed: {err}");
}
self.app.clear_dirty();
self.last_frame = Instant::now();
}
self.terminal
.backend()
.paint(ui.painter(), origin, self.metrics, &self.font);
if let Some(cmd) = self.app.take_exec() {
self.run_exec(cmd);
}
self.app.hold_quit_for_jobs();
if self.app.exiting() {
self.save_once();
ctx.send_viewport_cmd(egui::ViewportCommand::Close);
return;
}
ctx.request_repaint_after(Duration::from_millis(if busy { 50 } else { 500 }));
}
fn on_exit(&mut self, _gl: Option<&eframe::glow::Context>) {
self.save_once();
}
}
pub fn window_size(font_size: f32) -> Vec2 {
let metrics = Metrics::estimate(font_size);
Vec2::new(metrics.width * 100.0, metrics.height * 30.0)
}
fn palette_from_theme() -> Palette {
let (fg, bg) = ui::base_colors();
let fallback = Palette::default();
Palette {
fg: crate::grid::to_color32(fg, fallback.fg),
bg: crate::grid::to_color32(bg, fallback.bg),
}
}