diff --git a/src/app.rs b/src/app.rs index 526202d..6b70890 100644 --- a/src/app.rs +++ b/src/app.rs @@ -5,7 +5,6 @@ use anyhow::{bail, Result}; use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use std::cmp::Ordering; -use std::collections::BinaryHeap; use std::collections::HashMap; use std::collections::HashSet; use std::collections::VecDeque; @@ -528,12 +527,24 @@ pub enum ExternalMsg { /// Example: `Call: {command: bash, args: ["-c", "read -p test"]}` Call(Command), - /// An alias to `Call: {command: bash, args: ["-c", "${command}"]}` + /// Like `Call` but without the flicker. The stdin, stdout + /// stderr will be piped to null. So it's non-interactive. + /// + /// Example: `CallSilently: {command: tput, args: ["bell"]}` + CallSilently(Command), + + /// An alias to `Call: {command: bash, args: ["-c", "${command}"], silent: false}` /// where ${command} is the given value. /// /// Example: `BashExec: "read -p test"` BashExec(String), + /// Like `BashExec` but without the flicker. The stdin, stdout + /// stderr will be piped to null. So it's non-interactive. + /// + /// Example: `BashExecSilently: "tput bell"` + BashExecSilently(String), + /// Select the focused node. Select, @@ -626,6 +637,8 @@ pub enum MsgOut { PrintAppStateAndQuit, Debug(String), Call(Command), + CallSilently(Command), + Enque(Task), } #[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)] @@ -727,7 +740,6 @@ pub struct App { config: Config, pwd: String, directory_buffers: HashMap, - tasks: BinaryHeap, selection: Vec, msg_out: VecDeque, mode: Mode, @@ -795,7 +807,6 @@ impl App { config, pwd: pwd.to_string_lossy().to_string(), directory_buffers: Default::default(), - tasks: Default::default(), selection: Default::default(), msg_out: Default::default(), mode, @@ -819,7 +830,7 @@ impl App { } pub fn enqueue(mut self, task: Task) -> Self { - self.tasks.push(task); + self.msg_out.push_back(MsgOut::Enque(task)); self } @@ -871,7 +882,9 @@ impl App { ExternalMsg::ResetInputBuffer => self.reset_input_buffer(), ExternalMsg::SwitchMode(mode) => self.switch_mode(&mode), ExternalMsg::Call(cmd) => self.call(cmd), + ExternalMsg::CallSilently(cmd) => self.call_silently(cmd), ExternalMsg::BashExec(cmd) => self.bash_exec(cmd), + ExternalMsg::BashExecSilently(cmd) => self.bash_exec_silently(cmd), ExternalMsg::Select => self.select(), ExternalMsg::UnSelect => self.un_select(), ExternalMsg::ToggleSelection => self.toggle_selection(), @@ -1093,10 +1106,10 @@ impl App { self.change_directory(&parent.to_string_lossy().to_string())? .focus_by_file_name(&filename.to_string_lossy().to_string()) } else { - Ok(self) + bail!("invalid path {}", path) } } else { - Ok(self) + self.change_directory("/") } } @@ -1122,6 +1135,11 @@ impl App { Ok(self) } + fn call_silently(mut self, command: Command) -> Result { + self.msg_out.push_back(MsgOut::CallSilently(command)); + Ok(self) + } + fn bash_exec(self, script: String) -> Result { self.call(Command { command: "bash".into(), @@ -1129,6 +1147,13 @@ impl App { }) } + fn bash_exec_silently(self, script: String) -> Result { + self.call_silently(Command { + command: "bash".into(), + args: vec!["-c".into(), script], + }) + } + fn add_directory(mut self, parent: String, dir: DirectoryBuffer) -> Result { self.directory_buffers.insert(parent, dir); self.msg_out.push_back(MsgOut::Refresh); @@ -1421,9 +1446,4 @@ impl App { pub fn version(&self) -> &String { &self.version } - - /// Get a reference to the app's tasks. - pub fn pop_task_out(&mut self) -> Option { - self.tasks.pop() - } } diff --git a/src/config.rs b/src/config.rs index 229d6d8..ab72bdc 100644 --- a/src/config.rs +++ b/src/config.rs @@ -333,7 +333,7 @@ impl Default for KeyBindings { help: rename messages: - SwitchMode: rename - - BashExec: | + - BashExecSilently: | echo "SetInputBuffer: $(basename ${XPLR_FOCUS_PATH})" >> "${XPLR_PIPE_MSG_IN:?}" ".": @@ -586,7 +586,7 @@ impl Default for Config { x: help: open in gui messages: - - BashExec: | + - BashExecSilently: | OPENER="$(which xdg-open)" ${OPENER:-open} "${XPLR_FOCUS_PATH:?}" &> /dev/null - SwitchMode: default @@ -809,11 +809,12 @@ impl Default for Config { enter: help: create file messages: - - BashExec: | + - BashExecSilently: | PTH="${XPLR_INPUT_BUFFER:?}" if touch "${PTH:?}"; then - echo "LogSuccess: $PTH created" >> "${XPLR_PIPE_MSG_IN:?}" echo Explore >> "${XPLR_PIPE_MSG_IN:?}" + echo "LogSuccess: $PTH created" >> "${XPLR_PIPE_MSG_IN:?}" + echo "FocusPath: $PTH" >> "${XPLR_PIPE_MSG_IN:?}" else echo "LogError: failed to create $PTH" >> "${XPLR_PIPE_MSG_IN:?}" echo Refresh >> "${XPLR_PIPE_MSG_IN:?}" @@ -850,11 +851,12 @@ impl Default for Config { enter: help: create directory messages: - - BashExec: | + - BashExecSilently: | PTH="${XPLR_INPUT_BUFFER:?}" if mkdir -p "$PTH"; then echo Explore >> "${XPLR_PIPE_MSG_IN:?}" echo "LogSuccess: $PTH created" >> "${XPLR_PIPE_MSG_IN:?}" + echo "FocusPath: $PTH" >> "${XPLR_PIPE_MSG_IN:?}" else echo "LogError: failed to create $PTH" >> "${XPLR_PIPE_MSG_IN:?}" fi @@ -890,12 +892,13 @@ impl Default for Config { enter: help: rename messages: - - BashExec: | + - BashExecSilently: | SRC="${XPLR_FOCUS_PATH:?}" TARGET="${XPLR_INPUT_BUFFER:?}" if mv -v "${SRC:?}" "${TARGET:?}"; then - echo "LogSuccess: $SRC renamed to $TARGET" >> "${XPLR_PIPE_MSG_IN:?}" echo Explore >> "${XPLR_PIPE_MSG_IN:?}" + echo "LogSuccess: $SRC renamed to $TARGET" >> "${XPLR_PIPE_MSG_IN:?}" + echo "FocusPath: $TARGET" >> "${XPLR_PIPE_MSG_IN:?}" else echo "LogError: failed to rename $SRC to $TARGET" >> "${XPLR_PIPE_MSG_IN:?}" fi @@ -941,6 +944,7 @@ impl Default for Config { fi else if rm -v "${line:?}"; then + echo "FocusNext" >> "${XPLR_PIPE_MSG_IN:?}" echo "LogSuccess: $line deleted" >> "${XPLR_PIPE_MSG_IN:?}" else echo "LogError: failed to delete $line" >> "${XPLR_PIPE_MSG_IN:?}" @@ -957,6 +961,7 @@ impl Default for Config { - BashExec: | (while IFS= read -r line; do if rm -rfv "${line:?}"; then + echo "FocusNext" >> "${XPLR_PIPE_MSG_IN:?}" echo "LogSuccess: $line deleted" >> "${XPLR_PIPE_MSG_IN:?}" else echo "LogError: failed to delete $line" >> "${XPLR_PIPE_MSG_IN:?}" diff --git a/src/main.rs b/src/main.rs index 670d2f2..a61a874 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,8 +6,10 @@ use crossterm::terminal as term; use handlebars::Handlebars; use std::env; use std::fs; +use std::io; use std::io::prelude::*; use std::path::PathBuf; +use std::process::{Command, ExitStatus, Stdio}; use std::sync::mpsc; use termion::get_tty; use tui::backend::CrosstermBackend; @@ -20,6 +22,55 @@ use xplr::pipe_reader; use xplr::pwd_watcher; use xplr::ui; +fn call(app: &app::App, cmd: app::Command, silent: bool) -> io::Result { + let input_buffer = app.input_buffer().unwrap_or_default(); + + let focus_index = app + .directory_buffer() + .map(|d| d.focus) + .unwrap_or_default() + .to_string(); + + let pipe_msg_in = app.pipe().msg_in.clone(); + let pipe_mode_out = app.pipe().mode_out.clone(); + let pipe_focus_out = app.pipe().focus_out.clone(); + let pipe_selection_out = app.pipe().selection_out.clone(); + let pipe_result_out = app.pipe().result_out.clone(); + let pipe_directory_nodes_out = app.pipe().directory_nodes_out.clone(); + let pipe_global_help_menu_out = app.pipe().global_help_menu_out.clone(); + let pipe_logs_out = app.pipe().logs_out.clone(); + let session_path = app.session_path(); + + let (stdin, stdout, stderr) = if silent { + (Stdio::null(), Stdio::null(), Stdio::null()) + } else { + (Stdio::inherit(), Stdio::inherit(), Stdio::inherit()) + }; + + Command::new(cmd.command.clone()) + .current_dir(app.pwd()) + .env("XPLR_APP_VERSION", app.version()) + .env("XPLR_CONFIG_VERSION", &app.config().version) + .env("XPLR_PID", &app.pid().to_string()) + .env("XPLR_INPUT_BUFFER", input_buffer) + .env("XPLR_FOCUS_PATH", app.focused_node_str()) + .env("XPLR_FOCUS_INDEX", focus_index) + .env("XPLR_SESSION_PATH", session_path) + .env("XPLR_PIPE_MSG_IN", pipe_msg_in) + .env("XPLR_PIPE_SELECTION_OUT", pipe_selection_out) + .env("XPLR_PIPE_FOCUS_OUT", pipe_focus_out) + .env("XPLR_PIPE_MODE_OUT", pipe_mode_out) + .env("XPLR_PIPE_RESULT_OUT", pipe_result_out) + .env("XPLR_PIPE_GLOBAL_HELP_MENU_OUT", pipe_global_help_menu_out) + .env("XPLR_PIPE_DIRECTORY_NODES_OUT", pipe_directory_nodes_out) + .env("XPLR_PIPE_LOGS_OUT", pipe_logs_out) + .stdin(stdin) + .stdout(stdout) + .stderr(stderr) + .args(cmd.args) + .status() +} + fn main() -> Result<()> { let (tx_msg_in, rx_msg_in) = mpsc::channel(); let (tx_event_reader, rx_event_reader) = mpsc::channel(); @@ -114,6 +165,10 @@ fn main() -> Result<()> { while let Some(msg) = app.pop_msg_out() { match msg { + app::MsgOut::Enque(task) => { + tx_msg_in.send(task)?; + } + app::MsgOut::PrintResultAndQuit => { output = Some(app.result_str()); break 'outer; @@ -158,50 +213,36 @@ fn main() -> Result<()> { terminal.draw(|f| ui::draw(f, &app, &hb))?; } + app::MsgOut::CallSilently(cmd) => { + tx_event_reader.send(true)?; + + let status = call(&app, cmd, false) + .map(|s| { + if s.success() { + Ok(()) + } else { + Err(format!("process exited with code {}", &s)) + } + }) + .unwrap_or_else(|e| Err(e.to_string())); + + if let Err(e) = status { + let msg = app::MsgIn::External(app::ExternalMsg::LogError(e)); + tx_msg_in.send(app::Task::new(1, msg, None))?; + }; + + tx_event_reader.send(false)?; + } + app::MsgOut::Call(cmd) => { tx_event_reader.send(true)?; + terminal.clear()?; term::disable_raw_mode()?; terminal.set_cursor(0, 0)?; terminal.show_cursor()?; - let input_buffer = app.input_buffer().unwrap_or_default(); - - let focus_index = app - .directory_buffer() - .map(|d| d.focus) - .unwrap_or_default() - .to_string(); - - let pipe_msg_in = app.pipe().msg_in.clone(); - let pipe_mode_out = app.pipe().mode_out.clone(); - let pipe_focus_out = app.pipe().focus_out.clone(); - let pipe_selection_out = app.pipe().selection_out.clone(); - let pipe_result_out = app.pipe().result_out.clone(); - let pipe_directory_nodes_out = app.pipe().directory_nodes_out.clone(); - let pipe_global_help_menu_out = app.pipe().global_help_menu_out.clone(); - let pipe_logs_out = app.pipe().logs_out.clone(); - let session_path = app.session_path(); - - let status = std::process::Command::new(cmd.command.clone()) - .current_dir(app.pwd()) - .env("XPLR_APP_VERSION", app.version()) - .env("XPLR_CONFIG_VERSION", &app.config().version) - .env("XPLR_PID", &app.pid().to_string()) - .env("XPLR_INPUT_BUFFER", input_buffer) - .env("XPLR_FOCUS_PATH", app.focused_node_str()) - .env("XPLR_FOCUS_INDEX", focus_index) - .env("XPLR_SESSION_PATH", session_path) - .env("XPLR_PIPE_MSG_IN", pipe_msg_in) - .env("XPLR_PIPE_SELECTION_OUT", pipe_selection_out) - .env("XPLR_PIPE_FOCUS_OUT", pipe_focus_out) - .env("XPLR_PIPE_MODE_OUT", pipe_mode_out) - .env("XPLR_PIPE_RESULT_OUT", pipe_result_out) - .env("XPLR_PIPE_GLOBAL_HELP_MENU_OUT", pipe_global_help_menu_out) - .env("XPLR_PIPE_DIRECTORY_NODES_OUT", pipe_directory_nodes_out) - .env("XPLR_PIPE_LOGS_OUT", pipe_logs_out) - .args(cmd.args.clone()) - .status() + let status = call(&app, cmd, false) .map(|s| { if s.success() { Ok(()) @@ -224,10 +265,6 @@ fn main() -> Result<()> { }; } - while let Some(task) = app.pop_task_out() { - tx_msg_in.send(task)?; - } - if app.focused_node() != last_app.focused_node() { fs::write(&app.pipe().focus_out, app.focused_node_str())?; }; @@ -254,6 +291,7 @@ fn main() -> Result<()> { } terminal.clear()?; + terminal.set_cursor(0, 0)?; term::disable_raw_mode()?; execute!(terminal.backend_mut(), term::LeaveAlternateScreen)?; terminal.show_cursor()?;