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
-5
@@ -38,12 +38,16 @@ use settings_io::{compute_activity, poll_api_rx, poll_prs_issues_rx, poll_settin
|
|||||||
|
|
||||||
/// Opens the detail view for `summary`. Reuses an already-live terminal
|
/// Opens the detail view for `summary`. Reuses an already-live terminal
|
||||||
/// handed over by the hub when given one (avoids a flash of the alternate
|
/// handed over by the hub when given one (avoids a flash of the alternate
|
||||||
/// screen toggling off and back on), otherwise sets up a fresh one.
|
/// screen toggling off and back on), otherwise sets up a fresh one. On a
|
||||||
|
/// normal exit (back to the hub), hands the still-live terminal back to the
|
||||||
|
/// caller instead of tearing it down — the hub reuses it for its own next
|
||||||
|
/// frame, so the alternate screen never toggles off in between. Only torn
|
||||||
|
/// down here if `run_detail_loop` itself errors.
|
||||||
pub async fn run(
|
pub async fn run(
|
||||||
summary: RepoSummary,
|
summary: RepoSummary,
|
||||||
cfg: &AppConfig,
|
cfg: &AppConfig,
|
||||||
maybe_term: Option<crate::tui::Tty>,
|
maybe_term: Option<crate::tui::Tty>,
|
||||||
) -> Result<()> {
|
) -> Result<crate::tui::Tty> {
|
||||||
let path = summary.path.clone();
|
let path = summary.path.clone();
|
||||||
let name = summary.name.clone();
|
let name = summary.name.clone();
|
||||||
|
|
||||||
@@ -147,9 +151,10 @@ pub async fn run(
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Main loop — terminal stays alive the whole time.
|
// Main loop — terminal stays alive the whole time.
|
||||||
let result = run_detail_loop(&mut st, &mut terminal).await;
|
match run_detail_loop(&mut st, &mut terminal).await {
|
||||||
crate::tui::teardown(terminal)?;
|
Ok(()) => Ok(terminal),
|
||||||
result
|
Err(e) => { crate::tui::teardown(terminal)?; Err(e) }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn run_detail_loop(st: &mut St, terminal: &mut crate::tui::Tty) -> Result<()> {
|
async fn run_detail_loop(st: &mut St, terminal: &mut crate::tui::Tty) -> Result<()> {
|
||||||
|
|||||||
+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/
|
/// event, and routes it to whichever input mode is currently active. Modal/
|
||||||
/// confirm/filter input short-circuit here (they're loop control flow, not
|
/// confirm/filter input short-circuit here (they're loop control flow, not
|
||||||
/// part of the global keymap); everything else goes to `keys_dispatch`.
|
/// 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> {
|
let result = (|| -> Result<PhaseResult> {
|
||||||
loop {
|
loop {
|
||||||
state.poll_incoming();
|
state.poll_incoming();
|
||||||
|
|||||||
+10
-3
@@ -174,8 +174,13 @@ pub async fn run_overview_tui(
|
|||||||
log
|
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 {
|
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 {
|
if state.quit {
|
||||||
session::save(state.tab, state.cur_repo_idx().map(|ri| state.repos[ri].path.clone()));
|
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 => {
|
PhaseResult::OpenDetail => {
|
||||||
state.open_detail = false;
|
state.open_detail = false;
|
||||||
if let Some(ri) = state.cur_repo_idx() {
|
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() {
|
} else if let Some(pi) = state.cur_proj_idx() {
|
||||||
let repo_indices = state.project_repos(pi);
|
let repo_indices = state.project_repos(pi);
|
||||||
let proj_repos: Vec<RepoSummary> =
|
let proj_repos: Vec<RepoSummary> =
|
||||||
repo_indices.iter().filter_map(|&ri| state.repos.get(ri)).cloned().collect();
|
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 —
|
// Detail view can rename/delete/archive repos or change branches —
|
||||||
// rescan so the hub's list reflects whatever just happened, instead
|
// 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
|
/// Runs the project detail sub-view until the user backs out. Reuses the
|
||||||
/// caller's terminal handle when given one (avoids an alternate-screen
|
/// caller's terminal handle when given one (avoids an alternate-screen
|
||||||
/// flash when entering from the hub), otherwise sets up its own.
|
/// flash when entering from the hub), otherwise sets up its own. On a
|
||||||
pub async fn run(project: Project, repos: Vec<RepoSummary>, cfg: &AppConfig, maybe_term: Option<crate::tui::Tty>) -> Result<()> {
|
/// normal exit, hands the still-live terminal back to the caller instead of
|
||||||
let mut terminal = match maybe_term {
|
/// 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,
|
Some(t) => t,
|
||||||
None => crate::tui::setup()?,
|
None => crate::tui::setup()?,
|
||||||
};
|
});
|
||||||
let mut st = St::new(project, repos, cfg.clone());
|
let mut st = St::new(project, repos, cfg.clone());
|
||||||
|
|
||||||
// Spawn commits aggregation immediately
|
// Spawn commits aggregation immediately
|
||||||
spawn_commits(&mut st);
|
spawn_commits(&mut st);
|
||||||
|
|
||||||
let result = run_loop(&mut st, &mut terminal).await;
|
match run_loop(&mut st, &mut terminal).await {
|
||||||
crate::tui::teardown(terminal)?;
|
Ok(()) => Ok(terminal.expect("terminal always present after a clean run_loop exit")),
|
||||||
result
|
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 {
|
loop {
|
||||||
poll_commits(st);
|
poll_commits(st);
|
||||||
poll_activity(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 event = crate::tui::next_event(250)?;
|
||||||
let key = match event { Some(Event::Key(k)) => k, _ => continue };
|
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;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
handle_key(key.code, st).await;
|
handle_key(key.code, st, terminal).await;
|
||||||
if st.quit { break; }
|
if st.quit { break; }
|
||||||
}
|
}
|
||||||
Ok(())
|
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;
|
st.status_msg = None;
|
||||||
match code {
|
match code {
|
||||||
KeyCode::Char('q') | KeyCode::Esc => { st.quit = true; }
|
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 {
|
KeyCode::Enter => match st.tab {
|
||||||
ProjectTab::Repos => {
|
ProjectTab::Repos => {
|
||||||
if let Some(r) = st.repos.get(st.repo_cursor).cloned() {
|
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 => {
|
ProjectTab::Todos => {
|
||||||
|
|||||||
Reference in new issue
Block a user