Skip to content

Commit

Permalink
Removed screen flicker when entering TUI command screen
Browse files Browse the repository at this point in the history
  • Loading branch information
fritzrehde committed Nov 5, 2023
1 parent 368badb commit a536ede
Show file tree
Hide file tree
Showing 2 changed files with 60 additions and 39 deletions.
19 changes: 9 additions & 10 deletions src/ui/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ use crossterm::event::EventStream;
use futures::{future::FutureExt, StreamExt};
use std::sync::Arc;
use std::time::{Duration, Instant};
use terminal_manager::TerminalManager;
use terminal_manager::Tui;
use tokio::sync::mpsc::{self, Receiver, Sender};

pub use state::State;
Expand All @@ -22,7 +22,7 @@ pub type WatchedCommand = CommandBuilder<Blocking, WithEnv, WithOutput, Interrup

pub struct UI {
blocking_state: BlockingState,
terminal_manager: TerminalManager,
tui: Tui,
state: State,
watch_rate: Duration,
keybindings: Arc<Keybindings>,
Expand Down Expand Up @@ -117,10 +117,7 @@ enum BlockingState {
/// require borrowing self completely, which causes borrow-checker problems.
macro_rules! draw {
($self:expr) => {
$self
.terminal_manager
.terminal
.draw(|frame| $self.state.draw(frame))
$self.tui.terminal.draw(|frame| $self.state.draw(frame))
};
}

Expand Down Expand Up @@ -152,7 +149,7 @@ impl UI {
}

async fn new(config: Config) -> Result<(Self, PollingState)> {
let terminal_manager = TerminalManager::new()?;
let terminal_manager = Tui::new()?;

let env_variables = EnvVariables::generate_initial(config.initial_env_variables).await?;
let keybindings_str = config.keybindings_parsed.to_string();
Expand Down Expand Up @@ -189,7 +186,7 @@ impl UI {

let ui = Self {
blocking_state: BlockingState::default(),
terminal_manager,
tui: terminal_manager,
state,
watch_rate: config.watch_rate,
keybindings: Arc::new(keybindings),
Expand Down Expand Up @@ -265,8 +262,9 @@ impl UI {
Event::TUISubcommandCompleted(potential_error) => {
potential_error?;

self.terminal_manager.show_tui()?;
self.tui.restore()?;
log::info!("Watchbind's TUI is shown.");

// Resume listening to terminal events in our TUI.
self.channels
.polling_tx
Expand All @@ -288,6 +286,7 @@ impl UI {
},
BlockingState::BlockedReloadingWatchedCommand => match event {
Event::CommandOutput(lines) => {
// TODO: is called from async context, should be put in spawn_blocking
self.state.update_lines(lines?)?;

if let ControlFlow::Exit = self.conclude_blocking().await? {
Expand Down Expand Up @@ -388,7 +387,7 @@ impl UI {
RequestedAction::ExecutingTUISubcommand(tui_hidden_tx) => {
self.pause_terminal_events_polling().await?;

self.terminal_manager.hide_tui()?;
self.tui.hide()?;
tui_hidden_tx.send(()).await?;
log::info!("Watchbind's TUI has been hidden.");

Expand Down
80 changes: 51 additions & 29 deletions src/ui/terminal_manager.rs
Original file line number Diff line number Diff line change
@@ -1,61 +1,83 @@
use anyhow::Result;
use crossterm::{
event::{DisableMouseCapture, EnableMouseCapture},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
use crossterm::terminal::{
disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen,
};
use ratatui::backend::CrosstermBackend;
use std::io::{stdout, Stdout};

pub type Terminal = ratatui::Terminal<CrosstermBackend<Stdout>>;
type Terminal = ratatui::Terminal<CrosstermBackend<Stdout>>;

pub struct TerminalManager {
/// A Terminal User Interface (TUI).
pub struct Tui {
// TODO: don't make public (means moving the UI::draw! macro here)
pub terminal: Terminal,
}

impl TerminalManager {
impl Tui {
/// Initialise and show a new TUI.
pub fn new() -> Result<Self> {
let terminal = Self::create_tui()?;
let mut terminal_manager = TerminalManager { terminal };
terminal_manager.show_tui()?;
let terminal = Self::create_new_terminal()?;
let mut terminal_manager = Tui { terminal };

terminal_manager.show()?;

Ok(terminal_manager)
}

// TODO: maybe we don't have to create the stdout, backend variables again, only once at creation. Optimize that later
fn create_tui() -> Result<Terminal> {
/// Create a new terminal that will write the TUI to stdout.
fn create_new_terminal() -> Result<Terminal> {
Ok(Terminal::new(CrosstermBackend::new(stdout()))?)
}

/// Show the TUI.
fn show(&mut self) -> Result<()> {
enable_raw_mode()?;
let mut stdout = stdout();
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
terminal.hide_cursor()?;
crossterm::execute!(self.terminal.backend_mut(), EnterAlternateScreen)?;
self.terminal.hide_cursor()?;

Ok(terminal)
Ok(())
}

pub fn show_tui(&mut self) -> Result<()> {
self.terminal = Self::create_tui()?;
/// Show a TUI which has been painted over by another TUI program.
pub fn restore(&mut self) -> Result<()> {
// Our own TUI was painted over by another program previously, so we
// must create a new one.
self.terminal = Self::create_new_terminal()?;

self.show()?;

Ok(())
}

/// Hide the TUI, but don't unpaint the TUI we have already drawn. This is
/// useful if another program wants to paint a TUI onto the screen, and we
/// don't want to temporarily return to the user's terminal for a
/// split-second.
pub fn hide(&mut self) -> Result<()> {
disable_raw_mode()?;
// The trick to not unpainting our TUI is to not leave the alternate
// screen like we would normally do when hiding the TUI.
self.terminal.show_cursor()?;

Ok(())
}

pub fn hide_tui(&mut self) -> Result<()> {
/// Exit the TUI, which entails completely unpainting the TUI, and returning
/// to the user's terminal.
fn exit(&mut self) -> Result<()> {
disable_raw_mode()?;
execute!(
self.terminal.backend_mut(),
LeaveAlternateScreen,
DisableMouseCapture
)?;
crossterm::execute!(self.terminal.backend_mut(), LeaveAlternateScreen)?;
self.terminal.show_cursor()?;

Ok(())
}
}

impl Drop for TerminalManager {
impl Drop for Tui {
fn drop(&mut self) {
// TODO: remove unwrap
self.hide_tui().unwrap();
if let Err(e) = self.exit() {
// TODO: one shouldn't panic in a drop impl, since a second panic would cause instant termination
panic!("Tearing down the TUI failed with: {}", e);
}
}
}

0 comments on commit a536ede

Please sign in to comment.