use {
crate::{
app_context::AppContext,
app_state::{AppState, AppStateCmdResult},
browser_states::BrowserState,
command_parsing::parse_command_sequence,
commands::Command,
errors::ProgramError,
external::Launchable,
file_sizes,
git_status_computer,
io::WriteCleanup,
screens::Screen,
skin::Skin,
status::Status,
task_sync::Dam,
},
crossterm::{
self, cursor,
event::{DisableMouseCapture, EnableMouseCapture},
terminal::{EnterAlternateScreen, LeaveAlternateScreen},
QueueableCommand,
},
minimad::Composite,
std::io::Write,
termimad::EventSource,
};
#[inline]
fn just_queue(
mut writer: impl Write,
command: impl crossterm::Command,
) -> crossterm::Result<()> {
writer.queue(command).map(move |_| ())
}
pub struct App {
states: Vec<Box<dyn AppState>>, quitting: bool,
launch_at_end: Option<Launchable>, }
impl App {
pub fn new() -> App {
App {
states: Vec::new(),
quitting: false,
launch_at_end: None,
}
}
pub fn push(&mut self, new_state: Box<dyn AppState>) {
self.states.push(new_state);
}
fn mut_state(&mut self) -> &mut dyn AppState {
self.states
.last_mut()
.expect("No path has been pushed")
.as_mut()
}
fn state(&self) -> &dyn AppState {
self.states
.last()
.expect("No path has been pushed")
.as_ref()
}
fn do_pending_tasks(
&mut self,
w: &mut impl Write,
cmd: &Command,
screen: &mut Screen,
con: &AppContext,
dam: &mut Dam,
) -> Result<(), ProgramError> {
let state = self.mut_state();
while state.has_pending_task() & !dam.has_event() {
state.do_pending_task(screen, dam);
state.display(w, screen, con)?;
state.write_status(w, cmd, &screen, con)?;
}
Ok(())
}
fn apply_command(
&mut self,
w: &mut impl Write,
mut cmd: Command,
screen: &mut Screen,
con: &AppContext,
) -> Result<Command, ProgramError> {
debug!("action: {:?}", &cmd.action);
let mut error: Option<String> = None;
match self.mut_state().apply(&mut cmd, screen, con)? {
AppStateCmdResult::Quit => {
debug!("cmd result quit");
self.quitting = true;
}
AppStateCmdResult::Launch(launchable) => {
self.launch_at_end = Some(*launchable);
self.quitting = true;
}
AppStateCmdResult::NewState(boxed_state, new_cmd) => {
self.push(boxed_state);
cmd = new_cmd;
}
AppStateCmdResult::RefreshState { clear_cache } => {
if clear_cache {
clear_caches();
}
cmd = self.mut_state().refresh(screen, con);
}
AppStateCmdResult::PopState => {
if self.states.len() == 1 {
debug!("quitting on last pop state");
self.quitting = true;
} else {
self.states.pop();
cmd = self.mut_state().refresh(screen, con);
}
}
AppStateCmdResult::PopStateAndReapply => {
if self.states.len() == 1 {
debug!("quitting on last pop state");
self.quitting = true;
} else {
self.states.pop();
debug!("about to reapply {:?}", &cmd);
return self.apply_command(w, cmd, screen, con);
}
}
AppStateCmdResult::DisplayError(txt) => {
error = Some(txt);
}
_ => {}
}
self.mut_state().display(w, screen, con)?;
match error {
Some(text) => Status::from_error(Composite::from_inline(&text)).display(w, screen)?,
None => self.state().write_status(w, &cmd, screen, con)?,
}
screen.input_field.set_content(&cmd.raw);
screen.input_field.display_on(w)?;
self.state().write_flags(w, screen, con)?;
Ok(cmd)
}
pub fn run(
mut self,
writer: impl Write,
con: &AppContext,
skin: Skin,
) -> Result<Option<Launchable>, ProgramError> {
let writer = WriteCleanup::new(writer, |w| w.flush());
let writer = WriteCleanup::build(
writer,
|w| just_queue(w, EnterAlternateScreen),
|w| just_queue(w, LeaveAlternateScreen),
)?;
let writer = WriteCleanup::build(
writer,
|w| just_queue(w, cursor::Hide),
|w| just_queue(w, cursor::Show),
)?;
debug!("we're on screen");
let mut screen = Screen::new(con, skin)?;
let mut writer = WriteCleanup::build(
writer,
|w| just_queue(w, EnableMouseCapture),
|w| just_queue(w, DisableMouseCapture),
)?;
let event_source = EventSource::new()?;
let rx_events = event_source.receiver();
let mut dam = Dam::from(rx_events);
self.push(Box::new(
BrowserState::new(
con.launch_args.root.clone(),
con.launch_args.tree_options.clone(),
&screen,
&Dam::unlimited(),
)?
.expect("Failed to create BrowserState"),
));
let mut cmd = Command::new();
if let Some(unparsed_commands) = &con.launch_args.commands {
for arg_cmd in parse_command_sequence(unparsed_commands, con)? {
cmd = self.apply_command(&mut writer, arg_cmd, &mut screen, con)?;
self.do_pending_tasks(
&mut writer,
&cmd,
&mut screen,
con,
&mut dam,
)?;
if self.quitting {
return Ok(self.launch_at_end.take());
}
}
}
let state = self.mut_state();
state.display(&mut writer, &screen, con)?;
state.write_status(&mut writer, &cmd, &screen, con)?;
state.write_flags(&mut writer, &mut screen, con)?;
screen.input_field.display_on(&mut writer)?;
loop {
if !self.quitting {
self.do_pending_tasks(&mut writer, &cmd, &mut screen, con, &mut dam)?;
}
let event = match dam.next_event() {
Some(event) => event,
None => {
break;
}
};
cmd.add_event(&event, &mut screen.input_field, con, self.state());
debug!("command after add_event: {:?}", &cmd);
cmd = self.apply_command(&mut writer, cmd, &mut screen, con)?;
event_source.unblock(self.quitting);
}
Ok(self.launch_at_end.take())
}
}
impl Drop for App {
fn drop(&mut self) {
debug!("we left the screen");
}
}
fn clear_caches() {
file_sizes::clear_cache();
git_status_computer::clear_cache();
}