fix: eliminate alt-screen flash when returning from repo/project detail
The hub hands its live terminal to detail/project_detail on open (already flash-free going in), but both sub-views unconditionally tore the terminal down on exit; the hub's next loop iteration then always called tui::setup() again, producing a real LeaveAlternateScreen -> (plain terminal visible for one frame) -> EnterAlternateScreen blink on every trip back. - detail::run / project_detail::run now return the live terminal on a clean exit instead of tearing it down (only torn down if the inner loop itself errors); hub::tui_phase accepts an already-live terminal and skips its own setup() when given one, threading it across loop iterations - project_detail's own nested repo-detail call (Enter on its Repos tab) had the same issue one level down, fixed the same way via Option<Tty> so the terminal can be taken out for the nested call and put back - verified via raw pty byte counts: two full hub<->detail round trips now show zero EnterAlternateScreen/LeaveAlternateScreen pairs in between, vs. one pair per trip before
This commit is contained in:
1 parent
14ef7fb06c
commit
a3658d5e97
4 files changed
+64
-22
No files matched your search
+10
-2
@@ -148,8 +148,16 @@ pub fn maybe_load_activity(state: &mut HubState) {
|
||||
/// event, and routes it to whichever input mode is currently active. Modal/
|
||||
/// confirm/filter input short-circuit here (they're loop control flow, not
|
||||
/// part of the global keymap); everything else goes to `keys_dispatch`.
|
||||
pub fn tui_phase(state: &mut HubState) -> Result<(PhaseResult, Option<crate::tui::Tty>)> {
|
||||
let mut terminal = crate::tui::setup()?;
|
||||
///
|
||||
/// `existing_term` lets the caller hand back a terminal a sub-view (repo/
|
||||
/// project detail) just returned instead of tearing down and setting up a
|
||||
/// fresh one — the previous "leave alt screen, blink the real terminal,
|
||||
/// re-enter" was visible for a frame every time detail handed control back.
|
||||
pub fn tui_phase(state: &mut HubState, existing_term: Option<crate::tui::Tty>) -> Result<(PhaseResult, Option<crate::tui::Tty>)> {
|
||||
let mut terminal = match existing_term {
|
||||
Some(t) => t,
|
||||
None => crate::tui::setup()?,
|
||||
};
|
||||
let result = (|| -> Result<PhaseResult> {
|
||||
loop {
|
||||
state.poll_incoming();
|
||||
|
||||
+10
-3
@@ -174,8 +174,13 @@ pub async fn run_overview_tui(
|
||||
log
|
||||
};
|
||||
|
||||
// Carried across loop iterations so a terminal handed back by a sub-view
|
||||
// (repo/project detail) can be reused by the next `tui_phase` call
|
||||
// instead of tearing down and setting a fresh one up — see `tui_phase`'s
|
||||
// doc comment for the flash this avoids.
|
||||
let mut carried_term: Option<crate::tui::Tty> = None;
|
||||
loop {
|
||||
let (phase_result, handed_term) = tui_phase(&mut state)?;
|
||||
let (phase_result, handed_term) = tui_phase(&mut state, carried_term.take())?;
|
||||
|
||||
if state.quit {
|
||||
session::save(state.tab, state.cur_repo_idx().map(|ri| state.repos[ri].path.clone()));
|
||||
@@ -186,12 +191,14 @@ pub async fn run_overview_tui(
|
||||
PhaseResult::OpenDetail => {
|
||||
state.open_detail = false;
|
||||
if let Some(ri) = state.cur_repo_idx() {
|
||||
let _ = crate::status::detail::run(state.repos[ri].clone(), cfg, handed_term).await;
|
||||
if let Ok(term) = crate::status::detail::run(state.repos[ri].clone(), cfg, handed_term).await {
|
||||
carried_term = Some(term);
|
||||
}
|
||||
} else if let Some(pi) = state.cur_proj_idx() {
|
||||
let repo_indices = state.project_repos(pi);
|
||||
let proj_repos: Vec<RepoSummary> =
|
||||
repo_indices.iter().filter_map(|&ri| state.repos.get(ri)).cloned().collect();
|
||||
project_detail::run(state.projects[pi].clone(), proj_repos, cfg, handed_term).await?;
|
||||
carried_term = Some(project_detail::run(state.projects[pi].clone(), proj_repos, cfg, handed_term).await?);
|
||||
}
|
||||
// Detail view can rename/delete/archive repos or change branches —
|
||||
// rescan so the hub's list reflects whatever just happened, instead
|
||||
|
||||
+34
-12
@@ -109,28 +109,41 @@ impl St {
|
||||
|
||||
/// Runs the project detail sub-view until the user backs out. Reuses the
|
||||
/// caller's terminal handle when given one (avoids an alternate-screen
|
||||
/// flash when entering from the hub), otherwise sets up its own.
|
||||
pub async fn run(project: Project, repos: Vec<RepoSummary>, cfg: &AppConfig, maybe_term: Option<crate::tui::Tty>) -> Result<()> {
|
||||
let mut terminal = match maybe_term {
|
||||
/// flash when entering from the hub), otherwise sets up its own. On a
|
||||
/// normal exit, hands the still-live terminal back to the caller instead of
|
||||
/// tearing it down, so the hub can reuse it for its own next frame without
|
||||
/// the alternate screen toggling off in between; only torn down here if
|
||||
/// `run_loop` itself errors.
|
||||
pub async fn run(project: Project, repos: Vec<RepoSummary>, cfg: &AppConfig, maybe_term: Option<crate::tui::Tty>) -> Result<crate::tui::Tty> {
|
||||
// `Option` (rather than a bare `Tty`) so the Enter-on-Repos branch below
|
||||
// can `take()` it out for the nested `crate::detail::run` call (which
|
||||
// needs to own the terminal to hand it further down) and put the
|
||||
// (possibly-recreated) terminal back afterwards, without ever tearing
|
||||
// down and setting up a fresh one in between.
|
||||
let mut terminal = Some(match maybe_term {
|
||||
Some(t) => t,
|
||||
None => crate::tui::setup()?,
|
||||
};
|
||||
});
|
||||
let mut st = St::new(project, repos, cfg.clone());
|
||||
|
||||
// Spawn commits aggregation immediately
|
||||
spawn_commits(&mut st);
|
||||
|
||||
let result = run_loop(&mut st, &mut terminal).await;
|
||||
crate::tui::teardown(terminal)?;
|
||||
result
|
||||
match run_loop(&mut st, &mut terminal).await {
|
||||
Ok(()) => Ok(terminal.expect("terminal always present after a clean run_loop exit")),
|
||||
Err(e) => {
|
||||
if let Some(t) = terminal.take() { crate::tui::teardown(t)?; }
|
||||
Err(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn run_loop(st: &mut St, terminal: &mut crate::tui::Tty) -> Result<()> {
|
||||
async fn run_loop(st: &mut St, terminal: &mut Option<crate::tui::Tty>) -> Result<()> {
|
||||
loop {
|
||||
poll_commits(st);
|
||||
poll_activity(st);
|
||||
|
||||
terminal.draw(|f| super::project_detail_render::draw(f, st))?;
|
||||
terminal.as_mut().expect("terminal present during run_loop").draw(|f| super::project_detail_render::draw(f, st))?;
|
||||
|
||||
let event = crate::tui::next_event(250)?;
|
||||
let key = match event { Some(Event::Key(k)) => k, _ => continue };
|
||||
@@ -182,13 +195,13 @@ async fn run_loop(st: &mut St, terminal: &mut crate::tui::Tty) -> Result<()> {
|
||||
continue;
|
||||
}
|
||||
|
||||
handle_key(key.code, st).await;
|
||||
handle_key(key.code, st, terminal).await;
|
||||
if st.quit { break; }
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn handle_key(code: KeyCode, st: &mut St) {
|
||||
async fn handle_key(code: KeyCode, st: &mut St, terminal: &mut Option<crate::tui::Tty>) {
|
||||
st.status_msg = None;
|
||||
match code {
|
||||
KeyCode::Char('q') | KeyCode::Esc => { st.quit = true; }
|
||||
@@ -233,7 +246,16 @@ async fn handle_key(code: KeyCode, st: &mut St) {
|
||||
KeyCode::Enter => match st.tab {
|
||||
ProjectTab::Repos => {
|
||||
if let Some(r) = st.repos.get(st.repo_cursor).cloned() {
|
||||
let _ = crate::detail::run(r, &st.cfg, None).await;
|
||||
// Hand our live terminal down (no flash), then take back
|
||||
// whatever it returns instead of tearing it down here.
|
||||
let handed = terminal.take();
|
||||
match crate::detail::run(r, &st.cfg, handed).await {
|
||||
Ok(t) => *terminal = Some(t),
|
||||
// detail::run already tore its (our) terminal down on
|
||||
// error — nothing left to draw to, so back out clean
|
||||
// instead of panicking on the next frame's `.expect`.
|
||||
Err(_) => st.quit = true,
|
||||
}
|
||||
}
|
||||
}
|
||||
ProjectTab::Todos => {
|
||||
|
||||
Reference in new issue
Block a user