Skip to content

Commit

Permalink
add exit_behavior config option
Browse files Browse the repository at this point in the history
`exit_behavior = "Hold"` will keep the pane alive until explicitly
closed.  More details in the docs that are part of this commit.

refs: #499
  • Loading branch information
wez committed Feb 27, 2021
1 parent 38e6a1b commit 697a6ab
Show file tree
Hide file tree
Showing 7 changed files with 154 additions and 27 deletions.
20 changes: 20 additions & 0 deletions config/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -519,6 +519,23 @@ where
deserializer.deserialize_any(Number)
}

/// Behavior when the program spawned by wezterm terminates
#[derive(Debug, Deserialize, Clone, Copy, PartialEq, Eq)]
pub enum ExitBehavior {
/// Close the associated pane
Close,
/// Close the associated pane if the process was successful
CloseOnCleanExit,
/// Hold the pane until it is explicitly closed
Hold,
}

impl Default for ExitBehavior {
fn default() -> Self {
ExitBehavior::Close
}
}

#[derive(Debug, Deserialize, Clone)]
pub struct Config {
/// The font size, measured in points
Expand Down Expand Up @@ -596,6 +613,9 @@ pub struct Config {
/// info!)
pub default_cwd: Option<PathBuf>,

#[serde(default)]
pub exit_behavior: ExitBehavior,

/// Specifies a map of environment variables that should be set
/// when spawning commands in the local domain.
/// This is not used when working with remote domains.
Expand Down
1 change: 1 addition & 0 deletions docs/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ brief notes about them may accumulate here.
* OSC 52 (Clipboard manipulation) now respects the difference between PRIMARY and CLIPBOARD on X11 systems.
* Windows: the portable .zip file download now includes ANGLE EGL, just like the setup.exe installer has done since version 20201031-154415-9614e117
* Windows: Fixed [ToggleFullScreen](config/lua/keyassignment/ToggleFullScreen.md) so that it once again toggles between full screen and normal placement. [#177](https://github.com/wez/wezterm/issues/177)
* New: [exit_behavior](config/lua/config/exit_behavior.md) config option to keep panes open after the program has completed. [#499](https://github.com/wez/wezterm/issues/499)

### 20210203-095643-70a364eb

Expand Down
10 changes: 10 additions & 0 deletions docs/config/lua/config/exit_behavior.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
## `exit_behavior = "Close"`

*Since: nightly builds only*

Controls the behavior when the shell program spawned by the terminal exits.
There are three possible values:

* `"Close"` - close the corresponding pane as soon as the program exits. This is the default setting.
* `"Hold"` - keep the pane open after the program exits. The pane must be manually closed via [CloseCurrentPane](../keyassignment/CloseCurrentPane.md), [CloseCurrentTab](../keyassignment/CloseCurrentTab.md) or closeing the window.
* `"CloseOnCleanExit"` - if the shell program exited with a successful status, behave like `"Close"`, otherwise, behave like `"Hold"`.
21 changes: 16 additions & 5 deletions mux/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use crate::pane::{Pane, PaneId};
use crate::tab::{Tab, TabId};
use crate::window::{Window, WindowId};
use anyhow::{anyhow, Error};
use config::{configuration, ExitBehavior};
use domain::{Domain, DomainId};
use log::error;
use portable_pty::ExitStatus;
Expand Down Expand Up @@ -115,11 +116,21 @@ fn accumulator(pane_id: PaneId, dead: &Arc<AtomicBool>, rx: Receiver<Vec<u8>>) {
break;
}
}
promise::spawn::spawn_into_main_thread(async move {
let mux = Mux::get().unwrap();
mux.remove_pane(pane_id);
})
.detach();
match configuration().exit_behavior {
ExitBehavior::Hold | ExitBehavior::CloseOnCleanExit => {
// We don't know if we can unilaterally close
// this pane right now, so don't!
send_to_mux(pane_id, &dead, b"\n[Process completed]".to_vec());
return;
}
ExitBehavior::Close => {
promise::spawn::spawn_into_main_thread(async move {
let mux = Mux::get().unwrap();
mux.remove_pane(pane_id);
})
.detach();
}
}
}

/// This function is run in a separate thread; its purpose is to perform
Expand Down
82 changes: 71 additions & 11 deletions mux/src/localpane.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use crate::{Domain, Mux, MuxNotification};
use anyhow::Error;
use async_trait::async_trait;
use config::keyassignment::ScrollbackEraseMode;
use config::{configuration, ExitBehavior};
use portable_pty::{Child, MasterPty, PtySize};
use rangeset::RangeSet;
use std::cell::{RefCell, RefMut};
Expand All @@ -20,10 +21,23 @@ use wezterm_term::{
SemanticZone, StableRowIndex, Terminal,
};

#[derive(Debug)]
enum ProcessState {
Running {
child: Box<dyn Child>,
// Whether we've explicitly killed the child
killed: bool,
},
DeadPendingClose {
killed: bool,
},
Dead,
}

pub struct LocalPane {
pane_id: PaneId,
terminal: RefCell<Terminal>,
process: RefCell<Box<dyn Child>>,
process: RefCell<ProcessState>,
pty: RefCell<Box<dyn MasterPty>>,
domain_id: DomainId,
tmux_domain: RefCell<Option<Arc<TmuxDomainState>>>,
Expand Down Expand Up @@ -72,16 +86,57 @@ impl Pane for LocalPane {
}

fn kill(&self) {
log::debug!("killing process in pane {}", self.pane_id);
self.process.borrow_mut().kill().ok();
let mut proc = self.process.borrow_mut();
log::debug!(
"killing process in pane {}, state is {:?}",
self.pane_id,
proc
);
match &mut *proc {
ProcessState::Running { child, killed } => {
let _ = child.kill();
*killed = true;
}
ProcessState::DeadPendingClose { killed } => {
*killed = true;
}
_ => {}
}
}

fn is_dead(&self) -> bool {
if let Ok(None) = self.process.borrow_mut().try_wait() {
false
} else {
log::trace!("Pane id {} is_dead", self.pane_id);
true
let mut proc = self.process.borrow_mut();

match &mut *proc {
ProcessState::Running { child, killed } => {
if let Ok(Some(status)) = child.try_wait() {
match (configuration().exit_behavior, status.success(), killed) {
(ExitBehavior::Close, _, _) => *proc = ProcessState::Dead,
(ExitBehavior::CloseOnCleanExit, false, false) => {
*proc = ProcessState::DeadPendingClose { killed: false }
}
(ExitBehavior::CloseOnCleanExit, ..) => *proc = ProcessState::Dead,
(ExitBehavior::Hold, _, false) => {
*proc = ProcessState::DeadPendingClose { killed: false }
}
(ExitBehavior::Hold, _, true) => *proc = ProcessState::Dead,
}
log::debug!("child terminated, new state is {:?}", proc);
}
}
ProcessState::DeadPendingClose { killed } => {
if *killed {
*proc = ProcessState::Dead;
log::debug!("child state -> {:?}", proc);
}
}
ProcessState::Dead => {}
}

match &*proc {
ProcessState::Running { .. } => false,
ProcessState::DeadPendingClose { .. } => false,
ProcessState::Dead => true,
}
}

Expand Down Expand Up @@ -398,7 +453,10 @@ impl LocalPane {
Self {
pane_id,
terminal: RefCell::new(terminal),
process: RefCell::new(process),
process: RefCell::new(ProcessState::Running {
child: process,
killed: false,
}),
pty: RefCell::new(pty),
domain_id,
tmux_domain: RefCell::new(None),
Expand Down Expand Up @@ -506,7 +564,9 @@ impl LocalPane {
impl Drop for LocalPane {
fn drop(&mut self) {
// Avoid lingering zombies
self.process.borrow_mut().kill().ok();
self.process.borrow_mut().wait().ok();
if let ProcessState::Running { child, .. } = &mut *self.process.borrow_mut() {
let _ = child.kill();
let _ = child.wait();
}
}
}
15 changes: 14 additions & 1 deletion wezterm-gui/src/gui/overlay/confirm_close_pane.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use crate::gui::TermWindow;
use mux::pane::PaneId;
use mux::tab::TabId;
use mux::termwiztermtab::TermWizTerminal;
Expand Down Expand Up @@ -164,6 +165,7 @@ pub fn confirm_close_pane(
pane_id: PaneId,
mut term: TermWizTerminal,
mux_window_id: WindowId,
window: ::window::Window,
) -> anyhow::Result<()> {
if run_confirmation_app("🛑 Really kill this pane?", &mut term)? {
promise::spawn::spawn_into_main_thread(async move {
Expand All @@ -176,6 +178,7 @@ pub fn confirm_close_pane(
})
.detach();
}
TermWindow::schedule_cancel_overlay_for_pane(window, pane_id);

Ok(())
}
Expand All @@ -184,6 +187,7 @@ pub fn confirm_close_tab(
tab_id: TabId,
mut term: TermWizTerminal,
_mux_window_id: WindowId,
window: ::window::Window,
) -> anyhow::Result<()> {
if run_confirmation_app(
"🛑 Really kill this tab and all contained panes?",
Expand All @@ -195,13 +199,16 @@ pub fn confirm_close_tab(
})
.detach();
}
TermWindow::schedule_cancel_overlay(window, tab_id);

Ok(())
}

pub fn confirm_close_window(
mut term: TermWizTerminal,
mux_window_id: WindowId,
window: ::window::Window,
tab_id: TabId,
) -> anyhow::Result<()> {
if run_confirmation_app(
"🛑 Really kill this window and all contained tabs and panes?",
Expand All @@ -213,11 +220,16 @@ pub fn confirm_close_window(
})
.detach();
}
TermWindow::schedule_cancel_overlay(window, tab_id);

Ok(())
}

pub fn confirm_quit_program(mut term: TermWizTerminal) -> anyhow::Result<()> {
pub fn confirm_quit_program(
mut term: TermWizTerminal,
window: ::window::Window,
tab_id: TabId,
) -> anyhow::Result<()> {
if run_confirmation_app("🛑 Really Quit WezTerm?", &mut term)? {
promise::spawn::spawn_into_main_thread(async move {
use ::window::{Connection, ConnectionOps};
Expand All @@ -226,6 +238,7 @@ pub fn confirm_quit_program(mut term: TermWizTerminal) -> anyhow::Result<()> {
})
.detach();
}
TermWindow::schedule_cancel_overlay(window, tab_id);

Ok(())
}
32 changes: 22 additions & 10 deletions wezterm-gui/src/gui/termwindow.rs
Original file line number Diff line number Diff line change
Expand Up @@ -302,8 +302,9 @@ impl WindowCallbacks for TermWindow {
};

let mux_window_id = self.mux_window_id;
let (overlay, future) = start_overlay(self, &tab, move |_tab_id, term| {
confirm_close_window(term, mux_window_id)
let window = self.window.clone().unwrap();
let (overlay, future) = start_overlay(self, &tab, move |tab_id, term| {
confirm_close_window(term, mux_window_id, window, tab_id)
});
self.assign_overlay(tab.tab_id(), overlay);
promise::spawn::spawn(future).detach();
Expand Down Expand Up @@ -2045,8 +2046,9 @@ impl TermWindow {
None => anyhow::bail!("no active tab!?"),
};

let (overlay, future) = start_overlay(self, &tab, move |_tab_id, term| {
confirm_quit_program(term)
let window = self.window.clone().unwrap();
let (overlay, future) = start_overlay(self, &tab, move |tab_id, term| {
confirm_quit_program(term, window, tab_id)
});
self.assign_overlay(tab.tab_id(), overlay);
promise::spawn::spawn(future).detach();
Expand Down Expand Up @@ -2454,8 +2456,9 @@ impl TermWindow {

let pane_id = pane.pane_id();
if confirm {
let window = self.window.clone().unwrap();
let (overlay, future) = start_overlay_pane(self, &pane, move |pane_id, term| {
confirm_close_pane(pane_id, term, mux_window_id)
confirm_close_pane(pane_id, term, mux_window_id, window)
});
self.assign_overlay_for_pane(pane_id, overlay);
promise::spawn::spawn(future).detach();
Expand All @@ -2473,8 +2476,9 @@ impl TermWindow {
let tab_id = tab.tab_id();
let mux_window_id = self.mux_window_id;
if confirm {
let window = self.window.clone().unwrap();
let (overlay, future) = start_overlay(self, &tab, move |tab_id, term| {
confirm_close_tab(tab_id, term, mux_window_id)
confirm_close_tab(tab_id, term, mux_window_id, window)
});
self.assign_overlay(tab_id, overlay);
promise::spawn::spawn(future).detach();
Expand Down Expand Up @@ -4053,7 +4057,9 @@ impl TermWindow {

/// Removes any overlay for the specified tab
fn cancel_overlay_for_tab(&self, tab_id: TabId) {
self.tab_state(tab_id).overlay.take();
if let Some(pane) = self.tab_state(tab_id).overlay.take() {
Mux::get().unwrap().remove_pane(pane.pane_id());
}
if let Some(window) = self.window.as_ref() {
window.invalidate();
}
Expand All @@ -4069,7 +4075,9 @@ impl TermWindow {
}

fn cancel_overlay_for_pane(&self, pane_id: PaneId) {
self.pane_state(pane_id).overlay.take();
if let Some(pane) = self.pane_state(pane_id).overlay.take() {
Mux::get().unwrap().remove_pane(pane.pane_id());
}
if let Some(window) = self.window.as_ref() {
window.invalidate();
}
Expand All @@ -4085,12 +4093,16 @@ impl TermWindow {
}

pub fn assign_overlay_for_pane(&mut self, pane_id: PaneId, overlay: Rc<dyn Pane>) {
self.pane_state(pane_id).overlay.replace(overlay);
if let Some(prior) = self.pane_state(pane_id).overlay.replace(overlay) {
Mux::get().unwrap().remove_pane(prior.pane_id());
}
self.update_title();
}

pub fn assign_overlay(&mut self, tab_id: TabId, overlay: Rc<dyn Pane>) {
self.tab_state(tab_id).overlay.replace(overlay);
if let Some(prior) = self.tab_state(tab_id).overlay.replace(overlay) {
Mux::get().unwrap().remove_pane(prior.pane_id());
}
self.update_title();
}
}
Expand Down

0 comments on commit 697a6ab

Please sign in to comment.