aboutsummaryrefslogtreecommitdiff
path: root/src/server
diff options
context:
space:
mode:
authorOgulcan Celik <ogulcancelik@gmail.com>2026-07-15 15:48:38 +0300
committerOgulcan Celik <ogulcancelik@gmail.com>2026-07-16 22:47:10 +0300
commit8dcb75a5c08b4dd1f225ea531a27824a9f41ae4a (patch)
tree48b4b74e857c93ffc03ff1b790b2f9e76e0113ac /src/server
parentf43a6d4a5591a0286e01de737be7b3f42416898b (diff)
refactor: extract active tab surface rendering
Diffstat (limited to 'src/server')
-rw-r--r--src/server/headless.rs64
-rw-r--r--src/server/headless/pane_graphics.rs1
-rw-r--r--src/server/render_stream.rs84
3 files changed, 59 insertions, 90 deletions
diff --git a/src/server/headless.rs b/src/server/headless.rs
index 9c21aa1b..c5e8ca62 100644
--- a/src/server/headless.rs
+++ b/src/server/headless.rs
@@ -3384,6 +3384,7 @@ impl HeadlessServer {
&& crate::kitty_graphics::has_visible_pane_graphics(
&self.app.state,
&self.app.terminal_runtimes,
+ self.app.state.view.tab_surface(),
*cell_size,
)
{
@@ -3670,6 +3671,7 @@ impl HeadlessServer {
.extend(crate::kitty_graphics::encode_local_pane_graphics(
&self.app.state,
&self.app.terminal_runtimes,
+ self.app.state.view.tab_surface(),
cell_size,
&mut next_graphics_cache,
));
@@ -7145,16 +7147,28 @@ next_tab = ""
);
}
- #[test]
- fn render_and_stream_uses_each_client_terminal_size() {
+ #[tokio::test]
+ async fn render_and_stream_uses_each_client_terminal_size() {
let mut server = test_headless_server();
- server.app.state.workspaces = vec![crate::workspace::Workspace::test_new("test")];
+ let mut workspace = crate::workspace::Workspace::test_new("test");
+ let active_pane = workspace.tabs[0].root_pane;
+ let background_tab = workspace.test_add_tab(Some("background"));
+ let background_pane = workspace.tabs[background_tab].root_pane;
+ workspace.tabs[0].runtimes.insert(
+ active_pane,
+ crate::terminal::TerminalRuntime::test_with_screen_bytes(80, 24, b"active"),
+ );
+ workspace.tabs[background_tab].runtimes.insert(
+ background_pane,
+ crate::terminal::TerminalRuntime::test_with_screen_bytes(80, 24, b"background"),
+ );
+ server.app.state.workspaces = vec![workspace];
server.app.state.active = Some(0);
server.app.state.selected = 0;
server.app.state.mode = crate::app::Mode::Terminal;
let (desktop_tx, _desktop_control_rx, desktop_rx) = test_client_writer();
- let (phone_tx, _phone_control_rx, phone_rx) = test_client_writer();
+ let (mobile_tx, _mobile_control_rx, mobile_rx) = test_client_writer();
server.clients.insert(
1,
@@ -7171,13 +7185,13 @@ next_tab = ""
server.clients.insert(
2,
ClientConnection::new(
- (80, 24),
+ (44, 20),
crate::kitty_graphics::HostCellSize::default(),
crate::terminal_theme::TerminalTheme::default(),
None,
2,
RenderEncoding::SemanticFrame,
- Some(phone_tx),
+ Some(mobile_tx),
),
);
server.foreground_client_id = Some(1);
@@ -7187,10 +7201,44 @@ next_tab = ""
server.render_and_stream();
let desktop_frame = read_server_frame(desktop_rx.recv().expect("desktop frame"));
- let phone_frame = read_server_frame(phone_rx.recv().expect("phone frame"));
+ let mobile_frame = read_server_frame(mobile_rx.recv().expect("mobile frame"));
assert_eq!((desktop_frame.width, desktop_frame.height), (120, 40));
- assert_eq!((phone_frame.width, phone_frame.height), (80, 24));
+ assert_eq!((mobile_frame.width, mobile_frame.height), (44, 20));
+ let mobile_text = frame_text(&mobile_frame);
+ let mut mobile_rows = mobile_text.lines();
+ let mobile_header = mobile_rows.by_ref().take(2).collect::<String>();
+ let mobile_surface = mobile_rows.collect::<String>();
+ assert!(mobile_header.contains("test"), "header: {mobile_header:?}");
+ assert!(
+ mobile_surface.contains("active"),
+ "surface: {mobile_surface:?}"
+ );
+ assert!(!mobile_surface.contains("background"));
+
+ let foreground_terminal_area = Rect::new(26, 1, 94, 39);
+ let expected_pane_size = (
+ foreground_terminal_area.height,
+ foreground_terminal_area.width.saturating_sub(1),
+ );
+ assert_eq!(
+ server.app.state.view.layout,
+ crate::app::state::ViewLayout::Desktop
+ );
+ assert_eq!(server.app.state.view.mobile_header_rect, Rect::default());
+ assert_eq!(
+ server.app.state.view.terminal_area,
+ foreground_terminal_area
+ );
+ assert_eq!(
+ server.app.state.workspaces[0].tabs[0].runtimes[&active_pane].current_size(),
+ expected_pane_size
+ );
+ assert_eq!(
+ server.app.state.workspaces[0].tabs[background_tab].runtimes[&background_pane]
+ .current_size(),
+ expected_pane_size
+ );
}
#[tokio::test]
diff --git a/src/server/headless/pane_graphics.rs b/src/server/headless/pane_graphics.rs
index 18174f45..ed28eab0 100644
--- a/src/server/headless/pane_graphics.rs
+++ b/src/server/headless/pane_graphics.rs
@@ -121,6 +121,7 @@ impl HeadlessServer {
frame_pane_graphics_for_client(crate::kitty_graphics::encode_local_pane_graphics(
&self.app.state,
&self.app.terminal_runtimes,
+ self.app.state.view.tab_surface(),
cell_size,
&mut next_graphics_cache,
));
diff --git a/src/server/render_stream.rs b/src/server/render_stream.rs
index facebd2c..8858bb6b 100644
--- a/src/server/render_stream.rs
+++ b/src/server/render_stream.rs
@@ -384,94 +384,14 @@ pub(crate) fn visible_hyperlinks(
app_state: &AppState,
terminal_runtimes: &TerminalRuntimeRegistry,
) -> Vec<((u16, u16), String, String)> {
- let Some(ws_idx) = app_state.active else {
- return Vec::new();
- };
- if app_state.workspaces.get(ws_idx).is_none() {
- return Vec::new();
- }
-
- let mut links = Vec::new();
- for info in &app_state.view.pane_infos {
- if let Some(runtime) =
- app_state.runtime_for_pane_in_workspace(terminal_runtimes, ws_idx, info.id)
- {
- links.extend(runtime.visible_hyperlinks(info.inner_rect));
- }
- }
- links
+ crate::ui::tab_surface_hyperlinks(app_state, terminal_runtimes, app_state.view.tab_surface())
}
pub(crate) fn focused_terminal_cursor(
app_state: &AppState,
terminal_runtimes: &TerminalRuntimeRegistry,
) -> Option<CursorState> {
- if app_state.mode != Mode::Terminal {
- return None;
- }
-
- let ws_idx = app_state.active?;
- let info = app_state
- .view
- .pane_infos
- .iter()
- .find(|info| info.is_focused)?;
- if !app_state.pane_exposes_host_cursor(ws_idx, info.id) {
- return None;
- }
- let rt = app_state.runtime_for_pane_in_workspace(terminal_runtimes, ws_idx, info.id)?;
- if rt.synchronized_output_active() {
- return None;
- }
- let scrolled_back = crate::ui::pane_is_scrolled_back(rt);
- // Determine whether the IME-anchor reveal applies to this focused pane.
- // The master switch must be on, and either no agent filter is configured
- // (apply to any pane) or the focused pane's detected agent matches the
- // allow-list. A configured list with no valid entries reveals nothing.
- let reveal = app_state.reveal_hidden_cursor_for_cjk_ime
- && (!app_state.cjk_ime_agent_filter_configured || {
- let detected = app_state
- .workspaces
- .get(ws_idx)
- .and_then(|ws| ws.terminal_id(info.id))
- .and_then(|tid| app_state.terminals.get(tid))
- .and_then(|t| t.detected_agent);
- detected.is_some_and(|agent| app_state.cjk_ime_agents.contains(&agent))
- });
-
- if let Some(cursor) = rt.cursor_state(info.inner_rect, true) {
- // When the reveal applies, expose the cursor anchor regardless of the
- // pane's `?25l` request so macOS IMEs keep tracking the candidate
- // window when TUIs paint their own cursor. Scrollback suppression
- // still applies.
- let visible = if reveal {
- !scrolled_back
- } else {
- cursor.visible && !scrolled_back
- };
- Some(CursorState {
- x: cursor.x,
- y: cursor.y,
- visible,
- shape: if reveal && visible {
- app_state.cjk_ime_cursor_shape
- } else {
- cursor.shape
- },
- })
- } else if reveal && !scrolled_back {
- // cursor_state() returned None — the viewport has no cursor position
- // (can happen with complex TUIs). Fall back to the pane's top-left so
- // the outer terminal still exposes a cursor anchor for IME tracking.
- Some(CursorState {
- x: info.inner_rect.x,
- y: info.inner_rect.y,
- visible: true,
- shape: app_state.cjk_ime_cursor_shape,
- })
- } else {
- None
- }
+ crate::ui::tab_surface_cursor(app_state, terminal_runtimes, app_state.view.tab_surface())
}
fn focused_terminal_owns_host_cursor(