diff --git a/src/ui/app_state/helpers.rs b/src/ui/app_state/helpers.rs index b8e832b..6d18e03 100644 --- a/src/ui/app_state/helpers.rs +++ b/src/ui/app_state/helpers.rs @@ -186,6 +186,23 @@ pub fn calc_grid( (cols.max(10), rows.max(5)) } +pub fn calc_grid_split( + total_width: f32, + height: f32, + font_size: f32, + status_bar_visible: bool, + banner_visible: bool, +) -> (usize, usize) { + let char_width = font_size * 0.62; + let char_height = font_size * 1.29; + let banner_extra = if banner_visible { font_size * 2.5 } else { 0.0 }; + let padding_y = if status_bar_visible { 118.0 } else { 96.0 } + banner_extra; + let pane_content_width = ((total_width - 41.0) / 2.0).max(0.0); + let cols = (pane_content_width / char_width).floor() as usize; + let rows = ((height - padding_y) / char_height).max(0.0).floor() as usize; + (cols.max(10), rows.max(5)) +} + pub fn pixel_to_cell(pos: Point, font_size: f32) -> Option<(usize, usize)> { const X_ORIGIN: f32 = 20.0; const Y_ORIGIN: f32 = 88.0; diff --git a/src/ui/app_state/message.rs b/src/ui/app_state/message.rs index 44c8577..4a193b6 100644 --- a/src/ui/app_state/message.rs +++ b/src/ui/app_state/message.rs @@ -84,5 +84,8 @@ pub enum Message { DiagnosticBannerResponse(Result), DiagnosticBannerCommand(String), SettingsDiagnosticBannerToggled(bool), + SplitPane, + CloseSplitPane, + CloseLeftPane, NoOp, } diff --git a/src/ui/app_state/subscription.rs b/src/ui/app_state/subscription.rs index 6d3c40d..7fe7529 100644 --- a/src/ui/app_state/subscription.rs +++ b/src/ui/app_state/subscription.rs @@ -112,6 +112,19 @@ fn handle_key_pressed( }; } + #[cfg(target_os = "macos")] + let split_mod = modifiers.logo() && modifiers.shift(); + #[cfg(not(target_os = "macos"))] + let split_mod = modifiers.control() && modifiers.shift(); + + if split_mod && let Key::Character(c) = &key { + match c.as_str() { + "t" | "T" => return Some(Message::SplitPane), + "w" | "W" => return Some(Message::CloseSplitPane), + _ => {} + } + } + let kb = config::keybindings(); if matches_kb(&kb.prev_tab, &key, modifiers) { return Some(Message::PrevTab); @@ -268,25 +281,45 @@ impl Nova { })); for tab in &self.tabs { - if !tab.pty_alive { - continue; + if tab.pty_alive { + let key = PtyKey { + tab_id: tab.id, + shell_cmd: tab.shell_cmd.clone(), + initial_cols: tab.grid.cols as u16, + initial_rows: tab.grid.rows as u16, + initial_cwd: tab.initial_cwd.clone(), + }; + subs.push(Subscription::run_with(key, |k| { + pty_worker( + k.tab_id, + k.initial_cols, + k.initial_rows, + k.shell_cmd.clone(), + k.initial_cwd.clone(), + ) + })); + } + + if let Some(split) = &tab.split + && split.pty_alive + { + let split_key = PtyKey { + tab_id: split.id, + shell_cmd: split.shell_cmd.clone(), + initial_cols: split.grid.cols as u16, + initial_rows: split.grid.rows as u16, + initial_cwd: split.initial_cwd.clone(), + }; + subs.push(Subscription::run_with(split_key, |k| { + pty_worker( + k.tab_id, + k.initial_cols, + k.initial_rows, + k.shell_cmd.clone(), + k.initial_cwd.clone(), + ) + })); } - let key = PtyKey { - tab_id: tab.id, - shell_cmd: tab.shell_cmd.clone(), - initial_cols: tab.grid.cols as u16, - initial_rows: tab.grid.rows as u16, - initial_cwd: tab.initial_cwd.clone(), - }; - subs.push(Subscription::run_with(key, |k| { - pty_worker( - k.tab_id, - k.initial_cols, - k.initial_rows, - k.shell_cmd.clone(), - k.initial_cwd.clone(), - ) - })); } Subscription::batch(subs) diff --git a/src/ui/app_state/update/input.rs b/src/ui/app_state/update/input.rs index f47ab66..1c238e6 100644 --- a/src/ui/app_state/update/input.rs +++ b/src/ui/app_state/update/input.rs @@ -1,3 +1,4 @@ +#[cfg(target_os = "windows")] use std::io::Write; use std::sync::atomic::Ordering; @@ -16,6 +17,39 @@ impl Nova { return iced::Task::none(); } + let Some(active_tab) = self.tabs.get(self.active_index) else { + return iced::Task::none(); + }; + let active_pane_is_split = active_tab.active_pane_is_split; + + if active_pane_is_split { + self.selection_start = None; + self.selection_end = None; + self.click_count = 0; + self.diagnostic_banner = None; + self.ai_pending_diagnostic = None; + if let Some(active_tab) = self.tabs.get_mut(self.active_index) + && let Some(split) = &mut active_tab.split + { + if bytes == b"\t" + && let Some(suggestion) = split.grid.suggestion.take() + && !suggestion.is_empty() + { + if let Some(tx) = &split.pty_tx { + let _ = tx.send_blocking(PtyCommand::Input(suggestion.into_bytes())); + } + split.grid.input_start_col = None; + split.grid.input_start_row = None; + return iced::Task::none(); + } + split.scroll_offset = 0; + if let Some(tx) = &split.pty_tx { + let _ = tx.send_blocking(PtyCommand::Input(bytes)); + } + } + return iced::Task::none(); + } + if bytes == b"\t" && let Some(active_tab) = self.tabs.get_mut(self.active_index) && let Some(suggestion) = active_tab.grid.suggestion.take() @@ -85,12 +119,56 @@ impl Nova { } pub(super) fn handle_pty_output(&mut self, tab_id: usize, bytes: Vec) -> iced::Task { + if let Some(tab_idx) = self + .tabs + .iter() + .position(|t| t.split.as_ref().map(|s| s.id) == Some(tab_id)) + { + let tab = &mut self.tabs[tab_idx]; + let split = tab.split.as_mut().unwrap(); + #[cfg(target_os = "windows")] + if std::env::var("NOVA_DEBUG_PTY").is_ok() + && let Ok(mut f) = std::fs::OpenOptions::new() + .create(true) + .append(true) + .open(std::env::temp_dir().join("nova_pty_debug.bin")) + { + let _ = f.write_all(&bytes); + } + let mut executor = AnsiExecutor { + grid: &mut split.grid, + bell_pending: false, + }; + for byte in bytes { + split.ansi_parser.advance(&mut executor, &[byte]); + } + while !split.grid.output_queue.is_empty() { + let response = split.grid.output_queue.remove(0); + if let Some(tx) = &split.pty_tx { + let _ = tx.send_blocking(PtyCommand::Input(response)); + } + } + split.grid.control_queue.clear(); + let new_pwd = split.grid.pwd.clone(); + if new_pwd != split.pwd { + split.pwd = new_pwd; + } + split.scroll_offset = 0; + if let Some(partial) = split.grid.extract_current_input() { + split.grid.suggestion = split.grid.find_best_suggestion(&partial); + } else { + split.grid.suggestion = None; + } + return iced::Task::none(); + } + if let Some(tab) = self.tabs.iter_mut().find(|t| t.id == tab_id) { + #[cfg(target_os = "windows")] if std::env::var("NOVA_DEBUG_PTY").is_ok() && let Ok(mut f) = std::fs::OpenOptions::new() .create(true) .append(true) - .open("C:\\Users\\Public\\nova_pty_debug.bin") + .open(std::env::temp_dir().join("nova_pty_debug.bin")) { let _ = f.write_all(&bytes); } diff --git a/src/ui/app_state/update/mod.rs b/src/ui/app_state/update/mod.rs index 11ccdf8..a30810c 100644 --- a/src/ui/app_state/update/mod.rs +++ b/src/ui/app_state/update/mod.rs @@ -171,13 +171,34 @@ impl Nova { self.active_index = self.tabs.len() - 1; iced::Task::none() } + Message::SplitPane => self.handle_split_pane(), + Message::CloseSplitPane => { + self.handle_close_split_pane(); + iced::Task::none() + } + Message::CloseLeftPane => { + self.handle_close_left_pane(); + iced::Task::none() + } Message::SwitchTab(index) => { if index < self.tabs.len() { self.active_index = index; } iced::Task::none() } - Message::CloseActiveTab => self.update(Message::CloseTab(self.active_index)), + Message::CloseActiveTab => { + if let Some(tab) = self.tabs.get(self.active_index) + && tab.split.is_some() + { + let msg = if tab.active_pane_is_split { + Message::CloseSplitPane + } else { + Message::CloseLeftPane + }; + return self.update(msg); + } + self.update(Message::CloseTab(self.active_index)) + } Message::NextTab => { if !self.tabs.is_empty() { self.active_index = (self.active_index + 1) % self.tabs.len(); @@ -201,6 +222,16 @@ impl Nova { } tab.pty_tx = Some(tx); tab.pty_alive = true; + return iced::Task::none(); + } + for tab in &mut self.tabs { + if let Some(split) = &mut tab.split + && split.id == tab_id + { + split.pty_tx = Some(tx); + split.pty_alive = true; + return iced::Task::none(); + } } iced::Task::none() } @@ -208,6 +239,16 @@ impl Nova { if let Some(tab) = self.tabs.iter_mut().find(|t| t.id == tab_id) { tab.pty_alive = false; tab.pty_tx = None; + return iced::Task::none(); + } + for tab in &mut self.tabs { + if let Some(split) = &mut tab.split + && split.id == tab_id + { + split.pty_alive = false; + split.pty_tx = None; + return iced::Task::none(); + } } iced::Task::none() } diff --git a/src/ui/app_state/update/mouse.rs b/src/ui/app_state/update/mouse.rs index afd6c2b..2875885 100644 --- a/src/ui/app_state/update/mouse.rs +++ b/src/ui/app_state/update/mouse.rs @@ -115,6 +115,12 @@ impl Nova { } if button == mouse::Button::Left { + if let Some(tab) = self.tabs.get_mut(self.active_index) + && tab.split.is_some() + { + tab.active_pane_is_split = self.cursor_position.x > self.window_size.width / 2.0; + } + let now = std::time::Instant::now(); let threshold = std::time::Duration::from_millis(500); if cell.is_some() @@ -233,6 +239,17 @@ impl Nova { } let rows = (delta.abs() * 3.0).round() as usize; + if tab.active_pane_is_split { + if let Some(split) = &mut tab.split { + if delta > 0.0 { + let new_offset = split.scroll_offset.saturating_add(rows); + split.scroll_offset = new_offset.min(split.grid.scrollback.len()); + } else { + split.scroll_offset = split.scroll_offset.saturating_sub(rows); + } + } + return; + } let old_offset = tab.scroll_offset; if delta > 0.0 { let new_offset = tab.scroll_offset.saturating_add(rows); diff --git a/src/ui/app_state/update/tabs.rs b/src/ui/app_state/update/tabs.rs index b77eb32..9351f16 100644 --- a/src/ui/app_state/update/tabs.rs +++ b/src/ui/app_state/update/tabs.rs @@ -1,7 +1,11 @@ +use vte::Parser; + +use crate::core::grid::Grid; use crate::sys::pty::PtyCommand; -use crate::ui::tab::Tab; +use crate::ui::tab::{SplitPane, Tab, shell_display_name}; -use super::super::helpers::{calc_grid, command_history_path}; +use super::super::helpers::{calc_grid, calc_grid_split, command_history_path}; +use super::super::message::Message; use super::super::nova::Nova; impl Nova { @@ -23,20 +27,48 @@ impl Nova { } pub(super) fn resize_all_grids(&mut self) { - let (cols, rows) = calc_grid( - self.window_size.width, - self.window_size.height, - self.settings.theme.font.size, - self.settings.status_bar.visible, - self.diagnostic_banner.is_some(), - ); + let banner_visible = self.diagnostic_banner.is_some(); for tab in self.tabs.iter_mut() { - tab.grid.resize(cols, rows); - if let Some(tx) = &tab.pty_tx { - let _ = tx.send_blocking(PtyCommand::Resize { - cols: cols as u16, - rows: rows as u16, - }); + let has_split = tab.split.is_some(); + if has_split { + let (cols, rows) = calc_grid_split( + self.window_size.width, + self.window_size.height, + self.settings.theme.font.size, + self.settings.status_bar.visible, + banner_visible, + ); + tab.grid.resize(cols, rows); + if let Some(tx) = &tab.pty_tx { + let _ = tx.send_blocking(PtyCommand::Resize { + cols: cols as u16, + rows: rows as u16, + }); + } + if let Some(split) = &mut tab.split { + split.grid.resize(cols, rows); + if let Some(tx) = &split.pty_tx { + let _ = tx.send_blocking(PtyCommand::Resize { + cols: cols as u16, + rows: rows as u16, + }); + } + } + } else { + let (cols, rows) = calc_grid( + self.window_size.width, + self.window_size.height, + self.settings.theme.font.size, + self.settings.status_bar.visible, + banner_visible, + ); + tab.grid.resize(cols, rows); + if let Some(tx) = &tab.pty_tx { + let _ = tx.send_blocking(PtyCommand::Resize { + cols: cols as u16, + rows: rows as u16, + }); + } } } } @@ -61,4 +93,122 @@ impl Nova { self.active_index = self.tabs.len() - 1; } } + + pub(super) fn handle_split_pane(&mut self) -> iced::Task { + let Some(tab) = self.tabs.get_mut(self.active_index) else { + return iced::Task::none(); + }; + if tab.split.is_some() { + return iced::Task::none(); + } + let shell_cmd = tab.shell_cmd.clone(); + let initial_cwd = tab.pwd.clone(); + let split_id = self.next_tab_id; + self.next_tab_id += 1; + + let (cols, rows) = calc_grid_split( + self.window_size.width, + self.window_size.height, + self.settings.theme.font.size, + self.settings.status_bar.visible, + self.diagnostic_banner.is_some(), + ); + + let tab = &mut self.tabs[self.active_index]; + tab.split = Some(SplitPane { + id: split_id, + grid: Grid::new(cols, rows), + pty_tx: None, + pty_alive: true, + ansi_parser: Parser::new(), + shell_cmd, + pwd: initial_cwd.clone(), + scroll_offset: 0, + initial_cwd, + }); + tab.active_pane_is_split = true; + + let (primary_cols, primary_rows) = calc_grid_split( + self.window_size.width, + self.window_size.height, + self.settings.theme.font.size, + self.settings.status_bar.visible, + self.diagnostic_banner.is_some(), + ); + let tab = &mut self.tabs[self.active_index]; + tab.grid.resize(primary_cols, primary_rows); + if let Some(tx) = &tab.pty_tx { + let _ = tx.send_blocking(PtyCommand::Resize { + cols: primary_cols as u16, + rows: primary_rows as u16, + }); + } + + iced::Task::none() + } + + pub(super) fn handle_close_split_pane(&mut self) { + let Some(tab) = self.tabs.get_mut(self.active_index) else { + return; + }; + tab.split = None; + tab.active_pane_is_split = false; + + let (cols, rows) = calc_grid( + self.window_size.width, + self.window_size.height, + self.settings.theme.font.size, + self.settings.status_bar.visible, + self.diagnostic_banner.is_some(), + ); + let tab = &mut self.tabs[self.active_index]; + tab.grid.resize(cols, rows); + if let Some(tx) = &tab.pty_tx { + let _ = tx.send_blocking(PtyCommand::Resize { + cols: cols as u16, + rows: rows as u16, + }); + } + } + + pub(super) fn handle_close_left_pane(&mut self) { + let Some(tab) = self.tabs.get_mut(self.active_index) else { + return; + }; + let Some(split) = tab.split.take() else { + return; + }; + + drop(tab.pty_tx.take()); + + tab.id = split.id; + tab.grid = split.grid; + tab.pty_tx = split.pty_tx; + tab.pty_alive = split.pty_alive; + tab.ansi_parser = split.ansi_parser; + tab.shell_cmd = split.shell_cmd.clone(); + tab.shell = shell_display_name(&split.shell_cmd); + tab.pwd = split.pwd; + tab.scroll_offset = split.scroll_offset; + tab.initial_cwd = split.initial_cwd; + tab.current_input = String::new(); + tab.active_pane_is_split = false; + tab.update_git_status(); + + let (cols, rows) = calc_grid( + self.window_size.width, + self.window_size.height, + self.settings.theme.font.size, + self.settings.status_bar.visible, + self.diagnostic_banner.is_some(), + ); + let tab = &mut self.tabs[self.active_index]; + tab.grid.resize(cols, rows); + if let Some(tx) = &tab.pty_tx { + let _ = tx.send_blocking(PtyCommand::Resize { + cols: cols as u16, + rows: rows as u16, + }); + } + } } diff --git a/src/ui/app_state/update/window.rs b/src/ui/app_state/update/window.rs index ae8dd0d..4f0225b 100644 --- a/src/ui/app_state/update/window.rs +++ b/src/ui/app_state/update/window.rs @@ -1,6 +1,6 @@ use crate::sys::pty::PtyCommand; -use super::super::helpers::calc_grid; +use super::super::helpers::{calc_grid, calc_grid_split}; use super::super::message::Message; use super::super::nova::Nova; @@ -10,24 +10,46 @@ impl Nova { return iced::Task::none(); } self.window_size = iced::Size::new(width, height); - let (cols, rows) = calc_grid( - width, - height, - self.settings.theme.font.size, - self.settings.status_bar.visible, - self.diagnostic_banner.is_some(), - ); + let banner_visible = self.diagnostic_banner.is_some(); + let font_size = self.settings.theme.font.size; + let status_bar_visible = self.settings.status_bar.visible; for tab in self.tabs.iter_mut() { - if tab.grid.cols == cols && tab.grid.rows == rows { - continue; - } - tab.grid.resize(cols, rows); - if let Some(tx) = &tab.pty_tx { - let _ = tx.send_blocking(PtyCommand::Resize { - cols: cols as u16, - rows: rows as u16, - }); + if tab.split.is_some() { + let (cols, rows) = + calc_grid_split(width, height, font_size, status_bar_visible, banner_visible); + if tab.grid.cols != cols || tab.grid.rows != rows { + tab.grid.resize(cols, rows); + if let Some(tx) = &tab.pty_tx { + let _ = tx.send_blocking(PtyCommand::Resize { + cols: cols as u16, + rows: rows as u16, + }); + } + } + if let Some(split) = &mut tab.split + && (split.grid.cols != cols || split.grid.rows != rows) + { + split.grid.resize(cols, rows); + if let Some(tx) = &split.pty_tx { + let _ = tx.send_blocking(PtyCommand::Resize { + cols: cols as u16, + rows: rows as u16, + }); + } + } + } else { + let (cols, rows) = calc_grid(width, height, font_size, status_bar_visible, banner_visible); + if tab.grid.cols == cols && tab.grid.rows == rows { + continue; + } + tab.grid.resize(cols, rows); + if let Some(tx) = &tab.pty_tx { + let _ = tx.send_blocking(PtyCommand::Resize { + cols: cols as u16, + rows: rows as u16, + }); + } } } diff --git a/src/ui/app_state/view.rs b/src/ui/app_state/view.rs index d3bc487..08b4de1 100644 --- a/src/ui/app_state/view.rs +++ b/src/ui/app_state/view.rs @@ -2,8 +2,9 @@ use super::helpers::{dir_to_cursor, normalize_sel, resize_direction, strip_markd use super::message::Message; use super::nova::Nova; +use iced::alignment::{Horizontal, Vertical}; use iced::mouse; -use iced::widget::{button, column, container, mouse_area, stack, text}; +use iced::widget::{button, column, container, mouse_area, row, stack, text}; use iced::{Border, Color, Element, Length, Padding, Theme, border::Radius}; use crate::ui::components; @@ -31,16 +32,163 @@ impl Nova { mouse::Interaction::Text } }); - let term = mouse_area(components::term( - active_tab, - selection, - font_size, - active_tab.scroll_offset, - self.hovered_url.as_deref(), - self.hovered_link_span, - active_tab.grid.suggestion.as_deref(), - )) - .interaction(term_interaction); + + let term_area: Element<'_, Message> = if let Some(split) = &active_tab.split { + let rt = theme::color::runtime(); + let border_color = rt.border; + let accent = rt.accent; + let fg_muted = rt.foreground_muted; + drop(rt); + + let primary_selection = if !active_tab.active_pane_is_split { + selection + } else { + None + }; + let primary_url = if !active_tab.active_pane_is_split { + self.hovered_url.as_deref() + } else { + None + }; + let primary_span = if !active_tab.active_pane_is_split { + self.hovered_link_span + } else { + None + }; + + let make_close_btn = move |msg: Message| { + container( + button(text("\u{2715}").size(10).color(fg_muted)) + .on_press(msg) + .padding(Padding::from([2, 6])) + .style(move |_t, status| button::Style { + text_color: match status { + button::Status::Hovered | button::Status::Pressed => theme::color::RED.as_color(), + _ => fg_muted, + }, + background: None, + ..Default::default() + }), + ) + .align_x(Horizontal::Right) + .align_y(Vertical::Top) + .padding(Padding::from([4, 4])) + .width(Length::Fill) + .height(Length::Fill) + }; + + let make_active_triangle = move |is_active: bool| { + const SIZE: u32 = 16; + let fill = if is_active { + format!( + "#{:02x}{:02x}{:02x}", + (accent.r * 255.0) as u8, + (accent.g * 255.0) as u8, + (accent.b * 255.0) as u8, + ) + } else { + "none".to_string() + }; + let svg_data = format!( + r#""#, + s = SIZE, + fill = fill, + ); + let handle = iced::widget::svg::Handle::from_memory(svg_data.into_bytes()); + container(iced::widget::svg(handle).width(SIZE).height(SIZE)) + .align_x(Horizontal::Left) + .align_y(Vertical::Top) + .width(Length::Fill) + .height(Length::Fill) + }; + + let left = mouse_area( + container(stack![ + components::term( + &active_tab.grid, + primary_selection, + font_size, + active_tab.scroll_offset, + primary_url, + primary_span, + ), + make_active_triangle(!active_tab.active_pane_is_split), + make_close_btn(Message::CloseLeftPane), + ]) + .style(move |_| { + let border_width = if !active_tab.active_pane_is_split { + 1.0 + } else { + 0.0 + }; + container::Style { + border: Border { + color: accent, + width: border_width, + radius: Radius::new(0.0), + }, + ..Default::default() + } + }) + .width(Length::Fill) + .height(Length::Fill), + ) + .interaction(term_interaction); + + let divider = container(iced::widget::Space::new()) + .width(1) + .height(Length::Fill) + .style(move |_| container::Style { + background: Some(border_color.into()), + ..Default::default() + }); + + let right = mouse_area( + container(stack![ + components::term( + &split.grid, + None, + font_size, + split.scroll_offset, + None, + None, + ), + make_active_triangle(active_tab.active_pane_is_split), + make_close_btn(Message::CloseSplitPane), + ]) + .style(move |_| { + let border_width = if active_tab.active_pane_is_split { + 1.0 + } else { + 0.0 + }; + container::Style { + border: Border { + color: accent, + width: border_width, + radius: Radius::new(0.0), + }, + ..Default::default() + } + }) + .width(Length::Fill) + .height(Length::Fill), + ) + .interaction(term_interaction); + + row![left, divider, right].height(Length::Fill).into() + } else { + mouse_area(components::term( + &active_tab.grid, + selection, + font_size, + active_tab.scroll_offset, + self.hovered_url.as_deref(), + self.hovered_link_span, + )) + .interaction(term_interaction) + .into() + }; let tb_interaction = resize_cursor.unwrap_or(mouse::Interaction::Idle); @@ -54,7 +202,7 @@ impl Nova { self.bell_blink_visible, ), components::tab_bar(&self.tabs, self.active_index), - term, + term_area, ]; if let Some((_code, ref message, ref command)) = self.diagnostic_banner { diff --git a/src/ui/components/term.rs b/src/ui/components/term.rs index 68aa99a..ab26ece 100644 --- a/src/ui/components/term.rs +++ b/src/ui/components/term.rs @@ -3,8 +3,9 @@ use iced::{ widget::{column, container, rich_text, text::Span}, }; +use crate::core::grid::Grid; use crate::core::url::detect_urls; -use crate::ui::{app_state::Message, tab::Tab, theme}; +use crate::ui::{app_state::Message, theme}; fn in_selection(x: usize, y: usize, sel: Option<(usize, usize, usize, usize)>) -> bool { let (sc, sr, ec, er) = match sel { @@ -181,20 +182,20 @@ fn row_spans( spans } +#[allow(clippy::too_many_arguments)] pub fn term<'a>( - active_tab: &Tab, + grid: &Grid, selection: Option<(usize, usize, usize, usize)>, font_size: f32, scroll_offset: usize, hovered_url: Option<&str>, hovered_link_span: Option<(usize, usize, usize)>, - suggestion: Option<&str>, ) -> Element<'a, Message> { let mut grid_ui = column![].spacing(0); - let cursor_x = active_tab.grid.cursor_x; - let cursor_y = active_tab.grid.cursor_y; - let scrollback = &active_tab.grid.scrollback; + let cursor_x = grid.cursor_x; + let cursor_y = grid.cursor_y; + let scrollback = &grid.scrollback; let sb_len = scrollback.len(); let clamped_offset = scroll_offset.min(sb_len); @@ -219,9 +220,9 @@ pub fn term<'a>( viewport_y += 1; } - let live_count = active_tab.grid.rows.saturating_sub(clamped_offset); - for (y, row_cells) in active_tab.grid.cells[..live_count * active_tab.grid.cols] - .chunks_exact(active_tab.grid.cols) + let live_count = grid.rows.saturating_sub(clamped_offset); + for (y, row_cells) in grid.cells[..live_count * grid.cols] + .chunks_exact(grid.cols) .enumerate() { let cursor_col = if clamped_offset == 0 && y == cursor_y { @@ -230,7 +231,7 @@ pub fn term<'a>( None }; let row_suggestion = if clamped_offset == 0 && y == cursor_y { - suggestion + grid.suggestion.as_deref() } else { None }; @@ -250,10 +251,7 @@ pub fn term<'a>( viewport_y += 1; } - let (term_bg, _term_border) = { - let rt = theme::color::runtime(); - (rt.background, rt.border) - }; + let term_bg = theme::color::runtime().background; container(grid_ui) .style(move |_| container::Style { diff --git a/src/ui/tab.rs b/src/ui/tab.rs index 386fb39..a13c269 100644 --- a/src/ui/tab.rs +++ b/src/ui/tab.rs @@ -6,6 +6,18 @@ use vte::Parser; use crate::core::grid::Grid; use crate::sys::pty::PtyCommand; +pub struct SplitPane { + pub id: usize, + pub grid: Grid, + pub pty_tx: Option>, + pub pty_alive: bool, + pub ansi_parser: Parser, + pub shell_cmd: String, + pub pwd: String, + pub scroll_offset: usize, + pub initial_cwd: String, +} + pub struct Tab { pub id: usize, pub grid: Grid, @@ -20,6 +32,8 @@ pub struct Tab { pub scroll_offset: usize, pub initial_cwd: String, pub current_input: String, + pub split: Option, + pub active_pane_is_split: bool, } impl Tab { @@ -39,11 +53,13 @@ impl Tab { scroll_offset: 0, initial_cwd, current_input: String::new(), + split: None, + active_pane_is_split: false, } } } -fn shell_display_name(cmd: &str) -> String { +pub fn shell_display_name(cmd: &str) -> String { #[cfg(target_os = "windows")] { let lower = cmd.to_lowercase();