use self::main_ui::MainUI;
use crate::cli::ResolvedConnectArgs;
use crate::config::Theme;
use crate::files::FileEntry;
use crate::ssh::Session;
use async_lock::Mutex;
use color_eyre::Report as Error;
use color_eyre::eyre::Result;
use rat_salsa::event::RenderedEvent;
use rat_salsa::poll::{PollCrossterm, PollRendered, PollTasks, PollTimers};
use rat_salsa::timer::TimeOut;
use rat_salsa::{Control, RunConfig, SalsaAppContext, SalsaContext, run_tui};
use rat_theme3::{SalsaTheme, create_theme};
use rat_widget::event::{ConsumedEvent, Dialog, HandleEvent, Regular, ct_event};
use rat_widget::focus::FocusBuilder;
use rat_widget::layout::layout_middle;
use rat_widget::msgdialog::{MsgDialog, MsgDialogState};
use rat_widget::statusline::{StatusLine, StatusLineState};
use ratatui::buffer::Buffer;
use ratatui::crossterm::cursor::Show;
use ratatui::crossterm::terminal::{LeaveAlternateScreen, disable_raw_mode};
use ratatui::crossterm::{self, ExecutableCommand};
use ratatui::layout::{Constraint, Layout, Rect};
use ratatui::widgets::StatefulWidget;
use russh_sftp::client::SftpSession;
use std::io::stdout;
use std::path::PathBuf;
use std::sync::Arc;
use std::time::{Duration, SystemTime};
use tracing::error;
pub mod main_ui;
pub fn tui(
current_path: String,
cli: ResolvedConnectArgs,
rt: tokio::runtime::Runtime,
sftp: Arc<SftpSession>,
session: Arc<Mutex<Session>>,
theme: &Theme,
) -> Result<(), Error> {
let config = Config::new(cli);
let theme = match theme {
Theme::Custom(c) => c.clone().into(),
Theme::Default(d) => create_theme(&d.to_string()).expect("theme"),
};
let mut global = Global::new(config, theme);
let mut state = Scenery::new(current_path, sftp, session);
run_tui(
init, render,
event,
error,
&mut global,
&mut state,
RunConfig::default()?
.poll(PollCrossterm)
.poll(PollTimers::default())
.poll(PollTasks::default())
.poll(PollRendered)
.poll(rat_salsa::poll::PollTokio::new(rt)),
)?;
Ok(())
}
#[allow(dead_code)]
pub struct Global {
pub ctx: SalsaAppContext<AppEvent, Error>,
pub cfg: Config,
pub theme: Box<dyn SalsaTheme>,
}
impl SalsaContext<AppEvent, Error> for Global {
fn set_salsa_ctx(&mut self, app_ctx: SalsaAppContext<AppEvent, Error>) {
self.ctx = app_ctx;
}
fn salsa_ctx(&self) -> &SalsaAppContext<AppEvent, Error> {
&self.ctx
}
}
impl Global {
pub fn new(cfg: Config, theme: Box<dyn SalsaTheme>) -> Self {
Self {
ctx: Default::default(),
cfg,
theme,
}
}
}
#[derive(Debug, Default)]
pub struct Config {
pub(crate) cli: ResolvedConnectArgs,
}
impl Config {
pub fn new(cli: ResolvedConnectArgs) -> Self {
Self { cli }
}
}
#[derive(Debug)]
#[allow(dead_code)]
pub enum AppEvent {
Timer(TimeOut),
Event(crossterm::event::Event),
ChangeDir(String),
DownloadStart,
DownloadEnd,
UpdateCurrentPath(String),
Throb,
Gauge(f64),
SetTotalFilesToDownload(usize),
UpdateContent(Option<String>),
UpdateFiles(Vec<FileEntry>),
SpawnExternalEditor(String),
SpawnSSHCommand,
DownloadFile(String, PathBuf, Option<String>),
DownloadFolder(String, PathBuf),
DeleteEntry(FileEntry),
MoveEntry(String, String),
Rendered,
Message(String),
Status(usize, String),
AsyncMsg(String),
UpdateNextFiveFiles(Vec<FileEntry>),
AsyncTick(u32),
}
impl From<RenderedEvent> for AppEvent {
fn from(_: RenderedEvent) -> Self {
Self::Rendered
}
}
impl From<TimeOut> for AppEvent {
fn from(value: TimeOut) -> Self {
Self::Timer(value)
}
}
impl From<crossterm::event::Event> for AppEvent {
fn from(value: crossterm::event::Event) -> Self {
Self::Event(value)
}
}
pub struct Scenery {
pub async1: MainUI,
pub status: StatusLineState,
pub error_dlg: MsgDialogState,
}
impl Scenery {
pub fn new(current_path: String, sftp: Arc<SftpSession>, session: Arc<Mutex<Session>>) -> Self {
Self {
async1: MainUI::new(current_path, sftp, session),
status: StatusLineState::default(),
error_dlg: MsgDialogState::default(),
}
}
}
pub fn render(
area: Rect,
buf: &mut Buffer,
state: &mut Scenery,
ctx: &mut Global,
) -> Result<(), Error> {
let t0 = SystemTime::now();
let layout = Layout::vertical([
Constraint::Fill(1), Constraint::Length(1),
])
.split(area);
main_ui::render(layout[0], buf, &mut state.async1, ctx)?;
if state.error_dlg.active() {
MsgDialog::new()
.styles(ctx.theme.msg_dialog_style())
.render(
layout_middle(
layout[0],
Constraint::Percentage(20),
Constraint::Percentage(20),
Constraint::Length(1),
Constraint::Length(1),
),
buf,
&mut state.error_dlg,
);
}
let el = t0.elapsed().unwrap_or(Duration::from_nanos(0));
state.status.status(1, format!("R {:.0?}", el).to_string());
let remote_host_details = format!(
" Connected to {}@{}:{} ",
ctx.cfg.cli.username.as_ref().map_or("root", |s| s.as_str()),
ctx.cfg.cli.host.as_str(),
ctx.cfg.cli.port
);
let len = remote_host_details.len();
state.status.status(3, remote_host_details);
StatusLine::new()
.layout([
Constraint::Fill(1),
Constraint::Length(8),
Constraint::Length(8),
Constraint::Length(len as u16),
])
.styles_ext(ctx.theme.statusline_style_ext())
.render(layout[1], buf, &mut state.status);
Ok(())
}
pub fn init(state: &mut Scenery, ctx: &mut Global) -> Result<(), Error> {
ctx.set_focus(FocusBuilder::build_for(&state.async1));
main_ui::init(&mut state.async1, ctx)?;
Ok(())
}
pub fn event(
event: &AppEvent,
state: &mut Scenery,
ctx: &mut Global,
) -> Result<Control<AppEvent>, Error> {
let t0 = SystemTime::now();
if state.async1.in_editor {
return Ok(Control::Changed);
}
let mut r = match event {
AppEvent::Event(event) => {
let mut r = match &event {
ct_event!(resized) => Control::Changed,
ct_event!(key press CONTROL-'q') => {
if let Some(cancel) = state.async1.throbber_cancel.take() {
cancel.cancel();
}
let session = Arc::clone(&state.async1.session);
ctx.spawn_async_ext(async move |_| {
let mut session = session.lock().await;
session.close().await?;
Ok(Control::Quit)
});
Control::Quit
}
_ => Control::Continue,
};
r = r.or_else(|| {
if state.error_dlg.active() {
state.error_dlg.handle(event, Dialog).into()
} else {
Control::Continue
}
});
let f = ctx.focus_mut().handle(event, Regular);
ctx.queue(f);
r
}
AppEvent::Rendered => {
ctx.set_focus(FocusBuilder::rebuild_for(&state.async1, ctx.take_focus()));
Control::Continue
}
AppEvent::Message(s) => {
state.error_dlg.append(s);
Control::Changed
}
AppEvent::Status(n, s) => {
state.status.status(*n, s);
Control::Changed
}
_ => Control::Continue,
};
r = r.or_else_try(|| main_ui::event(event, &mut state.async1, ctx))?;
let el = t0.elapsed()?;
state.status.status(2, format!("E {:.0?}", el).to_string());
Ok(r)
}
pub fn error(
event: Error,
state: &mut Scenery,
_ctx: &mut Global,
) -> Result<Control<AppEvent>, Error> {
error!("{:?}", &*event);
state.error_dlg.append(format!("{:?}", &*event).as_str());
Ok(Control::Changed)
}
impl Drop for Scenery {
fn drop(&mut self) {
if let Some(cancel) = self.async1.throbber_cancel.take() {
cancel.cancel();
}
disable_raw_mode().unwrap();
stdout().execute(LeaveAlternateScreen).unwrap();
stdout().execute(Show).unwrap();
}
}