aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorOgulcan Celik <ogulcancelik@gmail.com>2026-05-11 18:25:44 +0300
committerOgulcan Celik <ogulcancelik@gmail.com>2026-05-11 18:25:44 +0300
commite12c7a2cef6c50f736658c20d0c5a33da033ae4b (patch)
tree71407c1fba6f6956ecd650e0deaf0726589692ed
parent733b109e20e82dffe8320efc2bed70c4e560da3b (diff)
fix: hide pane cursor while scrolled back
-rw-r--r--src/server/headless.rs34
-rw-r--r--src/server/render_stream.rs4
-rw-r--r--src/ui.rs1
-rw-r--r--src/ui/panes.rs8
4 files changed, 44 insertions, 3 deletions
diff --git a/src/server/headless.rs b/src/server/headless.rs
index 337bbec7..7bd7abce 100644
--- a/src/server/headless.rs
+++ b/src/server/headless.rs
@@ -1962,6 +1962,40 @@ mod tests {
);
}
+ #[tokio::test]
+ async fn virtual_render_hides_focused_pane_cursor_while_scrolled_back() {
+ let mut state = AppState::test_new();
+ let mut ws = crate::workspace::Workspace::test_new("test");
+ let pane_id = ws.tabs[0].root_pane;
+ let mut bytes = Vec::new();
+ for line in 0..80 {
+ bytes.extend_from_slice(format!("line {line:02}\r\n").as_bytes());
+ }
+ let runtime = crate::pane::PaneRuntime::test_with_scrollback_bytes(20, 5, 4096, &bytes);
+ ws.tabs[0].runtimes.insert(pane_id, runtime);
+
+ state.workspaces = vec![ws];
+ state.active = Some(0);
+ state.selected = 0;
+ state.mode = crate::app::Mode::Terminal;
+
+ let area = Rect::new(0, 0, 80, 24);
+ let _ = crate::server::render_stream::render_virtual(&mut state, area, true);
+ let runtime = state.workspaces[0]
+ .runtime(pane_id)
+ .expect("pane runtime after initial render");
+ runtime.scroll_up(6);
+ assert!(crate::ui::pane_is_scrolled_back(runtime));
+
+ let (_buffer, cursor) =
+ crate::server::render_stream::render_virtual(&mut state, area, true);
+
+ assert!(
+ cursor.as_ref().is_none_or(|cursor| !cursor.visible),
+ "cursor: {cursor:?}"
+ );
+ }
+
#[test]
fn latest_active_client_drives_shared_size_theme_and_fallback() {
let mut server = test_headless_server();
diff --git a/src/server/render_stream.rs b/src/server/render_stream.rs
index f1749657..45542a7a 100644
--- a/src/server/render_stream.rs
+++ b/src/server/render_stream.rs
@@ -257,11 +257,11 @@ fn focused_terminal_cursor(app_state: &AppState) -> Option<CursorState> {
.pane_infos
.iter()
.find(|info| info.is_focused)?;
- let rt = ws.runtimes.get(&info.id)?;
+ let rt = ws.runtime(info.id)?;
let cursor = rt.cursor_state(info.inner_rect, true)?;
Some(CursorState {
x: cursor.x,
y: cursor.y,
- visible: cursor.visible,
+ visible: cursor.visible && !crate::ui::pane_is_scrolled_back(rt),
})
}
diff --git a/src/ui.rs b/src/ui.rs
index 83a9d987..aac94a63 100644
--- a/src/ui.rs
+++ b/src/ui.rs
@@ -63,6 +63,7 @@ pub(crate) use self::{
mobile_switcher_areas, mobile_switcher_max_scroll, mobile_switcher_target_at,
mobile_switcher_workspace_doc_range, MobileSwitcherTarget,
},
+ panes::pane_is_scrolled_back,
tabs::compute_tab_bar_view,
widgets::{centered_popup_rect, modal_stack_areas},
};
diff --git a/src/ui/panes.rs b/src/ui/panes.rs
index 20ac9165..818ee9fd 100644
--- a/src/ui/panes.rs
+++ b/src/ui/panes.rs
@@ -13,6 +13,11 @@ use crate::app::{AppState, Mode};
use crate::layout::PaneInfo;
use crate::pane::PaneRuntime;
+pub(crate) fn pane_is_scrolled_back(rt: &PaneRuntime) -> bool {
+ rt.scroll_metrics()
+ .is_some_and(|metrics| metrics.offset_from_bottom > 0)
+}
+
fn stable_terminal_inner_rect(pane_inner: Rect) -> Rect {
if pane_inner.width <= 4 {
return pane_inner;
@@ -176,7 +181,8 @@ pub(super) fn render_panes(app: &AppState, frame: &mut Frame, area: Rect) {
frame.render_widget(block, info.rect);
}
- rt.render(frame, info.inner_rect, info.is_focused && terminal_active);
+ let show_cursor = info.is_focused && terminal_active && !pane_is_scrolled_back(rt);
+ rt.render(frame, info.inner_rect, show_cursor);
render_pane_scrollbar(app, frame, info, rt);
let should_dim = !info.is_focused && multi_pane && !terminal_active;