fix: retain alternate screen across executable restart
This commit is contained in:
+107
-84
@@ -59,7 +59,8 @@ struct HookState {
|
||||
active: bool,
|
||||
}
|
||||
|
||||
struct TerminalSession {
|
||||
/// Owns terminal modes and panic cleanup through application startup and shutdown.
|
||||
pub struct TerminalSession {
|
||||
hook: Arc<Mutex<HookState>>,
|
||||
}
|
||||
|
||||
@@ -91,10 +92,10 @@ fn restore_terminal() {
|
||||
}
|
||||
|
||||
impl TerminalSession {
|
||||
fn run(body: impl FnOnce() -> io::Result<()>) -> io::Result<()> {
|
||||
pub fn run(inherited_screen: bool, body: impl FnOnce() -> io::Result<()>) -> io::Result<()> {
|
||||
let hook = Arc::new(Mutex::new(HookState {
|
||||
previous: Some(panic::take_hook()),
|
||||
active: false,
|
||||
active: inherited_screen,
|
||||
}));
|
||||
let during_session = Arc::clone(&hook);
|
||||
panic::set_hook(Box::new(move |info| {
|
||||
@@ -119,9 +120,11 @@ impl TerminalSession {
|
||||
.lock()
|
||||
.unwrap_or_else(|error| error.into_inner())
|
||||
.active = true;
|
||||
if !inherited_screen {
|
||||
execute!(io::stdout(), EnterAlternateScreen)?;
|
||||
}
|
||||
execute!(
|
||||
io::stdout(),
|
||||
EnterAlternateScreen,
|
||||
EnableBracketedPaste,
|
||||
EnableMouseCapture,
|
||||
EnableFocusChange,
|
||||
@@ -135,6 +138,20 @@ impl TerminalSession {
|
||||
Err(payload) => panic::resume_unwind(payload),
|
||||
}
|
||||
}
|
||||
|
||||
/// Restores process-local raw/input state without exposing the normal screen.
|
||||
/// The guard remains armed if launching the replacement process fails.
|
||||
pub fn prepare_restart() -> io::Result<()> {
|
||||
disable_raw_mode()?;
|
||||
execute!(
|
||||
io::stdout(),
|
||||
EndSynchronizedUpdate,
|
||||
DisableBracketedPaste,
|
||||
DisableMouseCapture,
|
||||
DisableFocusChange,
|
||||
ColorSchemeNotifications(false)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for TerminalSession {
|
||||
@@ -737,95 +754,101 @@ fn wheel_kind(mouse: &MouseEvent) -> Option<MouseEventKind> {
|
||||
fn draw_frame<W: io::Write>(
|
||||
terminal: &mut Terminal<CrosstermBackend<Vec<u8>>>,
|
||||
output: &mut W,
|
||||
clear_screen: bool,
|
||||
render: impl FnOnce(&mut Frame),
|
||||
) -> io::Result<()> {
|
||||
// Do not suspend the terminal's presentation while computing the next frame.
|
||||
terminal.backend_mut().writer_mut().clear();
|
||||
crossterm::queue!(terminal.backend_mut(), BeginSynchronizedUpdate)?;
|
||||
// Terminals without synchronized updates still hide the painting cursor.
|
||||
let result = terminal
|
||||
.hide_cursor()
|
||||
.and_then(|()| terminal.draw(render).map(|_| ()));
|
||||
let result = terminal.hide_cursor().and_then(|()| {
|
||||
if clear_screen {
|
||||
// Fresh Ratatui buffers are already empty; avoid its blocking cursor query.
|
||||
crossterm::queue!(
|
||||
terminal.backend_mut(),
|
||||
crossterm::terminal::Clear(crossterm::terminal::ClearType::All)
|
||||
)?;
|
||||
}
|
||||
terminal.draw(render).map(|_| ())
|
||||
});
|
||||
let finish = execute!(terminal.backend_mut(), EndSynchronizedUpdate);
|
||||
result.and(finish)?;
|
||||
output.write_all(terminal.backend().writer())?;
|
||||
output.flush()
|
||||
}
|
||||
|
||||
/// Renders session state and translates terminal input; the session owns plugin
|
||||
/// execution and retirement, so the adapter needs no lifecycle callback.
|
||||
pub fn run(session: &mut EditorSession) -> io::Result<()> {
|
||||
TerminalSession::run(|| {
|
||||
let mut output: Stdout = io::stdout();
|
||||
let mut terminal = Terminal::new(CrosstermBackend::new(Vec::new()))?;
|
||||
let mut viewports = BufferViewports::default();
|
||||
let mut colors = TerminalColors::default();
|
||||
let mut query_theme = true;
|
||||
let mut redraw = true;
|
||||
let mut pending = None;
|
||||
while !session.editor().should_quit() {
|
||||
redraw |= session.poll();
|
||||
colors.expire(Instant::now());
|
||||
if viewports.theme != colors.colors {
|
||||
viewports.set_theme(colors.colors);
|
||||
redraw = true;
|
||||
}
|
||||
if redraw {
|
||||
draw_frame(&mut terminal, &mut output, |frame| {
|
||||
viewports.draw_session(frame, session)
|
||||
})?;
|
||||
redraw = false;
|
||||
}
|
||||
// Start the reply deadline after painting, not before expensive syntax initialization.
|
||||
if query_theme || viewports.diff_visible {
|
||||
let now = Instant::now();
|
||||
if query_theme || colors.refresh_due(now) {
|
||||
colors.query(&mut output, now)?;
|
||||
query_theme = false;
|
||||
}
|
||||
}
|
||||
let input = if let Some(input) = pending.take() {
|
||||
input
|
||||
} else {
|
||||
let Some(input) = next_terminal_event(&mut colors, Duration::from_millis(25))?
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
input
|
||||
};
|
||||
match input {
|
||||
Event::Key(key) if key.kind != KeyEventKind::Release => {
|
||||
if session.editor().mode() == Mode::Insert && typing_key(&key) {
|
||||
viewports.panels.follow_files();
|
||||
pending = typing_ready(session, key, || {
|
||||
next_terminal_event(&mut colors, Duration::ZERO)
|
||||
})?;
|
||||
redraw = true;
|
||||
} else if let Some(input) = translate(key) {
|
||||
viewports.panels.follow_files();
|
||||
session.handle(input);
|
||||
redraw = true;
|
||||
}
|
||||
}
|
||||
Event::Paste(text) => {
|
||||
viewports.panels.follow_files();
|
||||
session.handle(Input::Paste(text));
|
||||
redraw = true;
|
||||
}
|
||||
Event::Mouse(mouse) => {
|
||||
let (changed, following) = viewports.scroll_ready(session, mouse, || {
|
||||
next_terminal_event(&mut colors, Duration::ZERO)
|
||||
})?;
|
||||
redraw |= changed;
|
||||
pending = following;
|
||||
}
|
||||
Event::Resize(_, _) => redraw = true,
|
||||
Event::FocusGained | Event::ColorSchemeChanged => query_theme = true,
|
||||
_ => {}
|
||||
/// Renders inside an active `TerminalSession`; inherited contents are cleared
|
||||
/// inside the first synchronized frame rather than exposing a blank screen.
|
||||
pub fn run(session: &mut EditorSession, mut clear_screen: bool) -> io::Result<()> {
|
||||
let mut output: Stdout = io::stdout();
|
||||
let mut terminal = Terminal::new(CrosstermBackend::new(Vec::new()))?;
|
||||
let mut viewports = BufferViewports::default();
|
||||
let mut colors = TerminalColors::default();
|
||||
let mut query_theme = true;
|
||||
let mut redraw = true;
|
||||
let mut pending = None;
|
||||
while !session.editor().should_quit() {
|
||||
redraw |= session.poll();
|
||||
colors.expire(Instant::now());
|
||||
if viewports.theme != colors.colors {
|
||||
viewports.set_theme(colors.colors);
|
||||
redraw = true;
|
||||
}
|
||||
if redraw {
|
||||
draw_frame(&mut terminal, &mut output, clear_screen, |frame| {
|
||||
viewports.draw_session(frame, session)
|
||||
})?;
|
||||
redraw = false;
|
||||
clear_screen = false;
|
||||
}
|
||||
// Start the reply deadline after painting, not before expensive syntax initialization.
|
||||
if query_theme || viewports.diff_visible {
|
||||
let now = Instant::now();
|
||||
if query_theme || colors.refresh_due(now) {
|
||||
colors.query(&mut output, now)?;
|
||||
query_theme = false;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
let input = if let Some(input) = pending.take() {
|
||||
input
|
||||
} else {
|
||||
let Some(input) = next_terminal_event(&mut colors, Duration::from_millis(25))? else {
|
||||
continue;
|
||||
};
|
||||
input
|
||||
};
|
||||
match input {
|
||||
Event::Key(key) if key.kind != KeyEventKind::Release => {
|
||||
if session.editor().mode() == Mode::Insert && typing_key(&key) {
|
||||
viewports.panels.follow_files();
|
||||
pending = typing_ready(session, key, || {
|
||||
next_terminal_event(&mut colors, Duration::ZERO)
|
||||
})?;
|
||||
redraw = true;
|
||||
} else if let Some(input) = translate(key) {
|
||||
viewports.panels.follow_files();
|
||||
session.handle(input);
|
||||
redraw = true;
|
||||
}
|
||||
}
|
||||
Event::Paste(text) => {
|
||||
viewports.panels.follow_files();
|
||||
session.handle(Input::Paste(text));
|
||||
redraw = true;
|
||||
}
|
||||
Event::Mouse(mouse) => {
|
||||
let (changed, following) = viewports.scroll_ready(session, mouse, || {
|
||||
next_terminal_event(&mut colors, Duration::ZERO)
|
||||
})?;
|
||||
redraw |= changed;
|
||||
pending = following;
|
||||
}
|
||||
Event::Resize(_, _) => redraw = true,
|
||||
Event::FocusGained | Event::ColorSchemeChanged => query_theme = true,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn next_terminal_event(
|
||||
@@ -13927,7 +13950,7 @@ end
|
||||
)
|
||||
.unwrap();
|
||||
for text in ["first frame", "second frame", "third frame"] {
|
||||
draw_frame(&mut terminal, &mut output, |frame| {
|
||||
draw_frame(&mut terminal, &mut output, false, |frame| {
|
||||
assert!(
|
||||
!synchronized.get(),
|
||||
"terminal presentation suspended during frame computation"
|
||||
@@ -13958,7 +13981,7 @@ end
|
||||
let mut viewports = BufferViewports::default();
|
||||
for key in std::iter::repeat_n('j', 20).chain(std::iter::repeat_n('k', 20)) {
|
||||
editor.handle(Input::Char(key));
|
||||
draw_frame(&mut terminal, &mut output, |frame| {
|
||||
draw_frame(&mut terminal, &mut output, false, |frame| {
|
||||
viewports.draw(frame, &mut editor)
|
||||
})
|
||||
.unwrap();
|
||||
@@ -14023,10 +14046,10 @@ end
|
||||
}));
|
||||
if mode == "entry" {
|
||||
let mut editor = EditorSession::open(None, vec![], None).unwrap();
|
||||
assert!(run(&mut editor).is_err());
|
||||
assert!(TerminalSession::run(false, || run(&mut editor, false)).is_err());
|
||||
} else {
|
||||
let result = panic::catch_unwind(AssertUnwindSafe(|| {
|
||||
TerminalSession::run(|| {
|
||||
TerminalSession::run(false, || {
|
||||
assert!(crossterm::terminal::is_raw_mode_enabled().unwrap());
|
||||
match mode.as_str() {
|
||||
"normal" => Ok(()),
|
||||
@@ -14038,7 +14061,7 @@ end
|
||||
viewport: ratatui::Viewport::Fixed(Rect::new(0, 0, 20, 5)),
|
||||
},
|
||||
)?;
|
||||
draw_frame(&mut terminal, &mut io::stdout(), |_| {
|
||||
draw_frame(&mut terminal, &mut io::stdout(), false, |_| {
|
||||
panic::panic_any(73_u64)
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user