repo_description: A new way to see and navigate directory trees : https://dystroy.org/broot
repo_topics: balanced-bfs-descent command-line command-line-tool fuzzy-search hacktoberfest linux rust tree
code: use {
      super::*,
      crate::{
          browser::BrowserState,
          cli::TriBool,
          command::{
              Command,
              Sequence,
          },
          conf::Conf,
          display::*,
          errors::ProgramError,
          file_sum,
          git,
          kitty,
          launchable::Launchable,
          path::closest_dir,
          pattern::InputPattern,
          preview::PreviewState,
          skin::*,
          syntactic::SyntaxTheme,
          task_sync::{
              Dam,
              Either,
          },
          terminal,
          verb::Internal,
          watcher::Watcher,
      },
      crokey::crossterm::event::Event,
      std::{
          io::Write,
          path::PathBuf,
          str::FromStr,
          sync::{
              Arc,
              Mutex,
          },
      },
      termimad::{
          EventSource,
          EventSourceOptions,
          crossbeam::channel::{
              Receiver,
              Sender,
              unbounded,
          },
      },
  };
  
  /// The GUI
  pub struct App {
      /// the panels of the application, with their inputs
      panels: AppPanelsAndInputs,
  
      /// whether the app is in the (uncancellable) process of quitting
      quitting: bool,
  
      /// what must be done after having closed the TUI
      launch_at_end: Option<Launchable>,
  
      /// an optional copy of the root for the --server
      shared_root: Option<Arc<Mutex<PathBuf>>>,
  
      /// sender to the sequence channel
      tx_seqs: Sender<Sequence>,
  
      /// receiver to listen to the sequence channel
      rx_seqs: Receiver<Sequence>,
  
      /// a watcher for notify events
      watcher: Watcher,
  }
  
  impl App {
      pub fn new(con: &AppContext) -> Result<App, ProgramError> {
          let mut panels = AppPanelsAndInputs::new(con)?;
          if let Some(path) = con.initial_file.as_ref() {
              // open initial_file in preview
              let preview_state = Box::new(PreviewState::new(
                  path.clone(),
                  InputPattern::none(),
                  None,
                  con.initial_tree_options.clone(),
                  con,
              ));
              if let Err(err) = panels.new_panel(
                  preview_state,
                  PanelPurpose::Preview,
                  HDir::Right,
                  true, // activate
                  con,
              ) {
                  warn!("could not open preview: {err}");
              }
          }
          let (tx_seqs, rx_seqs) = unbounded::<Sequence>();
          let watcher = Watcher::new(tx_seqs.clone());
          Ok(Self {
              panels,
              quitting: false,
              launch_at_end: None,
              shared_root: None,
              tx_seqs,
              rx_seqs,
              watcher,
          })
      }
  
      /// apply a command. Change the states but don't redraw on screen.
      fn apply_command(
          &mut self,
          w: &mut W,
          cmd: &Command,
          panel_skin: &PanelSkin,
          app_state: &mut AppState,
          con: &mut AppContext,
      ) -> Result<(), ProgramError> {
          info!("app applying command: {:?}", &cmd);
          let is_input_invocation = cmd.is_verb_invocated_from_input();
          let cmd_result = self
              .panels
              .apply_command(w, cmd, None, panel_skin, app_state, con)?;
          debug!("cmd_result: {:?}", &cmd_result);
          let mut error: Option<String> = None;
          let mut new_active_panel_idx = None;
          match cmd_result {
              CmdResult::ApplyOnPanel { id } => {
                  let aop_cmd_result = self.panels.apply_command(
                      w,
                      cmd,
                      Some(PanelReference::Id(id)),
                      panel_skin,
                      app_state,
                      con,
                  )?;
                  if let CmdResult::DisplayError(txt) = aop_cmd_result {
                      // we should probably handle other results
                      // which implies the possibility of a recursion
                      error = Some(txt);
                  } else if is_input_invocation {
                      self.panels.clear_input();
                  }
              }
              CmdResult::ClosePanel {
                  validate_purpose,
                  panel_ref,
                  clear_cache,
              } => {
                  if is_input_invocation {
                      self.panels.clear_input_invocation(con);
                  }
                  let close_idx = self.panels.idx_by_ref(panel_ref)
                      .unwrap_or_else(||
                          // when there's a preview panel, we close it rather than the app
                          if self.panels.len()==2 && self.panels.has_preview_panel() {
                              1
                          } else {
                              self.panels.active_panel_idx()
                          }
                      );
                  let mut new_arg = None;
                  if validate_purpose {
                      let purpose = &self.panels.panel_by_idx_unchecked(close_idx).purpose;
                      if let PanelPurpose::ArgEdition { .. } = purpose {
                          new_arg = self
                              .panels
                              .panel_by_idx_unchecked(close_idx)
                              .state()
                              .selected_path()
                              .map(|p| p.to_string_lossy().to_string());
                      }
                  }
                  if clear_cache {
                      clear_caches();
                  }
                  if self.panels.close(close_idx, con) {
                      let screen = self.panels.screen();
                      self.panels.refresh_active_panel(con);
                      if let Some(new_arg) = new_arg {
                          self.panels.set_input_arg(new_arg);
                          let new_input = self.panels.get_input_content();
                          let cmd = Command::from_raw(new_input, false);
                          let app_cmd_context = AppCmdContext {
                              panel_skin,
                              preview_panel: self.panels.preview_panel_id(),
                              stage_panel: self.panels.stage_panel_id(),
                              screen,
                              con,
                          };
                          self.panels.mut_panel().apply_command(
                              w,
                              &cmd,
                              app_state,
                              &app_cmd_context,
                          )?;
                      }
                  } else {
                      self.quitting = true;
                  }
              }
              CmdResult::ChangeLayout(instruction) => {
                  con.layout_instructions.push(instruction);
                  self.panels.resize_all(con);
              }
              CmdResult::DisplayError(txt) => {
                  error = Some(txt);
              }
              CmdResult::ExecuteSequence { sequence } => {
                  if is_input_invocation {
                      self.panels.clear_input();
                  }
                  self.tx_seqs.send(sequence).unwrap();
              }
              CmdResult::HandleInApp(internal) => {
                  debug!("handling internal {internal:?} at app level");
                  match internal {
                      Internal::escape => {
                          let mode = self.panels.state().get_mode();
                          let cmd = self.panels.do_input_escape(mode, con);
                          debug!("cmd on escape: {cmd:?}");
                          self.apply_command(w, &cmd, panel_skin, app_state, con)?;
                      }
                      Internal::focus_staging_area_no_open => {
                          self.panels.focus_by_type(PanelStateType::Stage);
                      }
                      Internal::focus_panel_left => {
                          let len = self.panels.len();
                          new_active_panel_idx =
                              Some((self.panels.active_panel_idx() + len - 1) % len);
                      }
                      Internal::focus_panel_right => {
                          let len = self.panels.len();
                          new_active_panel_idx = Some((self.panels.active_panel_idx() + 1) % len);
                      }
                      Internal::panel_left_no_open => {
                          // we're here because the state wants us to either move to the panel
                          // to the left, or close the rightest one
                          new_active_panel_idx = if self.panels.active_panel_idx() == 0 {
                              self.panels.close(self.panels.len() - 1, con);
                              None
                          } else {
                              Some(self.panels.active_panel_idx() - 1)
                          };
                      }
                      Internal::panel_right_no_open => {
                          // we either move to the right or close the leftest panel
                          new_active_panel_idx =
                              if self.panels.active_panel_idx() + 1 == self.panels.len() {
                                  self.panels.close(0, con);
                                  None
                              } else {
                                  Some(self.panels.active_panel_idx() + 1)
                              };
                      }
                      Internal::search_again => {
                          if let Some(raw_pattern) = &self.panels.panel().last_raw_pattern {
                              let sequence = Sequence::new_single(raw_pattern.clone());
                              self.tx_seqs.send(sequence).unwrap();
                          }
                      }
                      Internal::set_syntax_theme => {
                          let arg = cmd.as_verb_invocation().and_then(|vi| vi.args.as_ref());
                          match arg {
                              Some(arg) => match SyntaxTheme::from_str(arg) {
                                  Ok(theme) => {
                                      con.syntax_theme = Some(theme);
                                      self.panels.update_preview(true, con);
                                  }
                                  Err(e) => {
                                      error = Some(e.to_string());
                                  }
                              },
                              None => {
                                  error = Some("no theme provided".to_string());
                              }
                          }
                      }
                      Internal::toggle_second_tree => {
                          let panels_count = self.panels.len();
                          let trees_count = self.panels.count_of_type(PanelStateType::Tree);
                          if trees_count < 2 {
                              // we open a tree, closing a (non tree) panel if necessary
                              if panels_count >= con.max_panels_count {
                                  self.panels.close_first_non_tree(con);
                              }
                              if let Some(selected_path) = self.panels.state().selected_path() {
                                  let dir = closest_dir(selected_path);
                                  let screen = self.panels.screen();
                                  if let Ok(new_state) = BrowserState::new(
                                      dir,
                                      self.panels.state().tree_options().without_pattern(),
                                      screen,
                                      con,
                                      &Dam::unlimited(),
                                  ) {
                                      if let Err(s) = self.panels.new_panel(
                                          Box::new(new_state),
                                          PanelPurpose::None,
                                          HDir::Right,
                                          is_input_invocation,
                                          con,
                                      ) {
                                          error = Some(s);
                                      }
                                  }
                              }
                          } else {
                              self.panels.close_rightest_inactive_tree(con);
                          }
                      }
                      Internal::toggle_watch => {
                          app_state.watch_tree ^= true;
                          if is_input_invocation {
                              self.panels.clear_input_invocation(con);
                          }
                      }
                      _ => {
                          let cmd = self.panels.on_input_internal(internal);
                          if cmd.is_none() {
                              warn!(
                                  "unhandled propagated internal. internal={internal:?} cmd={cmd:?}"
                              );
                          } else {
                              self.apply_command(w, &cmd, panel_skin, app_state, con)?;
                          }
                      }
                  }
              }
              CmdResult::Keep => {
                  if is_input_invocation {
                      self.panels.clear_input_invocation(con);
                  }
              }
              CmdResult::Message(md) => {
                  if is_input_invocation {
                      self.panels.clear_input_invocation(con);
                  }
                  self.panels.mut_panel().set_message(md);
              }
              CmdResult::Launch(launchable) => {
                  self.launch_at_end = Some(*launchable);
                  self.quitting = true;
              }
              CmdResult::NewPanel {
                  state,
                  purpose,
                  direction,
              } => {
                  if let Err(s) =
                      self.panels
                          .new_panel(state, purpose, direction, is_input_invocation, con)
                  {
                      error = Some(s);
                  }
              }
              CmdResult::NewState { state, message } => {
                  self.panels.clear_input();
                  self.panels.push_state(state);
                  if let Some(md) = message {
                      self.panels.mut_panel().set_message(md);
                  } else {
                      self.panels.refresh_input_status(app_state, panel_skin, con);
                  }
              }
              CmdResult::PopState => {
                  if is_input_invocation {
                      self.panels.clear_input();
                  }
                  if self.panels.remove_state(con) {
                      let screen = self.panels.screen();
                      self.panels.mut_state().refresh(screen, con);
                      self.panels.refresh_input_status(app_state, panel_skin, con);
                  } else if con.quit_on_last_cancel {
                      self.quitting = true;
                  }
              }
              CmdResult::PopStateAndReapply => {
                  if is_input_invocation {
                      self.panels.clear_input();
                  }
                  if self.panels.remove_state(con) {
                      self.panels.apply_command(
                          w, cmd, None, // active panel
                          panel_skin, app_state, con,
                      )?;
                  } else if con.quit_on_last_cancel {
                      self.quitting = true;
                  }
              }
              CmdResult::Quit => {
                  self.quitting = true;
              }
              CmdResult::RefreshState { clear_cache } => {
                  info!("refreshing, clearing cache={clear_cache}");
                  if is_input_invocation {
                      self.panels.clear_input_invocation(con);
                  }
                  if clear_cache {
                      clear_caches();
                  }
                  app_state.stage.refresh();
                  self.panels.refresh_all_panels(con);
              }
          }
          if let Some(text) = error {
              self.panels.mut_panel().set_error(text);
          }
  
          if let Some(idx) = new_active_panel_idx {
              debug!("activating panel idx {idx}");
              if is_input_invocation {
                  self.panels.clear_input();
              }
              self.panels.activate(idx);
              self.panels.refresh_input_status(app_state, panel_skin, con);
          }
  
          app_state.other_panel_path = self.panels.get_other_panel_path();
          if let Some(path) = self.panels.state().tree_root() {
              app_state.root = path.to_path_buf();
              terminal::update_title(w, app_state, con);
              if con.update_work_dir {
                  if let Err(e) = std::env::set_current_dir(&app_state.root) {
                      warn!("Failed to set current dir: {e}");
                  }
              }
              if let Some(shared_root) = &mut self.shared_root {
                  if let Ok(mut root) = shared_root.lock() {
                      root.clone_from(&app_state.root);
                  }
              }
          }
  
          self.panels.update_preview(false, con);
  
          Ok(())
      }
  
      /// This is the main loop of the application
      pub fn run(
          mut self,
          w: &mut W,
          con: &mut AppContext,
          conf: &Conf,
      ) -> Result<Option<Launchable>, ProgramError> {
          #[cfg(feature = "clipboard")]
          {
              // different systems have different clipboard capabilities
              // and it may be useful to know which one we have
              debug!("Clipboard backend: {:?}", terminal_clipboard::get_type());
          }
          // we listen for events in a separate thread so that we can go on listening
          // when a long search is running, and interrupt it if needed
          w.flush()?;
          let combine_keys = conf.enable_kitty_keyboard.unwrap_or(false) && con.is_tty;
          let event_source = EventSource::with_options(EventSourceOptions {
              combine_keys,
              ..Default::default()
          })?;
          con.keyboard_enhanced = event_source.supports_multi_key_combinations();
          info!(
              "event source is combining: {}",
              event_source.supports_multi_key_combinations()
          );
  
          let rx_events = event_source.receiver();
          let mut dam = Dam::from(rx_events);
          let skin = AppSkin::new(conf, con.launch_args.color == TriBool::No);
          let mut app_state = AppState::new(&con.initial_root);
          terminal::update_title(w, &app_state, con);
  
          self.panels
              .screen()
              .clear_bottom_right_char(w, &skin.focused)?;
  
          #[cfg(windows)]
          if con.cmd().is_some() {
              // Powershell sends to broot a resize event after it was launched
              // which interrupts its task queue. An easy fix is to wait for a
              // few ms for the terminal to be stabilized.
              // It's possible some other terminals, even not on Windows, might
              // need the same trick in the future
              let delay = std::time::Duration::from_millis(10);
              std::thread::sleep(delay);
              let dropped_events = dam.clear();
              debug!("Dropped {dropped_events} events");
              event_source.unblock(self.quitting);
          }
  
          if let Some(raw_sequence) = &con.cmd() {
              self.tx_seqs
                  .send(Sequence::new_local((*raw_sequence).to_string()))
                  .map_err(|e| ProgramError::Internal {
                      details: format!("failed to send initial command: {e}"),
                  })?;
          }
  
          #[cfg(unix)]
          let _server = con
              .server_name
              .as_ref()
              .map(|server_name| {
                  let shared_root = Arc::new(Mutex::new(app_state.root.clone()));
                  let server = crate::net::Server::new(
                      server_name,
                      self.tx_seqs.clone(),
                      Arc::clone(&shared_root),
                  );
                  self.shared_root = Some(shared_root);
                  server
              })
              .transpose()?;
  
          loop {
              if !self.quitting {
                  self.panels.display_panels(w, &skin, &app_state, con)?;
                  time!(
                      Debug,
                      "pending_tasks",
                      self.panels
                          .do_pending_tasks(w, &skin, &mut dam, &mut app_state, con)?,
                  );
              }
  
              // before starting to wait for events, we enable the watcher if needed
              if app_state.watch_tree {
                  let paths = self.panels.state().watchable_paths();
                  if let Err(e) = self.watcher.watch(paths) {
                      // errors aren't uncommon, especially on huge directories
                      warn!("Failed to watch tree: {e}");
                      // we disable watching
                      app_state.watch_tree = false;
                  }
              }
              let event = dam.next(&self.rx_seqs);
              if app_state.watch_tree {
                  // we must unwatch before applying the command, as it will probably do many system
                  // calls that would trigger events
                  self.watcher.stop_watching()?;
              }
  
              #[allow(unused_mut)]
              match event {
                  Either::First(Some(event)) => {
                      info!("<-- event: {:?}", &event);
                      if let Some(key_combination) = event.key_combination {
                          info!("key combination: {key_combination}");
                      }
                      let mut handled = false;
  
                      // app level handling
                      if let Some((x, y)) = event.as_click() {
                          let clicked_idx = self.panels.clicked_panel_index(x, y);
                          if clicked_idx != self.panels.active_panel_idx() {
                              // panel activation click
                              self.panels.activate(clicked_idx);
                              handled = true;
                          }
                      } else if let Event::Resize(mut width, mut height) = event.event {
                          self.panels.set_terminal_size(width, height, con);
                          handled = true;
                      }
  
                      // event handled by the panel
                      if !handled {
                          let cmd = self.panels.on_input_event(w, &event, &app_state, con)?;
                          info!("command from panels.on_input_event: {:#?}", &cmd);
                          self.apply_command(w, &cmd, &skin.focused, &mut app_state, con)?;
                      }
  
                      event_source.unblock(self.quitting);
                  }
                  Either::First(None) => {
                      // This is how we quit the application,
                      // when the input thread is properly closed
                      break;
                  }
                  Either::Second(Some(sequence)) => {
                      info!("got command sequence: {:?}", &sequence);
                      for (input, arg_cmd) in sequence.parse(con)? {
                          if !matches!(&arg_cmd, Command::Internal { .. }) {
                              self.panels.input().set_content(&input);
                          }
                          self.apply_command(w, &arg_cmd, &skin.focused, &mut app_state, con)?;
                          if self.quitting {
                              return Ok(self.launch_at_end.take());
                          }
                          self.panels.display_panels(w, &skin, &app_state, con)?;
                          time!(
                              "sequence pending tasks",
                              self.panels.do_pending_tasks(
                                  w,
                                  &skin,
                                  &mut dam,
                                  &mut app_state,
                                  con
                              )?,
                          );
                      }
                  }
                  Either::Second(None) => {
                      warn!("I didn't expect a None to occur here");
                  }
              }
          }
          terminal::reset_title(w, con);
          if let Ok(mut manager) = kitty::manager().lock() {
              manager.erase_images_before(w, usize::MAX)?;
          }
          w.flush()?;
  
          Ok(self.launch_at_end.take())
      }
  }
  
  /// clear the file sizes and git stats cache.
  ///
  /// This should be done on Refresh actions and after any external command.
  fn clear_caches() {
      file_sum::clear_cache();
      git::clear_status_computer_cache();
      #[cfg(any(target_os = "macos", target_os = "linux", target_os = "windows"))]
      crate::filesystems::clear_cache();
  }
  