refactor(shell): own window interaction state

This commit is contained in:
Kang
2026-07-28 22:41:08 +08:00
parent 69c80d7503
commit 69f8e4c29d
93 changed files with 1432 additions and 925 deletions
@@ -1,9 +1,9 @@
use crate::models::{
ActivityBarLayoutState, BottomPanelMode, HeaderStatusState, MainMode, NavItem, PanelSide,
RightFocus, SessionEventBridge, SettingsTab, StoreStatus, TerminalFramePipeline,
ActivityBarLayoutState, BottomPanelMode, NavItem, PanelSide, SessionEventBridge, StoreStatus,
TerminalFramePipeline,
};
use crate::terminal::initial_terminal_screen;
use gpui::{Context, ScrollHandle};
use gpui::Context;
use nyaterm_core::{
AiSettings, AppRuntime, AppSettingsSummary, CLOUD_SYNC_HISTORY_LIMIT, CloudSyncSettings,
CloudSyncState, ConnectionStore, KeywordHighlightConfig, NativeServices, TranslationSettings,
@@ -11,7 +11,7 @@ use nyaterm_core::{
};
use nyaterm_terminal::TerminalOutputDecoder;
use nyaterm_transport::{SessionManager, SftpDuplicatePolicy};
use std::collections::{HashMap, HashSet, VecDeque};
use std::collections::{HashMap, VecDeque};
use std::sync::Arc;
use super::super::settings::{SettingsFeatureFocus, SettingsFeatureState};
@@ -21,10 +21,10 @@ use super::super::{
QuickCommandFeatureFocus, QuickCommandFeatureState, RecordingFeatureState,
RemoteOpsFeatureFocus, RemoteOpsFeatureState, SecurityFeatureFocus, SecurityFeatureState,
SendCommandFeatureFocus, SendCommandFeatureState, SessionFeatureFocus, SessionFeatureState,
ShellFeatureState, SyncInputFeatureState, TerminalFeatureFocus, TerminalFeatureState,
TextInputRegistry, TransferFeatureFocus, TransferFeatureState, TranslationFeatureState,
TunnelFeatureState, UpdateFeatureState, ai_active_profile_drafts, ai_usage_counts,
appearance_font_options, quick_command_sort_mode_from_setting,
ShellFeatureInit, ShellFeatureState, SyncInputFeatureState, TerminalFeatureFocus,
TerminalFeatureState, TextInputRegistry, TransferFeatureFocus, TransferFeatureState,
TranslationFeatureState, TunnelFeatureState, UpdateFeatureState, ai_active_profile_drafts,
ai_usage_counts, appearance_font_options, quick_command_sort_mode_from_setting,
quick_command_view_mode_from_setting, spawn_command_persistence_worker,
};
use super::NyaTermApp;
@@ -444,17 +444,28 @@ impl NyaTermApp {
},
),
text_inputs: TextInputRegistry::default(),
shell: ShellFeatureState::new(
if settings.ui_serial_send_visible {
shell: ShellFeatureState::new(ShellFeatureInit {
bottom_panel_mode: if settings.ui_serial_send_visible {
BottomPanelMode::CommandSend
} else if settings.ui_quick_cmd_visible {
BottomPanelMode::QuickCommands
} else {
BottomPanelMode::Hidden
},
quick_cmd_height,
serial_send_height,
),
quick_commands_height: quick_cmd_height,
command_send_height: serial_send_height,
active_left_panel,
active_right_panel,
left_open_panels,
right_open_panels,
panel_stack_sizes,
panel_multi_open,
left_sidebar_collapsed,
right_inspector_collapsed,
left_panel_width,
right_panel_width,
activity_bar_layout,
}),
sync_input: SyncInputFeatureState::new(cx.focus_handle()),
keyword_highlights,
settings,
@@ -472,53 +483,6 @@ impl NyaTermApp {
active_snapshot_password_prompt: None,
pending_session_events: VecDeque::new(),
diagnostic_log_last_at: HashMap::new(),
last_viewport_size: (1280., 800.),
wallpaper_tile_dimensions: None,
last_viewport_change_at: None,
title_drag_active_until: None,
selected_nav: NavItem::Workspace,
main_mode: MainMode::Workspace,
settings_active_tab: SettingsTab::General,
settings_expanded_groups: HashSet::from(["workspace".to_string()]),
settings_draft_snapshot: None,
settings_window: None,
settings_window_open_pending: false,
settings_previous_left_collapsed: None,
settings_previous_right_collapsed: None,
active_left_panel,
active_right_panel,
left_open_panels,
right_open_panels,
panel_stack_sizes,
panel_multi_open,
right_focus: RightFocus::Default,
left_sidebar_collapsed,
right_inspector_collapsed,
mobile_left_open: false,
mobile_right_open: false,
left_panel_width,
right_panel_width,
panel_resize: None,
panel_stack_resize: None,
activity_bar_layout,
activity_bar_context_menu: None,
title_menu_open: None,
title_menu_submenu: None,
header_status: HeaderStatusState::default(),
open_tabs_menu_open: false,
new_session_menu_open: false,
new_session_all_sessions_open: false,
new_session_group_menu_path: Vec::new(),
session_tab_strip_scroll: ScrollHandle::new(),
session_tab_scroll_into_view_pending: false,
last_connect_failure_name: None,
last_connect_failure_error: None,
workspace_split: None,
workspace_split_resize: None,
session_pane_roots: HashMap::new(),
session_tab_owner: HashMap::new(),
focused_terminal_window_leaf_id: None,
workspace_pane_layout_restored: false,
startup_restore_complete: false,
}
}
@@ -1,8 +1,8 @@
use std::collections::{HashMap, HashSet, VecDeque};
use std::collections::{HashMap, VecDeque};
use std::sync::{Arc, mpsc};
use std::time::Instant;
use gpui::{ScrollHandle, WindowHandle};
use gpui::WindowHandle;
use nyaterm_core::{
AppRuntime, AppSettingsSummary, CommandHistoryEntry, Group, KeywordHighlightConfig,
NativeServices, OtpEntry, ProxyConfig, ProxyGroup, QuickCommand, QuickCommandCategory,
@@ -21,7 +21,6 @@ use super::remote_editor_window::RemoteFileEditorWindow;
use super::runtime_jobs::{CommandPersistenceRequest, CommandPersistenceResult};
use super::session::SessionFeatureState;
use super::settings::{SecurityFeatureState, SettingsFeatureState};
use super::settings_window::SettingsWindow;
use super::shell::ShellFeatureState;
use super::sync::CloudSyncFeatureState;
use super::sync_input::SyncInputFeatureState;
@@ -32,11 +31,8 @@ use super::translation::TranslationFeatureState;
use super::tunnels::TunnelFeatureState;
use super::update::UpdateFeatureState;
use crate::models::{
ActivityBarContextMenuState, ActivityBarLayoutState, ConfigPathPromptKind,
DiagnosticsPathPromptKind, HeaderStatusState, KeywordHighlightPathPromptKind, MainMode,
NavItem, PanelResizeState, PanelStackResizeState, RightFocus, SettingsTab,
SnapshotPasswordPromptState, StoreStatus, TitleMenu, TitleMenuSubmenu, WorkspacePaneNode,
WorkspaceSplitResizeState, WorkspaceSplitState,
ConfigPathPromptKind, DiagnosticsPathPromptKind, KeywordHighlightPathPromptKind,
SnapshotPasswordPromptState, StoreStatus,
};
mod construct;
@@ -104,70 +100,5 @@ pub struct NyaTermApp {
pub(in crate::features) active_snapshot_password_prompt: Option<SnapshotPasswordPromptState>,
pub(in crate::features) pending_session_events: VecDeque<SessionEvent>,
pub(in crate::features) diagnostic_log_last_at: HashMap<&'static str, Instant>,
pub(in crate::features) last_viewport_size: (f32, f32),
/// Cached intrinsic dimensions for the current tiled wallpaper path.
pub(in crate::features) wallpaper_tile_dimensions: Option<(String, u32, u32)>,
/// When the window viewport last changed (resize/drag geometry).
pub(in crate::features) last_viewport_change_at: Option<Instant>,
/// Deadline for treating title-bar window dragging as active.
pub(in crate::features) title_drag_active_until: Option<Instant>,
pub(in crate::features) selected_nav: NavItem,
pub(in crate::features) main_mode: MainMode,
pub(in crate::features) settings_active_tab: SettingsTab,
/// Expanded multi-item groups in the settings sidebar (Tauri keeps this local to the page).
pub(in crate::features) settings_expanded_groups: HashSet<String>,
/// Committed values captured when the in-window settings page opens.
pub(in crate::features) settings_draft_snapshot: Option<SettingsDraftSnapshot>,
pub(in crate::features) settings_window: Option<WindowHandle<SettingsWindow>>,
pub(in crate::features) settings_window_open_pending: bool,
/// Main workspace panel state to restore after leaving the in-window settings page.
pub(in crate::features) settings_previous_left_collapsed: Option<bool>,
pub(in crate::features) settings_previous_right_collapsed: Option<bool>,
pub(in crate::features) active_left_panel: Option<NavItem>,
pub(in crate::features) active_right_panel: Option<NavItem>,
pub(in crate::features) left_open_panels: Vec<String>,
pub(in crate::features) right_open_panels: Vec<String>,
pub(in crate::features) panel_stack_sizes: HashMap<String, f32>,
pub(in crate::features) panel_multi_open: bool,
pub(in crate::features) right_focus: RightFocus,
pub(in crate::features) left_sidebar_collapsed: bool,
pub(in crate::features) right_inspector_collapsed: bool,
pub(in crate::features) mobile_left_open: bool,
pub(in crate::features) mobile_right_open: bool,
pub(in crate::features) left_panel_width: f32,
pub(in crate::features) right_panel_width: f32,
pub(in crate::features) panel_resize: Option<PanelResizeState>,
pub(in crate::features) panel_stack_resize: Option<PanelStackResizeState>,
pub(in crate::features) activity_bar_layout: ActivityBarLayoutState,
pub(in crate::features) activity_bar_context_menu: Option<ActivityBarContextMenuState>,
pub(in crate::features) title_menu_open: Option<TitleMenu>,
pub(in crate::features) title_menu_submenu: Option<TitleMenuSubmenu>,
pub(in crate::features) header_status: HeaderStatusState,
/// Open-tabs overflow menu (Tauri TabBar expand-more when many tabs).
pub(in crate::features) open_tabs_menu_open: bool,
/// New-session menu next to the tab strip + control.
pub(in crate::features) new_session_menu_open: bool,
/// Whether the Tauri-style "All sessions" submenu is expanded.
pub(in crate::features) new_session_all_sessions_open: bool,
/// Hovered group ids from the root submenu through the deepest open child menu.
pub(in crate::features) new_session_group_menu_path: Vec<String>,
/// Horizontal scroll handle for the global session tab strip (scroll-into-view).
pub(in crate::features) session_tab_strip_scroll: ScrollHandle,
/// Request scroll-into-view of the active tab on next paint (Tauri TabBar).
pub(in crate::features) session_tab_scroll_into_view_pending: bool,
/// Last failed connect name (shown as ephemeral failed tab chrome).
pub(in crate::features) last_connect_failure_name: Option<String>,
/// Last failed connect error text.
pub(in crate::features) last_connect_failure_error: Option<String>,
/// Legacy/global active pane tree view: mirrors the active tab's per-tab root when split.
pub(in crate::features) workspace_split: Option<WorkspaceSplitState>,
pub(in crate::features) workspace_split_resize: Option<WorkspaceSplitResizeState>,
/// Per-tab pane trees keyed by tab-root session id (Tauri `Tab.root`).
pub(in crate::features) session_pane_roots: HashMap<String, WorkspacePaneNode>,
/// Leaf session id → owning tab-root session id (hidden from tab strip when secondary).
pub(in crate::features) session_tab_owner: HashMap<String, String>,
pub(in crate::features) focused_terminal_window_leaf_id: Option<String>,
/// Whether we already attempted startup restore of global workspace pane splits.
pub(in crate::features) workspace_pane_layout_restored: bool,
pub(in crate::features) startup_restore_complete: bool,
}
@@ -1154,7 +1154,7 @@ impl NyaTermApp {
insets.left,
insets.top,
self.terminal_gutter_width_px_for_session(session_id),
self.last_viewport_size,
self.shell.viewport.size,
cursor_row,
cursor_col,
menu_w,
@@ -47,8 +47,8 @@ impl NyaTermApp {
self.connection_state.open_import_dialog();
self.connection_state.close_list_more_menu();
self.title_menu_open = None;
self.title_menu_submenu = None;
self.shell.chrome.title_menu_open = None;
self.shell.chrome.title_menu_submenu = None;
self.terminal.view.status = "select a connection import source".to_string();
let import_focus = self.connection_state.import_focus_handle();
window.focus(&import_focus);
@@ -185,8 +185,8 @@ impl NyaTermApp {
) -> bool {
if self.saved_connection_start_is_pending_or_queued(&connection) {
self.terminal.view.status = format!("{} is already queued", connection.name);
self.selected_nav = NavItem::Workspace;
self.main_mode = MainMode::Workspace;
self.shell.navigation.selected_nav = NavItem::Workspace;
self.shell.navigation.main_mode = MainMode::Workspace;
cx.notify();
return false;
}
@@ -200,8 +200,8 @@ impl NyaTermApp {
"queued {name} ({} pending)",
self.pending_saved_connection_queue.len()
);
self.selected_nav = NavItem::Workspace;
self.main_mode = MainMode::Workspace;
self.shell.navigation.selected_nav = NavItem::Workspace;
self.shell.navigation.main_mode = MainMode::Workspace;
cx.notify();
true
}
@@ -687,7 +687,7 @@ impl NyaTermApp {
|this, _, _, cx| {
this.ai.discovery.menu_open =
false;
this.settings_active_tab =
this.shell.navigation.settings.active_tab =
SettingsTab::AiModels;
this.open_page(
NavItem::Settings,
@@ -86,7 +86,7 @@ impl NyaTermApp {
});
let quote_text = state.text.clone();
let copy_text = state.text.clone();
let (viewport_w, viewport_h) = self.last_viewport_size;
let (viewport_w, viewport_h) = self.shell.viewport.size;
let (menu_x, menu_y, menu_max_h) = ai_message_menu_position(
f32::from(state.x),
f32::from(state.y),
@@ -112,7 +112,7 @@ impl NyaTermApp {
.cursor_pointer()
.hover(|this| this.bg(rgb(0x2ea043)))
.on_click(cx.listener(|this, _, _, cx| {
this.settings_active_tab = SettingsTab::AiGeneral;
this.shell.navigation.settings.active_tab = SettingsTab::AiGeneral;
this.open_page(NavItem::Settings, cx);
}))
.child(self.tr("ai.setupAction")),
@@ -9,9 +9,9 @@ impl NyaTermApp {
window: &mut gpui::Window,
cx: &mut Context<Self>,
) -> impl IntoElement {
let mut width = self.right_panel_width.clamp(200., 720.);
if !cfg!(target_os = "macos") && self.last_viewport_size.0 < 768. {
width = width.min((self.last_viewport_size.0 - 80.).max(120.));
let mut width = self.shell.panels.right_width.clamp(200., 720.);
if !cfg!(target_os = "macos") && self.shell.viewport.size.0 < 768. {
width = width.min((self.shell.viewport.size.0 - 80.).max(120.));
}
let palette = self.theme_palette();
div()
@@ -15,12 +15,12 @@ impl NyaTermApp {
cx: &mut Context<Self>,
) -> impl IntoElement {
let palette = self.theme_palette();
let Some(menu) = self.activity_bar_context_menu.clone() else {
let Some(menu) = self.shell.chrome.activity_bar_context_menu.clone() else {
return div().into_any_element();
};
let entry_id = menu.entry_id.clone();
let show_labels = self.activity_bar_layout.show_labels;
let (viewport_w, viewport_h) = self.last_viewport_size;
let show_labels = self.shell.chrome.activity_bar_layout.show_labels;
let (viewport_w, viewport_h) = self.shell.viewport.size;
let menu_w = 180.;
let submenu_w = 164.;
let margin = 8.;
@@ -191,7 +191,7 @@ impl NyaTermApp {
let bottom_entries = self.activity_entries_for_zone(bottom_zone);
let top_len = top_entries.len();
let bottom_len = bottom_entries.len();
let show_labels = self.activity_bar_layout.show_labels;
let show_labels = self.shell.chrome.activity_bar_layout.show_labels;
let palette = self.theme_palette();
// Tauri DropZone: gap-0.5 pt-1
@@ -67,7 +67,7 @@ impl NyaTermApp {
.child(
div()
.id("duplicate-prompt-dialog")
.w(px((self.last_viewport_size.0 - 32.).min(448.).max(280.)))
.w(px((self.shell.viewport.size.0 - 32.).min(448.).max(280.)))
.rounded_md()
.border_1()
.border_color(rgb(palette.border))
@@ -616,7 +616,7 @@ impl NyaTermApp {
prompt.id
)))
.w_full()
.max_h(px((self.last_viewport_size.1 - 32.).max(240.)))
.max_h(px((self.shell.viewport.size.1 - 32.).max(240.)))
.overflow_y_scroll()
.rounded_md()
.border_1()
@@ -9,9 +9,9 @@ impl NyaTermApp {
window: &mut gpui::Window,
cx: &mut Context<Self>,
) -> impl IntoElement {
let mut width = self.left_panel_width.clamp(160., 720.);
if !cfg!(target_os = "macos") && self.last_viewport_size.0 < 1024. {
width = width.min((self.last_viewport_size.0 - 80.).max(120.));
let mut width = self.shell.panels.left_width.clamp(160., 720.);
if !cfg!(target_os = "macos") && self.shell.viewport.size.0 < 1024. {
width = width.min((self.shell.viewport.size.0 - 80.).max(120.));
}
let palette = self.theme_palette();
div()
@@ -28,8 +28,8 @@ impl NyaTermApp {
let palette = self.theme_palette();
let macos = cfg!(target_os = "macos");
let compact_layout = !cfg!(target_os = "macos");
let narrow_left = compact_layout && self.last_viewport_size.0 < 1024.;
let narrow_right = compact_layout && self.last_viewport_size.0 < 768.;
let narrow_left = compact_layout && self.shell.viewport.size.0 < 1024.;
let narrow_right = compact_layout && self.shell.viewport.size.0 < 768.;
let header_status_visible = self.settings.ui_header_status_visible;
let header_status = self.header_status_content();
// Match Tauri Header: h-10.
@@ -207,7 +207,7 @@ impl NyaTermApp {
cx: &mut Context<Self>,
) -> impl IntoElement {
let palette = self.theme_palette();
let menu_open = self.header_status.menu_open;
let menu_open = self.shell.chrome.header_status.menu_open;
let select_label = self.tr("headerStatus.select");
div()
@@ -406,14 +406,14 @@ impl NyaTermApp {
}
pub(in crate::features) fn toggle_header_status_menu(&mut self, cx: &mut Context<Self>) {
self.header_status.menu_open = !self.header_status.menu_open;
if self.header_status.menu_open {
self.title_menu_open = None;
self.title_menu_submenu = None;
self.open_tabs_menu_open = false;
self.new_session_menu_open = false;
self.new_session_all_sessions_open = false;
self.new_session_group_menu_path.clear();
self.shell.chrome.header_status.menu_open = !self.shell.chrome.header_status.menu_open;
if self.shell.chrome.header_status.menu_open {
self.shell.chrome.title_menu_open = None;
self.shell.chrome.title_menu_submenu = None;
self.shell.chrome.open_tabs_menu_open = false;
self.shell.chrome.new_session_menu_open = false;
self.shell.chrome.new_session_all_sessions_open = false;
self.shell.chrome.new_session_group_menu_path.clear();
}
cx.notify();
}
@@ -425,8 +425,8 @@ impl NyaTermApp {
) {
self.settings.ui_header_status_mode = mode.persistence_id().to_string();
self.settings.ui_header_status_visible = true;
self.header_status.menu_open = false;
self.header_status.rendered_minute = current_unix_minute();
self.shell.chrome.header_status.menu_open = false;
self.shell.chrome.header_status.rendered_minute = current_unix_minute();
self.persist_header_status_settings();
cx.notify();
}
@@ -437,13 +437,13 @@ impl NyaTermApp {
cx: &mut Context<Self>,
) {
self.settings.ui_header_status_visible = visible;
self.header_status.menu_open = false;
self.shell.chrome.header_status.menu_open = false;
self.persist_header_status_settings();
cx.notify();
}
fn persist_header_status_settings(&mut self) {
if self.settings_draft_snapshot.is_some() {
if self.shell.navigation.settings.draft_snapshot.is_some() {
self.terminal.view.status =
"header status changed; apply settings to persist".to_string();
} else {
@@ -461,14 +461,14 @@ impl NyaTermApp {
self.settings.ui_header_status_visible
&& HeaderStatusMode::from_setting(&self.settings.ui_header_status_mode)
== HeaderStatusMode::DateTime
&& self.header_status.rendered_minute != current_unix_minute()
&& self.shell.chrome.header_status.rendered_minute != current_unix_minute()
}
pub(in crate::features) fn refresh_header_status_clock(&mut self) -> bool {
if !self.header_status_clock_refresh_due() {
return false;
}
self.header_status.rendered_minute = current_unix_minute();
self.shell.chrome.header_status.rendered_minute = current_unix_minute();
true
}
@@ -509,7 +509,7 @@ impl NyaTermApp {
if let Some(failed) = self.failed_session_display_name() {
return failed;
}
if let Some(failed) = self.last_connect_failure_name.as_ref() {
if let Some(failed) = self.shell.chrome.last_connect_failure_name.as_ref() {
return failed.clone();
}
"NyaTerm".to_string()
@@ -530,7 +530,8 @@ impl NyaTermApp {
if self.has_pending_session_start() {
return Some("icons/conn/connect.svg");
}
if self.has_failed_session_start() || self.last_connect_failure_name.is_some() {
if self.has_failed_session_start() || self.shell.chrome.last_connect_failure_name.is_some()
{
return Some("icons/session/disconnect.svg");
}
None
@@ -541,7 +542,7 @@ impl NyaTermApp {
menu: TitleMenu,
cx: &mut Context<Self>,
) -> impl IntoElement {
let open = self.title_menu_open == Some(menu);
let open = self.shell.chrome.title_menu_open == Some(menu);
let id_label = menu.label();
let label = self.tr(menu.i18n_key());
let palette = self.theme_palette();
@@ -585,25 +586,25 @@ impl NyaTermApp {
menu: TitleMenu,
cx: &mut Context<Self>,
) {
self.title_menu_open = if self.title_menu_open == Some(menu) {
self.shell.chrome.title_menu_open = if self.shell.chrome.title_menu_open == Some(menu) {
None
} else {
Some(menu)
};
self.title_menu_submenu = None;
if self.title_menu_open.is_some() {
self.header_status.menu_open = false;
self.open_tabs_menu_open = false;
self.new_session_menu_open = false;
self.new_session_all_sessions_open = false;
self.new_session_group_menu_path.clear();
self.shell.chrome.title_menu_submenu = None;
if self.shell.chrome.title_menu_open.is_some() {
self.shell.chrome.header_status.menu_open = false;
self.shell.chrome.open_tabs_menu_open = false;
self.shell.chrome.new_session_menu_open = false;
self.shell.chrome.new_session_all_sessions_open = false;
self.shell.chrome.new_session_group_menu_path.clear();
}
cx.notify();
}
pub(in crate::features) fn close_title_menu(&mut self, cx: &mut Context<Self>) {
self.title_menu_submenu = None;
if self.title_menu_open.take().is_some() {
self.shell.chrome.title_menu_submenu = None;
if self.shell.chrome.title_menu_open.take().is_some() {
cx.notify();
}
}
@@ -613,8 +614,8 @@ impl NyaTermApp {
submenu: TitleMenuSubmenu,
cx: &mut Context<Self>,
) {
if self.title_menu_submenu != Some(submenu) {
self.title_menu_submenu = Some(submenu);
if self.shell.chrome.title_menu_submenu != Some(submenu) {
self.shell.chrome.title_menu_submenu = Some(submenu);
cx.notify();
}
}
@@ -43,10 +43,10 @@ impl NyaTermApp {
Some(shortcut("tab.newSession", "Ctrl+Shift+N")),
cx.listener(|this, _, window, cx| {
this.close_title_menu(cx);
this.open_tabs_menu_open = false;
this.new_session_menu_open = false;
this.new_session_all_sessions_open = false;
this.new_session_group_menu_path.clear();
this.shell.chrome.open_tabs_menu_open = false;
this.shell.chrome.new_session_menu_open = false;
this.shell.chrome.new_session_all_sessions_open = false;
this.shell.chrome.new_session_group_menu_path.clear();
this.open_connection_editor(None, None, false, window, cx);
}),
))
@@ -266,7 +266,7 @@ impl NyaTermApp {
}
}
items.when_some(self.title_menu_submenu, |this, submenu| {
items.when_some(self.shell.chrome.title_menu_submenu, |this, submenu| {
this.child(self.title_menu_submenu(submenu, cx))
})
}
@@ -280,7 +280,7 @@ impl NyaTermApp {
cx: &mut Context<Self>,
) -> impl IntoElement {
let palette = self.theme_palette();
let open = self.title_menu_submenu == Some(submenu);
let open = self.shell.chrome.title_menu_submenu == Some(submenu);
title_menu_submenu_trigger(
palette,
id,
@@ -293,10 +293,10 @@ impl NyaTermApp {
}
}),
cx.listener(move |this, _, _, cx| {
if this.title_menu_submenu == Some(submenu) {
this.title_menu_submenu = None;
if this.shell.chrome.title_menu_submenu == Some(submenu) {
this.shell.chrome.title_menu_submenu = None;
} else {
this.title_menu_submenu = Some(submenu);
this.shell.chrome.title_menu_submenu = Some(submenu);
}
cx.notify();
}),
@@ -151,7 +151,7 @@ impl NyaTermApp {
let error = self
.active_failed_session()
.map(|failed| failed.error.clone())
.or_else(|| self.last_connect_failure_error.clone())
.or_else(|| self.shell.chrome.last_connect_failure_error.clone())
.unwrap_or_default();
div()
@@ -165,7 +165,9 @@ impl NyaTermApp {
.as_deref()
.is_some_and(|id| self.tab_root_for_session(id) == session_id);
let leaf_ids = self
.session_pane_roots
.shell
.workspace
.pane_roots
.get(&session_id)
.map(|root| root.session_ids())
.unwrap_or_else(|| vec![session_id.clone()]);
@@ -282,7 +284,7 @@ impl NyaTermApp {
let no_shell_sessions_label = self.tr("terminal.noShellSessions");
let recent_sessions_label = self.tr("terminal.recentSessions");
let no_recent_sessions_label = self.tr("terminal.noRecentSessions");
let all_sessions_open = self.new_session_all_sessions_open;
let all_sessions_open = self.shell.chrome.new_session_all_sessions_open;
// Tauri TabBar new-session: shell sessions + recent by last_used.
let mut shell: Vec<_> = self
.connections
@@ -478,7 +480,7 @@ impl NyaTermApp {
&mut self,
cx: &mut Context<Self>,
) -> impl IntoElement {
let path = self.new_session_group_menu_path.clone();
let path = self.shell.chrome.new_session_group_menu_path.clone();
let visible_group_ids =
new_session_visible_group_ids(&self.connections, &self.connection_groups);
let mut parent_group_id = None;
@@ -715,23 +717,26 @@ impl NyaTermApp {
}
fn open_new_session_all_sessions_menu(&mut self, cx: &mut Context<Self>) {
if !self.new_session_all_sessions_open {
self.new_session_all_sessions_open = true;
self.new_session_group_menu_path.clear();
if !self.shell.chrome.new_session_all_sessions_open {
self.shell.chrome.new_session_all_sessions_open = true;
self.shell.chrome.new_session_group_menu_path.clear();
cx.notify();
}
}
fn toggle_new_session_all_sessions_menu(&mut self, cx: &mut Context<Self>) {
self.new_session_all_sessions_open = !self.new_session_all_sessions_open;
self.new_session_group_menu_path.clear();
self.shell.chrome.new_session_all_sessions_open =
!self.shell.chrome.new_session_all_sessions_open;
self.shell.chrome.new_session_group_menu_path.clear();
cx.notify();
}
fn close_new_session_all_sessions_menu(&mut self, cx: &mut Context<Self>) {
if self.new_session_all_sessions_open || !self.new_session_group_menu_path.is_empty() {
self.new_session_all_sessions_open = false;
self.new_session_group_menu_path.clear();
if self.shell.chrome.new_session_all_sessions_open
|| !self.shell.chrome.new_session_group_menu_path.is_empty()
{
self.shell.chrome.new_session_all_sessions_open = false;
self.shell.chrome.new_session_group_menu_path.clear();
cx.notify();
}
}
@@ -742,21 +747,27 @@ impl NyaTermApp {
depth: usize,
cx: &mut Context<Self>,
) {
let unchanged = self.new_session_all_sessions_open
&& self.new_session_group_menu_path.get(depth) == Some(&group_id)
&& self.new_session_group_menu_path.len() == depth + 1;
let unchanged = self.shell.chrome.new_session_all_sessions_open
&& self.shell.chrome.new_session_group_menu_path.get(depth) == Some(&group_id)
&& self.shell.chrome.new_session_group_menu_path.len() == depth + 1;
if unchanged {
return;
}
self.new_session_all_sessions_open = true;
self.new_session_group_menu_path.truncate(depth);
self.new_session_group_menu_path.push(group_id);
self.shell.chrome.new_session_all_sessions_open = true;
self.shell
.chrome
.new_session_group_menu_path
.truncate(depth);
self.shell.chrome.new_session_group_menu_path.push(group_id);
cx.notify();
}
fn truncate_new_session_group_menu(&mut self, depth: usize, cx: &mut Context<Self>) {
if self.new_session_group_menu_path.len() > depth {
self.new_session_group_menu_path.truncate(depth);
if self.shell.chrome.new_session_group_menu_path.len() > depth {
self.shell
.chrome
.new_session_group_menu_path
.truncate(depth);
cx.notify();
}
}
@@ -368,7 +368,7 @@ impl NyaTermApp {
.then_with(|| left.2.cmp(&right.2))
.then_with(|| left.3.cmp(&right.3))
});
if self.session_tab_scroll_into_view_pending {
if self.shell.chrome.session_tab_scroll_into_view_pending {
if let Some(active_id) = self.session.active_id.as_deref() {
if let Some(index) = sessions.iter().position(|session| session.id == active_id) {
let pending_count = transient_tabs
@@ -376,10 +376,13 @@ impl NyaTermApp {
.filter(|(pending_index, _, _, _, _, _)| *pending_index <= index)
.count();
let child_index = index + pending_count;
self.session_tab_strip_scroll.scroll_to_item(child_index);
self.shell
.chrome
.session_tab_strip_scroll
.scroll_to_item(child_index);
}
}
self.session_tab_scroll_into_view_pending = false;
self.shell.chrome.session_tab_scroll_into_view_pending = false;
}
let mut tabs = div()
.id("session-tab-strip-scroll")
@@ -391,7 +394,7 @@ impl NyaTermApp {
// Tauri tab-strip-scroll: horizontal overflow instead of clipping tabs.
.overflow_x_scroll()
.overflow_y_hidden()
.track_scroll(&self.session_tab_strip_scroll);
.track_scroll(&self.shell.chrome.session_tab_strip_scroll);
let mut transient_cursor = 0usize;
for (tab_index, session) in sessions.into_iter().enumerate() {
@@ -436,7 +439,9 @@ impl NyaTermApp {
.as_deref()
.is_some_and(|id| self.tab_root_for_session(id) == session.id);
let leaf_ids = self
.session_pane_roots
.shell
.workspace
.pane_roots
.get(&session.id)
.map(|root| root.session_ids())
.unwrap_or_else(|| vec![session.id.clone()]);
@@ -695,11 +700,17 @@ impl NyaTermApp {
}
// Tauri TabBar trailing chrome: optional open-tabs overflow menu + new session menu.
let open_tabs_menu = self.open_tabs_menu_open;
let new_session_menu = self.new_session_menu_open;
let open_tabs_menu = self.shell.chrome.open_tabs_menu_open;
let new_session_menu = self.shell.chrome.new_session_menu_open;
let open_tabs_label = self.tr("terminal.openTabs").to_string();
let new_session_label = self.tr("terminal.newSession").to_string();
let tab_strip_has_overflow = self.session_tab_strip_scroll.max_offset().width > px(0.);
let tab_strip_has_overflow = self
.shell
.chrome
.session_tab_strip_scroll
.max_offset()
.width
> px(0.);
// Tauri shows Open Tabs only when the strip actually overflows.
let show_open_tabs_menu = tab_strip_has_overflow || open_tabs_menu;
+1 -1
View File
@@ -87,7 +87,7 @@ pub(in crate::features) use session::{
pub(in crate::features) use settings::{SecurityFeatureFocus, SecurityFeatureState};
pub(in crate::features) use shell::{
ChromeTooltip, SessionTabDragPayload, SessionTabDragPreview, SessionTabTooltip,
ShellFeatureState, TAB_MOUSE_ACTIONS, TabMouseActionTarget,
ShellFeatureInit, ShellFeatureState, TAB_MOUSE_ACTIONS, TabMouseActionTarget,
};
pub(in crate::features) use shell::{
appearance_font_options, appearance_font_stack, gpui_code_font_family,
@@ -59,7 +59,7 @@ impl NyaTermApp {
cx: &mut Context<Self>,
) -> AnyElement {
let palette = self.theme_palette();
let (viewport_width, viewport_height) = self.last_viewport_size;
let (viewport_width, viewport_height) = self.shell.viewport.size;
let max_height = connection_menu_max_height(viewport_height);
let submenu_offset = connection_submenu_offset(f32::from(x), viewport_width);
deferred(
@@ -130,7 +130,7 @@ impl NyaTermApp {
cx: &mut Context<Self>,
) -> AnyElement {
let palette = self.theme_palette();
let max_height = connection_menu_max_height(self.last_viewport_size.1);
let max_height = connection_menu_max_height(self.shell.viewport.size.1);
let ungrouped_ids = move_ids.clone();
let mut panel = div()
.id(SharedString::from(id))
@@ -290,7 +290,7 @@ impl NyaTermApp {
self.tr("dockerManager.compose").to_string(),
],
self.tr("common.more").to_string(),
self.right_panel_width,
self.shell.panels.right_width,
self.remote_ops.docker.tab_menu_open,
cx,
))
@@ -99,7 +99,7 @@ impl NyaTermApp {
.cloned()
.collect::<Vec<_>>();
// Responsive mode first so hidden columns do not keep invalid sort keys.
let mode = process_display_mode(self.right_panel_width);
let mode = process_display_mode(self.shell.panels.right_width);
if mode != ProcessDisplayMode::Wide
&& self.remote_ops.process.sort_key == RemoteProcessSortKey::User
{
@@ -21,7 +21,7 @@ impl NyaTermApp {
&mut self,
cx: &mut Context<Self>,
) -> impl IntoElement {
self.settings_surface(self.last_viewport_size.0, false, cx)
self.settings_surface(self.shell.viewport.size.0, false, cx)
}
pub(in crate::features) fn settings_window_view(
@@ -61,8 +61,8 @@ impl NyaTermApp {
// Tauri SettingsPage shell: compact header + narrow nav + scroll content.
let palette = self.theme_palette();
let settings_title = self.tr("settings.title");
let active_group = self.tr(self.settings_active_tab.group_i18n_key());
let active_label = self.tr(self.settings_active_tab.i18n_key());
let active_group = self.tr(self.shell.navigation.settings.active_tab.group_i18n_key());
let active_label = self.tr(self.shell.navigation.settings.active_tab.i18n_key());
let back_label = self.tr("common.close");
div()
.flex()
@@ -303,9 +303,24 @@ impl NyaTermApp {
} else {
224.
};
let workspace_expanded = self.settings_expanded_groups.contains("workspace");
let terminal_expanded = self.settings_expanded_groups.contains("terminal_session");
let ai_expanded = self.settings_expanded_groups.contains("ai_group");
let workspace_expanded = self
.shell
.navigation
.settings
.expanded_groups
.contains("workspace");
let terminal_expanded = self
.shell
.navigation
.settings
.expanded_groups
.contains("terminal_session");
let ai_expanded = self
.shell
.navigation
.settings
.expanded_groups
.contains("ai_group");
let mut sidebar_nav = div()
.id(SharedString::from("settings-sidebar-scroll"))
@@ -540,8 +555,14 @@ impl NyaTermApp {
this.tooltip(move |_, cx| cx.new(|_| ChromeTooltip::new(title)).into())
})
.on_click(cx.listener(move |this, _, _, cx| {
if !this.settings_expanded_groups.insert(group.to_string()) {
this.settings_expanded_groups.remove(group);
if !this
.shell
.navigation
.settings
.expanded_groups
.insert(group.to_string())
{
this.shell.navigation.settings.expanded_groups.remove(group);
}
cx.notify();
}))
@@ -556,7 +577,7 @@ impl NyaTermApp {
cx: &mut Context<Self>,
) -> impl IntoElement {
let palette = self.theme_palette();
let selected = self.settings_active_tab == tab;
let selected = self.shell.navigation.settings.active_tab == tab;
let label = self.tr(tab.i18n_key());
// Tauri settings nav item: soft primary fill, no permanent green border.
@@ -619,7 +640,7 @@ impl NyaTermApp {
this.tooltip(move |_, cx| cx.new(|_| ChromeTooltip::new(label)).into())
})
.on_click(cx.listener(move |this, _, _, cx| {
this.settings_active_tab = tab;
this.shell.navigation.settings.active_tab = tab;
this.settings_state.appearance.menu_open = None;
cx.notify();
}))
@@ -631,7 +652,7 @@ impl NyaTermApp {
cx: &mut Context<Self>,
) -> impl IntoElement {
let palette = self.theme_palette();
let active_tab = self.settings_active_tab;
let active_tab = self.shell.navigation.settings.active_tab;
let active_label = self.tr(active_tab.i18n_key());
let content = self.settings_tab_content(active_tab, cx);
let compact = viewport_width < 640.;
@@ -436,7 +436,8 @@ impl NyaTermApp {
"cloud-open-security",
self.tr("settings.openSecuritySettings"),
cx.listener(|this, _, _, cx| {
this.settings_active_tab = SettingsTab::Security;
this.shell.navigation.settings.active_tab =
SettingsTab::Security;
cx.notify();
}),
)),
@@ -57,7 +57,7 @@ impl NyaTermApp {
settings_switch(
palette,
"appearance-panel-multi-open",
self.panel_multi_open,
self.shell.panels.multi_open,
cx.listener(|this, _, _, cx| {
this.toggle_panel_multi_open(cx);
}),
@@ -287,7 +287,7 @@ impl NyaTermApp {
} else {
// Tauri File Explorer virtual list (30px rows and overscan).
let viewport_rows = transfer_browser_viewport_rows(
self.last_viewport_size.1,
self.shell.viewport.size.1,
self.transfer.panel.height,
self.transfer.browser.viewport_height,
);
@@ -527,7 +527,7 @@ impl NyaTermApp {
let parent_count = usize::from(current_path != "/" && current_path != ".");
let total = this.visible_transfer_browser_entries().len() + parent_count;
let viewport_rows = transfer_browser_viewport_rows(
this.last_viewport_size.1,
this.shell.viewport.size.1,
this.transfer.panel.height,
this.transfer.browser.viewport_height,
);
@@ -32,7 +32,7 @@ impl NyaTermApp {
let description = self
.tr("fileTransfer.deleteConfirmDesc")
.replace("{{name}}", &state.title);
let dialog_width = transfer_dialog_width(self.last_viewport_size.0, 320.);
let dialog_width = transfer_dialog_width(self.shell.viewport.size.0, 320.);
div()
.id(SharedString::from("transfer-job-delete-overlay"))
@@ -152,7 +152,7 @@ impl NyaTermApp {
let retry_id = state.job_id.clone();
let open_id = state.job_id.clone();
let delete_id = state.job_id.clone();
let (viewport_w, viewport_h) = self.last_viewport_size;
let (viewport_w, viewport_h) = self.shell.viewport.size;
let (menu_x, menu_y, menu_max_height) = transfer_menu_position(
f32::from(state.x),
f32::from(state.y),
@@ -46,7 +46,7 @@ impl NyaTermApp {
.map(|entry| self.enabled_transfer_file_ai_actions_for_entry(entry))
.unwrap_or_default();
let has_ai_actions = !selected_file_ai_actions.is_empty();
let (viewport_w, viewport_h) = self.last_viewport_size;
let (viewport_w, viewport_h) = self.shell.viewport.size;
let preferred_height = if state.is_current_directory {
380.
} else if state.is_parent {
@@ -23,7 +23,7 @@ impl NyaTermApp {
cx: &mut Context<Self>,
) -> impl IntoElement {
let palette = self.theme_palette();
let dialog_width = transfer_dialog_width(self.last_viewport_size.0, 500.);
let dialog_width = transfer_dialog_width(self.shell.viewport.size.0, 500.);
let state = self
.transfer
.file_ops
@@ -228,7 +228,7 @@ impl NyaTermApp {
cx: &mut Context<Self>,
) -> impl IntoElement {
let palette = self.theme_palette();
let dialog_width = transfer_dialog_width(self.last_viewport_size.0, 500.);
let dialog_width = transfer_dialog_width(self.shell.viewport.size.0, 500.);
let state = self
.transfer
.file_ops
@@ -431,7 +431,7 @@ impl NyaTermApp {
cx: &mut Context<Self>,
) -> impl IntoElement {
let palette = self.theme_palette();
let dialog_width = transfer_dialog_width(self.last_viewport_size.0, 480.);
let dialog_width = transfer_dialog_width(self.shell.viewport.size.0, 480.);
let state = self
.transfer
.file_ops
@@ -42,7 +42,7 @@ impl NyaTermApp {
.map(|path| remote_file_name(path))
.collect::<Vec<_>>();
let remaining_items = delete_count.saturating_sub(preview_items.len());
let dialog_width = transfer_dialog_width(self.last_viewport_size.0, 320.);
let dialog_width = transfer_dialog_width(self.shell.viewport.size.0, 320.);
div()
.id(SharedString::from("transfer-delete-overlay"))
@@ -170,7 +170,7 @@ impl NyaTermApp {
cx,
)
.into_any_element();
let dialog_width = transfer_dialog_width(self.last_viewport_size.0, 384.);
let dialog_width = transfer_dialog_width(self.shell.viewport.size.0, 384.);
div()
.id(SharedString::from("transfer-move-overlay"))
@@ -365,7 +365,7 @@ impl NyaTermApp {
.min_w_0()
.flex()
.overflow_x_scroll();
let tabs_menu_max_height = (self.last_viewport_size.1 - 48.).clamp(160., 360.);
let tabs_menu_max_height = (self.shell.viewport.size.1 - 48.).clamp(160., 360.);
let tabs_menu_bg = if standalone {
rgb(palette.surface)
} else {
@@ -57,7 +57,7 @@ impl NyaTermApp {
.iter()
.cloned()
.collect::<Vec<_>>();
let (viewport_w, viewport_h) = self.last_viewport_size;
let (viewport_w, viewport_h) = self.shell.viewport.size;
let (menu_x, menu_y, menu_max_height) = transfer_menu_position(
f32::from(state.x),
f32::from(state.y),
@@ -80,8 +80,8 @@ impl NyaTermApp {
let property_mode = parse_transfer_mode(&state.mode_value)
.or(entry.permissions)
.unwrap_or(0o644);
let dialog_width = (self.last_viewport_size.0 - 32.).min(460.).max(280.);
let dialog_max_height = (self.last_viewport_size.1 * 0.75).clamp(320., 720.);
let dialog_width = (self.shell.viewport.size.0 - 32.).min(460.).max(280.);
let dialog_max_height = (self.shell.viewport.size.1 * 0.75).clamp(320., 720.);
let owner_input = self
.text_input_box(
"transfer.properties.owner",
@@ -38,7 +38,7 @@ impl NyaTermApp {
} else {
state.entry.name.clone()
};
let dialog_width = transfer_dialog_width(self.last_viewport_size.0, 512.);
let dialog_width = transfer_dialog_width(self.shell.viewport.size.0, 512.);
let description = self
.tr("fileExplorer.unknownFileTypeDesc")
.replace("{{name}}", &name);
@@ -48,7 +48,7 @@ impl NyaTermApp {
x: px(24.),
y: px(24.),
});
let (viewport_w, viewport_h) = self.last_viewport_size;
let (viewport_w, viewport_h) = self.shell.viewport.size;
let (menu_x, menu_y, menu_max_height) = transfer_menu_position(
f32::from(state.x),
f32::from(state.y),
@@ -316,7 +316,7 @@ impl NyaTermApp {
TransferBrowserChildrenMenuStatus::Error(_) => 112.,
},
};
let (viewport_w, viewport_h) = self.last_viewport_size;
let (viewport_w, viewport_h) = self.shell.viewport.size;
let (menu_x, menu_y, menu_max_height) = transfer_menu_position(
f32::from(menu.x),
f32::from(menu.y),
@@ -17,8 +17,8 @@ impl NyaTermApp {
// register/close/idle already keep terminal_windows coherent.
let multi_leaf = self.terminal_windows_is_multi_leaf();
let has_connect_failure = self.has_failed_session_start()
|| (self.last_connect_failure_name.is_some()
&& self.last_connect_failure_error.is_some());
|| (self.shell.chrome.last_connect_failure_name.is_some()
&& self.shell.chrome.last_connect_failure_error.is_some());
let show_tab_strip = !multi_leaf
&& (self.ordered_tab_session_count() > 0
|| self.has_pending_session_start()
@@ -52,8 +52,8 @@ impl NyaTermApp {
return self.pending_workspace_state().into_any_element();
}
if self.has_failed_session_start()
|| (self.last_connect_failure_name.is_some()
&& self.last_connect_failure_error.is_some())
|| (self.shell.chrome.last_connect_failure_name.is_some()
&& self.shell.chrome.last_connect_failure_error.is_some())
{
return self.failed_workspace_state().into_any_element();
}
@@ -72,10 +72,10 @@ impl NyaTermApp {
.into_any_element();
}
}
let root = self
.workspace_split
.clone()
.unwrap_or_else(|| WorkspacePaneNode::leaf(self.session.active_id.clone().unwrap()));
let root =
self.shell.workspace.split.clone().unwrap_or_else(|| {
WorkspacePaneNode::leaf(self.session.active_id.clone().unwrap())
});
let show_chrome = root.is_split();
div()
@@ -77,7 +77,9 @@ impl NyaTermApp {
.unwrap_or(("Session", "icons/conn/terminal.svg"));
let custom_color = self.session.tab_colors.get(tab_id).copied();
let leaf_ids = self
.session_pane_roots
.shell
.workspace
.pane_roots
.get(tab_id)
.map(|root| root.session_ids())
.unwrap_or_else(|| vec![tab_id.clone()]);
@@ -327,7 +329,8 @@ impl NyaTermApp {
.on_click(cx.listener({
let leaf_id = id.clone();
move |this, _, window, cx| {
this.focused_terminal_window_leaf_id = Some(leaf_id.clone());
this.shell.workspace.focused_terminal_leaf_id =
Some(leaf_id.clone());
this.start_local_session(window, cx);
}
})),
@@ -396,7 +399,8 @@ impl NyaTermApp {
cx.listener({
let leaf_id = id.clone();
move |this, _, _, cx| {
this.focused_terminal_window_leaf_id = Some(leaf_id.clone());
this.shell.workspace.focused_terminal_leaf_id =
Some(leaf_id.clone());
cx.notify();
}
}),
@@ -29,7 +29,7 @@ impl NyaTermApp {
} else {
self.tr("tabCtx.disconnect").to_string()
};
let (viewport_w, viewport_h) = self.last_viewport_size;
let (viewport_w, viewport_h) = self.shell.viewport.size;
let (menu_x, menu_y) = active_session_clamped_menu_position(
f32::from(menu.x) - 132.,
f32::from(menu.y) + 8.,
@@ -13,8 +13,8 @@ impl NyaTermApp {
cx: &mut Context<Self>,
) -> impl IntoElement {
let palette = self.theme_palette();
let narrow = self.last_viewport_size.0 < 520.;
let width = (self.last_viewport_size.0 - 32.).clamp(280., 480.);
let narrow = self.shell.viewport.size.0 < 520.;
let width = (self.shell.viewport.size.0 - 32.).clamp(280., 480.);
let docs_url = if self
.settings
.language
@@ -14,7 +14,7 @@ impl NyaTermApp {
cx: &mut Context<Self>,
) -> impl IntoElement {
let palette = self.theme_palette();
let (viewport_w, viewport_h) = self.last_viewport_size;
let (viewport_w, viewport_h) = self.shell.viewport.size;
let draft = self
.terminal
.paste
@@ -14,7 +14,7 @@ impl NyaTermApp {
let category_id = menu.category_id.clone();
let rename_id = category_id.clone();
let delete_id = category_id.clone();
let (viewport_w, viewport_h) = self.last_viewport_size;
let (viewport_w, viewport_h) = self.shell.viewport.size;
let (menu_x, menu_y) = quick_command_category_clamped_menu_position(
f32::from(menu.x),
f32::from(menu.y),
@@ -38,7 +38,7 @@ impl NyaTermApp {
.child(
div()
.id(SharedString::from("quick-command-category-delete-dialog"))
.w(px((self.last_viewport_size.0 - 32.).clamp(280., 384.)))
.w(px((self.shell.viewport.size.0 - 32.).clamp(280., 384.)))
.rounded_md()
.border_1()
.border_color(rgb(0x7f1d1d))
@@ -136,7 +136,7 @@ impl NyaTermApp {
.child(
div()
.id(SharedString::from("quick-command-category-rename-dialog"))
.w(px((self.last_viewport_size.0 - 32.).clamp(280., 384.)))
.w(px((self.shell.viewport.size.0 - 32.).clamp(280., 384.)))
.rounded_md()
.border_1()
.border_color(rgb(palette.border))
@@ -35,7 +35,7 @@ impl NyaTermApp {
.child(
div()
.id(SharedString::from("quick-command-delete-dialog"))
.w(px((self.last_viewport_size.0 - 32.).clamp(280., 384.)))
.w(px((self.shell.viewport.size.0 - 32.).clamp(280., 384.)))
.rounded_md()
.border_1()
.border_color(rgb(0x7f1d1d))
@@ -30,7 +30,7 @@ impl NyaTermApp {
.filter(|description| !description.is_empty());
let command_text = command.command.clone();
let estimated_h = if description.is_some() { 224. } else { 182. };
let (viewport_w, viewport_h) = self.last_viewport_size;
let (viewport_w, viewport_h) = self.shell.viewport.size;
let (popover_x, popover_y) = quick_command_details_popover_position(
f32::from(anchor_x) - 304.,
f32::from(anchor_y) - estimated_h - 6.,
@@ -17,7 +17,7 @@ impl NyaTermApp {
&mut self,
cx: &mut Context<Self>,
) -> AnyElement {
self.quick_command_editor_surface(self.last_viewport_size.0, false, cx)
self.quick_command_editor_surface(self.shell.viewport.size.0, false, cx)
}
pub(in crate::features) fn quick_command_editor_window_view(
@@ -48,7 +48,7 @@ impl NyaTermApp {
.child(
div()
.id(SharedString::from("quick-command-import-dialog"))
.w(px((self.last_viewport_size.0 - 32.).clamp(280., 380.)))
.w(px((self.shell.viewport.size.0 - 32.).clamp(280., 380.)))
.max_w_full()
.rounded_md()
.border_1()
@@ -16,7 +16,7 @@ impl NyaTermApp {
let all_command_id = command_id.clone();
let delete_command_id = command_id.clone();
let can_send_to_all = self.live_session_count() > 1;
let (viewport_w, viewport_h) = self.last_viewport_size;
let (viewport_w, viewport_h) = self.shell.viewport.size;
let (menu_x, menu_y) = quick_command_clamped_menu_position(
f32::from(menu.x),
f32::from(menu.y),
@@ -147,7 +147,7 @@ impl NyaTermApp {
.child(
div()
.id(SharedString::from("quick-command-variable-dialog"))
.w(px((self.last_viewport_size.0 - 32.).clamp(280., 400.)))
.w(px((self.shell.viewport.size.0 - 32.).clamp(280., 400.)))
.max_w_full()
.rounded_md()
.border_1()
@@ -173,7 +173,7 @@ impl NyaTermApp {
div()
.id("quick-command-variable-body")
.p_5()
.max_h(px((self.last_viewport_size.1 * 0.6).clamp(180., 420.)))
.max_h(px((self.shell.viewport.size.1 * 0.6).clamp(180., 420.)))
.overflow_y_scroll()
.child(rows)
.child(
@@ -63,11 +63,11 @@ impl NyaTermApp {
let category_sidebar = self.quick_command_category_sidebar(categories, palette, cx);
let view_mode = self.quick_command_state.list.view_mode;
let tile_columns = quick_command_tile_column_count(
self.last_viewport_size.0,
self.left_panel_width,
self.right_panel_width,
!self.left_sidebar_collapsed,
!self.right_inspector_collapsed,
self.shell.viewport.size.0,
self.shell.panels.left_width,
self.shell.panels.right_width,
!self.shell.panels.left_collapsed,
!self.shell.panels.right_collapsed,
);
let logical_row_height = match view_mode {
QuickCommandViewMode::Tile => 32.,
@@ -14,7 +14,7 @@ impl NyaTermApp {
cx: &mut Context<Self>,
) -> impl IntoElement {
let palette = self.theme_palette();
let (viewport_w, viewport_h) = self.last_viewport_size;
let (viewport_w, viewport_h) = self.shell.viewport.size;
let items = self.filtered_quick_switch_items(cx);
self.update_quick_switch_state(cx, |store| {
store.clamp_quick_switch_selected_index(items.len())
@@ -28,7 +28,7 @@ impl NyaTermApp {
cx,
);
let query_focus = query_input.read(cx).focus_handle();
let list_max_height = (self.last_viewport_size.1 * 0.55).clamp(160., 384.);
let list_max_height = (self.shell.viewport.size.1 * 0.55).clamp(160., 384.);
let selected_row_bg = rgba((palette.primary << 8) | 0x26);
let hover_row_bg = self.shell_surface_color(palette.hover);
let mut rows = div()
@@ -54,7 +54,7 @@ impl NyaTermApp {
.child(
div()
.id("close-all-sessions-confirm-dialog")
.w(px((self.last_viewport_size.0 - 32.).clamp(280., 400.)))
.w(px((self.shell.viewport.size.0 - 32.).clamp(280., 400.)))
.max_w_full()
.mx_4()
.rounded_md()
@@ -23,7 +23,7 @@ impl NyaTermApp {
)
.into_any_element();
let can_save = !self.session.dialogs.rename_draft.trim().is_empty();
let dialog_width = (self.last_viewport_size.0 - 32.).clamp(280., 320.);
let dialog_width = (self.shell.viewport.size.0 - 32.).clamp(280., 320.);
div()
.id(SharedString::from("rename-tab-overlay"))
@@ -350,7 +350,7 @@ impl NyaTermApp {
.into_any_element();
let can_submit = !self.session.dialogs.startup_command_draft.trim().is_empty();
let delay_label = format!("{} ms", self.session.dialogs.startup_command_delay_ms);
let dialog_width = (self.last_viewport_size.0 - 32.).clamp(280., 448.);
let dialog_width = (self.shell.viewport.size.0 - 32.).clamp(280., 448.);
div()
.id(SharedString::from("startup-command-overlay"))
@@ -17,7 +17,7 @@ impl NyaTermApp {
cx: &mut Context<Self>,
) -> impl IntoElement {
let palette = self.theme_palette();
let (viewport_w, viewport_h) = self.last_viewport_size;
let (viewport_w, viewport_h) = self.shell.viewport.size;
let dialog_width = (viewport_w - 24.).clamp(280., 900.);
let dialog_height = (viewport_h - 56.).clamp(280., 500.);
let groups_width = (dialog_width * 0.23).clamp(160., 208.);
@@ -33,7 +33,7 @@ impl NyaTermApp {
_session_count: usize,
cx: &mut Context<Self>,
) -> gpui::AnyElement {
let (viewport_w, viewport_h) = self.last_viewport_size;
let (viewport_w, viewport_h) = self.shell.viewport.size;
let menu_max_height = (viewport_h - 16.).clamp(160., 440.);
let (menu_x, menu_y) = if let Some((x, y)) = self.session.dialogs.tab_actions_anchor {
clamp_tab_actions_position(x, y, 240., menu_max_height, viewport_w, viewport_h)
@@ -69,9 +69,9 @@ impl NyaTermApp {
.active_id
.as_deref()
.map(|id| self.tab_root_for_session(id))
.and_then(|root| self.session_pane_roots.get(&root))
.and_then(|root| self.shell.workspace.pane_roots.get(&root))
.is_some_and(|root| root.is_split())
|| self.workspace_split.is_some();
|| self.shell.workspace.split.is_some();
let scroll_offset = self
.terminal
.view
@@ -55,7 +55,7 @@ impl NyaTermApp {
.child(
div()
.id(SharedString::from("temporary-ssh-link-dialog"))
.w(px((self.last_viewport_size.0 - 32.).clamp(280., 480.)))
.w(px((self.shell.viewport.size.0 - 32.).clamp(280., 480.)))
.max_w_full()
.mx_4()
.rounded_md()
@@ -14,7 +14,7 @@ impl NyaTermApp {
cx: &mut Context<Self>,
) -> impl IntoElement {
let palette = self.theme_palette();
let (viewport_w, viewport_h) = self.last_viewport_size;
let (viewport_w, viewport_h) = self.shell.viewport.size;
let visible_text = self.active_terminal_visible_text();
let buffer_tail = self.active_terminal_buffer_tail();
let visible_lines = visible_text.lines().count();
@@ -347,7 +347,7 @@ impl NyaTermApp {
self.tr("terminalActions.sessionLog"),
cx.listener(|this, _, _, cx| {
this.terminal.menus.actions_open = false;
this.right_focus = RightFocus::Recording;
this.shell.panels.right_focus = RightFocus::Recording;
cx.notify();
}),
))
@@ -21,8 +21,8 @@ impl NyaTermApp {
let available =
!checking && !failed && update_info.as_ref().is_some_and(|info| info.available);
let portable = self.runtime.mode() == RuntimeMode::Portable;
let (_, viewport_h) = self.last_viewport_size;
let dialog_width = (self.last_viewport_size.0 - 32.).clamp(320., 560.);
let (_, viewport_h) = self.shell.viewport.size;
let dialog_width = (self.shell.viewport.size.0 - 32.).clamp(320., 560.);
let release_url = update_info
.as_ref()
.and_then(|info| info.html_url.clone())
@@ -233,7 +233,7 @@ impl NyaTermApp {
if !command.ends_with('\n') {
command.push('\n');
}
self.selected_nav = NavItem::Workspace;
self.shell.navigation.selected_nav = NavItem::Workspace;
if self.send_terminal_input(command.into_bytes(), cx) {
self.remote_ops.docker.status = status;
self.terminal.view.status = self.remote_ops.docker.status.clone();
+39 -35
View File
@@ -82,23 +82,23 @@ impl NyaTermApp {
.on_mouse_down(
MouseButton::Left,
cx.listener(|this, _, _, cx| {
let changed = this.title_menu_open.is_some()
|| this.header_status.menu_open
|| this.open_tabs_menu_open
|| this.new_session_menu_open
|| this.new_session_all_sessions_open
|| !this.new_session_group_menu_path.is_empty()
let changed = this.shell.chrome.title_menu_open.is_some()
|| this.shell.chrome.header_status.menu_open
|| this.shell.chrome.open_tabs_menu_open
|| this.shell.chrome.new_session_menu_open
|| this.shell.chrome.new_session_all_sessions_open
|| !this.shell.chrome.new_session_group_menu_path.is_empty()
|| this.remote_ops.docker.tab_menu_open
|| this.remote_ops.docker.header_menu_open
|| this.connection_state.list_more_menu_is_open();
if changed {
this.title_menu_open = None;
this.title_menu_submenu = None;
this.header_status.menu_open = false;
this.open_tabs_menu_open = false;
this.new_session_menu_open = false;
this.new_session_all_sessions_open = false;
this.new_session_group_menu_path.clear();
this.shell.chrome.title_menu_open = None;
this.shell.chrome.title_menu_submenu = None;
this.shell.chrome.header_status.menu_open = false;
this.shell.chrome.open_tabs_menu_open = false;
this.shell.chrome.new_session_menu_open = false;
this.shell.chrome.new_session_all_sessions_open = false;
this.shell.chrome.new_session_group_menu_path.clear();
this.remote_ops.docker.tab_menu_open = false;
this.remote_ops.docker.header_menu_open = false;
this.connection_state.close_list_more_menu();
@@ -205,7 +205,7 @@ impl NyaTermApp {
.into();
if let Some((tile_width, tile_height)) = wallpaper_tile_size {
let (columns, rows) =
wallpaper_tile_grid(self.last_viewport_size, (tile_width, tile_height));
wallpaper_tile_grid(self.shell.viewport.size, (tile_width, tile_height));
let mut layer = div()
.absolute()
.inset_0()
@@ -251,11 +251,12 @@ impl NyaTermApp {
}
fn wallpaper_tile_size(&mut self, path: &str) -> (f32, f32) {
if let Some((cached_path, width, height)) = self.wallpaper_tile_dimensions.as_ref()
if let Some((cached_path, width, height)) =
self.shell.viewport.wallpaper_tile_dimensions.as_ref()
&& cached_path == path
{
return fit_wallpaper_tile_size(
self.last_viewport_size,
self.shell.viewport.size,
(*width as f32, *height as f32),
);
}
@@ -263,8 +264,8 @@ impl NyaTermApp {
.ok()
.filter(|(width, height)| *width > 0 && *height > 0)
.unwrap_or((256, 256));
self.wallpaper_tile_dimensions = Some((path.to_string(), width, height));
fit_wallpaper_tile_size(self.last_viewport_size, (width as f32, height as f32))
self.shell.viewport.wallpaper_tile_dimensions = Some((path.to_string(), width, height));
fit_wallpaper_tile_size(self.shell.viewport.size, (width as f32, height as f32))
}
fn workspace_surface(
@@ -273,7 +274,9 @@ impl NyaTermApp {
window: &mut Window,
cx: &mut Context<Self>,
) -> AnyElement {
if self.main_mode == MainMode::Page && self.selected_nav == NavItem::Settings {
if self.shell.navigation.main_mode == MainMode::Page
&& self.shell.navigation.selected_nav == NavItem::Settings
{
div()
.flex()
.flex_1()
@@ -285,15 +288,15 @@ impl NyaTermApp {
let compact_layout = !cfg!(target_os = "macos");
let has_left_activity_items = self.activity_side_has_items(ActivitySide::Left);
let has_right_activity_items = self.activity_side_has_items(ActivitySide::Right);
let left_overlay_mode = compact_layout && self.last_viewport_size.0 < 1024.;
let right_overlay_mode = compact_layout && self.last_viewport_size.0 < 768.;
let left_overlay_mode = compact_layout && self.shell.viewport.size.0 < 1024.;
let right_overlay_mode = compact_layout && self.shell.viewport.size.0 < 768.;
let left_drawer_open = has_left_activity_items
&& left_overlay_mode
&& self.mobile_left_open
&& self.shell.panels.mobile_left_open
&& self.left_side_open();
let right_drawer_open = has_right_activity_items
&& right_overlay_mode
&& self.mobile_right_open
&& self.shell.panels.mobile_right_open
&& self.right_side_open();
let mut surface = div()
.flex()
@@ -324,7 +327,7 @@ impl NyaTermApp {
this.child(self.activity_bar(ActivitySide::Right, cx))
});
if self.terminal_windows_is_multi_leaf() && self.new_session_menu_open {
if self.terminal_windows_is_multi_leaf() && self.shell.chrome.new_session_menu_open {
surface = surface.child(self.render_new_session_menu(cx));
}
@@ -336,8 +339,8 @@ impl NyaTermApp {
.inset_0()
.bg(rgba(0x00000080))
.on_click(cx.listener(|this, _, _, cx| {
this.mobile_left_open = false;
this.mobile_right_open = false;
this.shell.panels.mobile_left_open = false;
this.shell.panels.mobile_right_open = false;
cx.notify();
})),
);
@@ -439,9 +442,9 @@ impl NyaTermApp {
)
.on_click(cx.listener(move |this, _, _, cx| {
if left {
this.mobile_left_open = false;
this.shell.panels.mobile_left_open = false;
} else {
this.mobile_right_open = false;
this.shell.panels.mobile_right_open = false;
}
cx.notify();
})),
@@ -633,9 +636,10 @@ impl NyaTermApp {
.when(quick_switch_open, |this| {
this.child(self.quick_switch_overlay(cx))
})
.when(self.activity_bar_context_menu.is_some(), |this| {
this.child(self.activity_bar_context_menu_overlay(cx))
})
.when(
self.shell.chrome.activity_bar_context_menu.is_some(),
|this| this.child(self.activity_bar_context_menu_overlay(cx)),
)
.when(overlay.locked, |this| {
this.child(self.lock_screen_overlay(window, cx))
})
@@ -657,8 +661,8 @@ impl NyaTermApp {
}
fn modal_child_window_open(&self) -> bool {
self.settings_window.is_some()
|| self.settings_window_open_pending
self.shell.navigation.settings.window.is_some()
|| self.shell.navigation.settings.window_open_pending
|| self.quick_command_state.editor.window.is_some()
|| self.quick_command_state.editor.window_open_pending
|| self.connection_state.editor_modal_window_open_or_pending()
@@ -667,9 +671,9 @@ impl NyaTermApp {
}
fn activate_modal_child_window(&mut self, cx: &mut Context<Self>) -> bool {
if self.settings_window.is_some() {
if self.shell.navigation.settings.window.is_some() {
self.activate_settings_window(cx)
} else if self.settings_window_open_pending {
} else if self.shell.navigation.settings.window_open_pending {
true
} else if self.quick_command_state.editor.window.is_some() {
self.activate_quick_command_window(cx)
@@ -214,9 +214,8 @@ impl NyaTermApp {
self.clear_terminal_mouse_report_for_session(session_id);
self.session.start.reconnect_failures.remove(session_id);
self.session.order.retain(|id| id != session_id);
self.session_tab_owner.remove(session_id);
// If this leaf was a tab root, drop its pane tree (prune will rekey survivors).
self.session_pane_roots.remove(session_id);
self.shell.workspace.remove_session(session_id);
let multiplex_key = self
.session
.metadata
@@ -105,8 +105,8 @@ impl NyaTermApp {
);
}
}
self.selected_nav = NavItem::Workspace;
self.main_mode = MainMode::Workspace;
self.shell.navigation.selected_nav = NavItem::Workspace;
self.shell.navigation.main_mode = MainMode::Workspace;
cx.notify();
}
@@ -169,8 +169,8 @@ impl NyaTermApp {
existing_multiplex,
cx,
);
self.selected_nav = NavItem::Workspace;
self.main_mode = MainMode::Workspace;
self.shell.navigation.selected_nav = NavItem::Workspace;
self.shell.navigation.main_mode = MainMode::Workspace;
cx.notify();
}
@@ -420,8 +420,8 @@ impl NyaTermApp {
self.session
.busy_actions
.retain(|id, _| self.session.metadata.contains_key(id));
self.selected_nav = NavItem::Workspace;
self.main_mode = MainMode::Workspace;
self.shell.navigation.selected_nav = NavItem::Workspace;
self.shell.navigation.main_mode = MainMode::Workspace;
cx.notify();
}
@@ -463,17 +463,7 @@ impl NyaTermApp {
.insert(new_id.to_string(), history);
}
let mut pane_roots = std::mem::take(&mut self.session_pane_roots);
for root in pane_roots.values_mut() {
root.replace_session_id(old_id, new_id);
}
if let Some(root) = pane_roots.remove(old_id) {
pane_roots.insert(new_id.to_string(), root);
}
self.session_pane_roots = pane_roots;
if let Some(root) = self.workspace_split.as_mut() {
root.replace_session_id(old_id, new_id);
}
self.shell.workspace.replace_session_id(old_id, new_id);
if let Some(root) = self.terminal.windows.tree.as_mut() {
root.replace_tab_id(old_id, new_id);
}
@@ -481,7 +471,6 @@ impl NyaTermApp {
if self.session.active_id.as_deref() == Some(old_id) {
self.activate_session_id(new_id);
}
self.rebuild_session_tab_owners();
self.sync_workspace_split_from_active_tab();
}
}
@@ -128,7 +128,9 @@ impl NyaTermApp {
/// True when this session is a secondary leaf inside another tab's pane tree
/// (Tauri: multiple SessionPanes under one Tab, only one strip entry).
pub(in crate::features) fn is_secondary_pane_session(&self, session_id: &str) -> bool {
self.session_tab_owner
self.shell
.workspace
.tab_owner
.get(session_id)
.is_some_and(|owner| owner != session_id)
}
@@ -138,7 +140,7 @@ impl NyaTermApp {
let mut current = session_id.to_string();
// Flatten owner chains defensively.
for _ in 0..8 {
match self.session_tab_owner.get(&current) {
match self.shell.workspace.tab_owner.get(&current) {
Some(owner) if owner != &current => current = owner.clone(),
_ => break,
}
@@ -176,7 +178,7 @@ impl NyaTermApp {
return active.to_string();
}
}
if let Some(root) = self.session_pane_roots.get(tab_root) {
if let Some(root) = self.shell.workspace.pane_roots.get(tab_root) {
if let Some(first) = root.session_ids().into_iter().next() {
return first;
}
@@ -50,10 +50,10 @@ impl NyaTermApp {
if !self.session.start.select_pending(&request_id) {
return;
}
self.open_tabs_menu_open = false;
self.new_session_menu_open = false;
self.selected_nav = NavItem::Workspace;
self.main_mode = MainMode::Workspace;
self.shell.chrome.open_tabs_menu_open = false;
self.shell.chrome.new_session_menu_open = false;
self.shell.navigation.selected_nav = NavItem::Workspace;
self.shell.navigation.main_mode = MainMode::Workspace;
cx.notify();
}
@@ -80,10 +80,10 @@ impl NyaTermApp {
if !self.session.start.select_failed(&request_id) {
return;
}
self.open_tabs_menu_open = false;
self.new_session_menu_open = false;
self.selected_nav = NavItem::Workspace;
self.main_mode = MainMode::Workspace;
self.shell.chrome.open_tabs_menu_open = false;
self.shell.chrome.new_session_menu_open = false;
self.shell.navigation.selected_nav = NavItem::Workspace;
self.shell.navigation.main_mode = MainMode::Workspace;
cx.notify();
}
@@ -96,8 +96,8 @@ impl NyaTermApp {
return;
};
if !self.session.start.has_failed() {
self.last_connect_failure_name = None;
self.last_connect_failure_error = None;
self.shell.chrome.last_connect_failure_name = None;
self.shell.chrome.last_connect_failure_error = None;
}
self.terminal.view.status = format!(
"closed failed connection {}",
@@ -136,8 +136,8 @@ impl NyaTermApp {
} = registration;
if reconnect_session_id.is_none() {
self.last_connect_failure_name = None;
self.last_connect_failure_error = None;
self.shell.chrome.last_connect_failure_name = None;
self.shell.chrome.last_connect_failure_error = None;
}
self.session.start.panes.insert(
request_id.clone(),
@@ -174,8 +174,8 @@ impl NyaTermApp {
// Status + connecting tab already show progress; avoid full terminal decode
// work on the click path before the worker even starts.
let _ = append_start_log;
self.selected_nav = NavItem::Workspace;
self.main_mode = MainMode::Workspace;
self.shell.navigation.selected_nav = NavItem::Workspace;
self.shell.navigation.main_mode = MainMode::Workspace;
cx.notify();
request_id
}
@@ -469,8 +469,8 @@ impl NyaTermApp {
match event.result {
Ok(success) => {
let ui_register_started_at = Instant::now();
self.last_connect_failure_name = None;
self.last_connect_failure_error = None;
self.shell.chrome.last_connect_failure_name = None;
self.shell.chrome.last_connect_failure_error = None;
let session_info = success.session_info;
let session_id = session_info.id.clone();
let reconnect_session_id = pending
@@ -640,7 +640,7 @@ impl NyaTermApp {
self.schedule_startup_command(session_id.clone(), startup_command, cx);
}
self.apply_pending_workspace_split_for_duplicate(&session_id);
self.selected_nav = NavItem::Workspace;
self.shell.navigation.selected_nav = NavItem::Workspace;
let ui_register_duration = ui_register_started_at.elapsed();
let request_to_ui_duration = requested_at
.map(|requested_at| requested_at.elapsed())
@@ -685,8 +685,8 @@ impl NyaTermApp {
let _ = self.session.start.reconnect_replace_id.take();
}
if !reconnect_failure {
self.last_connect_failure_name = Some(connection_name.clone());
self.last_connect_failure_error = Some(error.clone());
self.shell.chrome.last_connect_failure_name = Some(connection_name.clone());
self.shell.chrome.last_connect_failure_error = Some(error.clone());
}
if let Some(session_id) = reconnect_session_id {
if self.session.metadata.contains_key(&session_id) {
@@ -725,7 +725,7 @@ impl NyaTermApp {
connection_name
));
}
self.selected_nav = NavItem::Workspace;
self.shell.navigation.selected_nav = NavItem::Workspace;
let request_to_ui_duration = requested_at
.map(|requested_at| requested_at.elapsed())
.unwrap_or(worker_duration + worker_to_ui_duration);
@@ -80,8 +80,8 @@ impl NyaTermApp {
if self.saved_connection_start_is_pending_or_queued(&connection) {
self.terminal.view.status =
format!("{} is already connecting or queued", connection.name);
self.selected_nav = NavItem::Workspace;
self.main_mode = MainMode::Workspace;
self.shell.navigation.selected_nav = NavItem::Workspace;
self.shell.navigation.main_mode = MainMode::Workspace;
cx.notify();
return;
}
@@ -244,10 +244,7 @@ impl NyaTermApp {
pub(in crate::features) fn activate_session_id(&mut self, session_id: &str) -> Option<String> {
self.session.start.active_pending = None;
self.session.start.active_failed = None;
self.open_tabs_menu_open = false;
self.new_session_menu_open = false;
self.new_session_all_sessions_open = false;
self.new_session_group_menu_path.clear();
self.shell.chrome.prepare_session_switch();
// Session switch resets terminal-output credential autofill (Tauri XTerminal remount).
self.terminal.assist.reset_for_session_switch();
let previous_session_id = self.session.active_id.clone();
@@ -265,7 +262,6 @@ impl NyaTermApp {
self.transfer.queue.job_delete = None;
self.reset_remote_runtime_for_session_switch();
}
self.session_tab_scroll_into_view_pending = true;
// Keep workspace_split mirrored to the active tab's per-tab pane root.
self.sync_workspace_split_from_active_tab();
self.transfer.browser.auto_sync_cwd_last_at = None;
@@ -279,9 +275,9 @@ impl NyaTermApp {
// Transfer browser state is only needed when the transfers panel is open
// or we already have cached browser state for this session. Skipping the
// full reset on every activate keeps connect/switch chrome responsive.
let transfers_panel_visible = self.active_left_panel == Some(NavItem::Transfers)
|| self.active_right_panel == Some(NavItem::Transfers)
|| self.selected_nav == NavItem::Transfers;
let transfers_panel_visible = self.shell.panels.active_left == Some(NavItem::Transfers)
|| self.shell.panels.active_right == Some(NavItem::Transfers)
|| self.shell.navigation.selected_nav == NavItem::Transfers;
if transfers_panel_visible
|| self.transfer.browser.session_cache.contains_key(session_id)
|| !self.transfer.browser.entries.is_empty()
@@ -435,7 +431,7 @@ impl NyaTermApp {
} else {
format!("active {}", short_id(&focus_id))
};
self.selected_nav = NavItem::Workspace;
self.shell.navigation.selected_nav = NavItem::Workspace;
cx.notify();
}
@@ -464,8 +460,8 @@ impl NyaTermApp {
let next_index = (active_index as isize + offset).rem_euclid(len) as usize;
let session_id = sessions[next_index].id.clone();
self.activate_session_id_with_surface_sync(&session_id, cx);
self.selected_nav = NavItem::Workspace;
self.main_mode = MainMode::Workspace;
self.shell.navigation.selected_nav = NavItem::Workspace;
self.shell.navigation.main_mode = MainMode::Workspace;
self.terminal.view.status = format!("active {}", short_id(&session_id));
cx.notify();
}
@@ -484,49 +480,30 @@ impl NyaTermApp {
let index = index.min(sessions.len().saturating_sub(1));
let session_id = sessions[index].id.clone();
self.activate_session_id_with_surface_sync(&session_id, cx);
self.selected_nav = NavItem::Workspace;
self.main_mode = MainMode::Workspace;
self.shell.navigation.selected_nav = NavItem::Workspace;
self.shell.navigation.main_mode = MainMode::Workspace;
self.terminal.view.status = format!("active {}", short_id(&session_id));
cx.notify();
}
pub(in crate::features) fn toggle_open_tabs_menu(&mut self, cx: &mut Context<Self>) {
self.open_tabs_menu_open = !self.open_tabs_menu_open;
if self.open_tabs_menu_open {
self.new_session_menu_open = false;
self.new_session_all_sessions_open = false;
self.new_session_group_menu_path.clear();
self.title_menu_open = None;
}
self.shell.chrome.toggle_open_tabs_menu();
cx.notify();
}
pub(in crate::features) fn close_open_tabs_menu(&mut self, cx: &mut Context<Self>) {
if self.open_tabs_menu_open {
self.open_tabs_menu_open = false;
if self.shell.chrome.close_open_tabs_menu() {
cx.notify();
}
}
pub(in crate::features) fn toggle_new_session_menu(&mut self, cx: &mut Context<Self>) {
self.new_session_menu_open = !self.new_session_menu_open;
if self.new_session_menu_open {
self.open_tabs_menu_open = false;
self.title_menu_open = None;
}
self.new_session_all_sessions_open = false;
self.new_session_group_menu_path.clear();
self.shell.chrome.toggle_new_session_menu();
cx.notify();
}
pub(in crate::features) fn close_new_session_menu(&mut self, cx: &mut Context<Self>) {
let changed = self.new_session_menu_open
|| self.new_session_all_sessions_open
|| !self.new_session_group_menu_path.is_empty();
self.new_session_menu_open = false;
self.new_session_all_sessions_open = false;
self.new_session_group_menu_path.clear();
if changed {
if self.shell.chrome.close_new_session_menu() {
cx.notify();
}
}
@@ -198,7 +198,7 @@ impl NyaTermApp {
.into_iter()
.map(|session| {
let mut tab = self.serialize_open_tab_for_session(&session);
if let Some(root) = self.session_pane_roots.get(&session.id) {
if let Some(root) = self.shell.workspace.pane_roots.get(&session.id) {
if root.is_split() {
if let Some(pane_root) = self.workspace_pane_to_restorable_pane(root) {
tab.root = Some(pane_root);
@@ -241,14 +241,16 @@ impl NyaTermApp {
/// entry whose `root` is a Tauri RestorablePaneNode tree (for interop).
fn serialize_open_tabs_as_single_pane_tab(&self) -> Option<Vec<RestorableOpenTab>> {
// Only collapse to one open_tabs entry when exactly one split tree covers every session.
if self.session_pane_roots.len() > 1 {
if self.shell.workspace.pane_roots.len() > 1 {
return None;
}
let root = self
.session_pane_roots
.shell
.workspace
.pane_roots
.values()
.find(|root| root.is_split())
.or(self.workspace_split.as_ref())?;
.or(self.shell.workspace.split.as_ref())?;
if !root.is_split() {
return None;
}
@@ -464,7 +466,7 @@ impl NyaTermApp {
self.mark_startup_restore_complete(cx);
// After all tabs reconnect, attempt multi-leaf then global pane layout restore.
self.terminal.windows.restored = false;
self.workspace_pane_layout_restored = false;
self.shell.workspace.pane_layout_restored = false;
self.try_restore_terminal_window_layout();
// Prefer stored ui.workspace_pane_layout only when no open_tabs per-tab roots exist.
// open_tabs[].root maps to per-tab session_pane_roots (Tauri Tab.root).
@@ -479,7 +481,7 @@ impl NyaTermApp {
if pending_layouts.is_empty() {
self.try_restore_workspace_pane_layout();
} else {
self.workspace_pane_layout_restored = true;
self.shell.workspace.pane_layout_restored = true;
for layout in pending_layouts {
self.apply_restorable_workspace_pane_layout(layout);
}
@@ -498,9 +500,11 @@ impl NyaTermApp {
}
if self.terminal_windows_is_multi_leaf() {
self.terminal.view.status = "restored workspace tabs and window layout".to_string();
} else if !self.session_pane_roots.is_empty()
} else if !self.shell.workspace.pane_roots.is_empty()
|| self
.workspace_split
.shell
.workspace
.split
.as_ref()
.is_some_and(|root| root.is_split())
{
@@ -600,20 +604,23 @@ impl NyaTermApp {
return;
};
// Avoid clobbering an existing distinct per-tab tree for the same root.
if let Some(existing) = self.session_pane_roots.get(&first) {
if let Some(existing) = self.shell.workspace.pane_roots.get(&first) {
if existing != &restored {
// Prefer the newly restored tree from open_tabs for this root.
}
}
self.session_pane_roots.insert(first.clone(), restored);
self.shell
.workspace
.pane_roots
.insert(first.clone(), restored);
self.rebuild_session_tab_owners();
if self.session.active_id.is_none() {
self.session.active_id = Some(first);
}
self.sync_workspace_split_from_active_tab();
self.workspace_pane_layout_restored = true;
self.selected_nav = NavItem::Workspace;
self.main_mode = MainMode::Workspace;
self.shell.workspace.pane_layout_restored = true;
self.shell.navigation.selected_nav = NavItem::Workspace;
self.shell.navigation.main_mode = MainMode::Workspace;
self.terminal.view.status = "restored pane layout from open_tabs root".to_string();
}
}
@@ -76,7 +76,7 @@ impl NyaTermApp {
) {
self.security.unlock.master_required_prompt_open = false;
self.security.unlock.pending_action = None;
self.settings_active_tab = SettingsTab::Security;
self.shell.navigation.settings.active_tab = SettingsTab::Security;
self.open_page(NavItem::Settings, cx);
}
@@ -8,10 +8,10 @@ use crate::models::{CloudSyncSecretDraft, MainMode, NavItem, TranslationSecretDr
impl NyaTermApp {
pub(in crate::features) fn begin_settings_draft(&mut self) {
if self.settings_draft_snapshot.is_some() {
if self.shell.navigation.settings.draft_snapshot.is_some() {
return;
}
self.settings_draft_snapshot = Some(SettingsDraftSnapshot {
self.shell.navigation.settings.draft_snapshot = Some(SettingsDraftSnapshot {
settings: self.settings.clone(),
ai_settings: self.ai.settings.config.clone(),
ai_model_draft: self.ai.settings.model_draft.clone(),
@@ -28,7 +28,7 @@ impl NyaTermApp {
}
pub(in crate::features) fn settings_draft_dirty(&self) -> bool {
let Some(snapshot) = self.settings_draft_snapshot.as_ref() else {
let Some(snapshot) = self.shell.navigation.settings.draft_snapshot.as_ref() else {
return false;
};
snapshot.settings != self.settings
@@ -50,7 +50,7 @@ impl NyaTermApp {
&mut self,
cx: &mut Context<Self>,
) -> bool {
if self.settings_draft_snapshot.is_none() {
if self.shell.navigation.settings.draft_snapshot.is_none() {
return false;
}
self.store_status.message = "settings draft changed".to_string();
@@ -241,10 +241,10 @@ impl NyaTermApp {
}
pub(in crate::features) fn rebase_open_settings_draft(&mut self) {
if self.settings_draft_snapshot.is_none() {
if self.shell.navigation.settings.draft_snapshot.is_none() {
return;
}
self.settings_draft_snapshot = None;
self.shell.navigation.settings.draft_snapshot = None;
self.settings_master_password_enabled = self.settings.has_master_password;
self.settings_master_password_draft.clear();
self.begin_settings_draft();
@@ -255,7 +255,7 @@ impl NyaTermApp {
close_after_apply: bool,
cx: &mut Context<Self>,
) {
if self.settings_draft_snapshot.is_none() {
if self.shell.navigation.settings.draft_snapshot.is_none() {
if close_after_apply {
self.finish_settings_page(cx);
}
@@ -357,7 +357,7 @@ impl NyaTermApp {
store.save_workspace_pane_layout(None)
});
}
self.settings_draft_snapshot = None;
self.shell.navigation.settings.draft_snapshot = None;
self.store_status.message = "settings applied".to_string();
self.store_status.ready = true;
self.terminal.view.status = "settings applied".to_string();
@@ -378,7 +378,7 @@ impl NyaTermApp {
}
pub(in crate::features) fn cancel_settings(&mut self, cx: &mut Context<Self>) {
if let Some(snapshot) = self.settings_draft_snapshot.take() {
if let Some(snapshot) = self.shell.navigation.settings.draft_snapshot.take() {
self.apply_gpui_settings(snapshot.settings);
self.ai.settings.config = snapshot.ai_settings;
self.ai.settings.model_draft = snapshot.ai_model_draft;
@@ -410,7 +410,7 @@ impl NyaTermApp {
if self.settings_draft_dirty() {
self.apply_settings_draft(true, cx);
} else {
self.settings_draft_snapshot = None;
self.shell.navigation.settings.draft_snapshot = None;
self.finish_settings_page(cx);
}
}
@@ -454,22 +454,30 @@ impl NyaTermApp {
self.forget_text_inputs("ai.settings.action.");
self.forget_text_inputs("ai.settings.manual-model.");
self.forget_text_inputs("keyword.highlight.");
self.settings_window = None;
self.settings_window_open_pending = false;
if self.main_mode == MainMode::Page && self.selected_nav == NavItem::Settings {
self.main_mode = MainMode::Workspace;
self.left_sidebar_collapsed = self
.settings_previous_left_collapsed
self.shell.navigation.settings.window = None;
self.shell.navigation.settings.window_open_pending = false;
if self.shell.navigation.main_mode == MainMode::Page
&& self.shell.navigation.selected_nav == NavItem::Settings
{
self.shell.navigation.main_mode = MainMode::Workspace;
self.shell.panels.left_collapsed = self
.shell
.navigation
.settings
.previous_left_collapsed
.take()
.unwrap_or_else(|| self.active_left_panel.is_none());
self.right_inspector_collapsed = self
.settings_previous_right_collapsed
.unwrap_or_else(|| self.shell.panels.active_left.is_none());
self.shell.panels.right_collapsed = self
.shell
.navigation
.settings
.previous_right_collapsed
.take()
.unwrap_or_else(|| self.active_right_panel.is_none());
.unwrap_or_else(|| self.shell.panels.active_right.is_none());
self.persist_ui_layout();
} else {
self.settings_previous_left_collapsed = None;
self.settings_previous_right_collapsed = None;
self.shell.navigation.settings.previous_left_collapsed = None;
self.shell.navigation.settings.previous_right_collapsed = None;
}
self.terminal.view.status = "settings closed".to_string();
cx.notify();
@@ -24,7 +24,9 @@ impl NyaTermApp {
) {
self.settings.startup_restore_window_layout = !self.settings.startup_restore_window_layout;
self.save_general_settings(cx);
if !self.settings.startup_restore_window_layout && self.settings_draft_snapshot.is_none() {
if !self.settings.startup_restore_window_layout
&& self.shell.navigation.settings.draft_snapshot.is_none()
{
// Clear stored layouts when the user disables restore.
let _ = ConnectionStore::open_with_portable_key_path(
self.runtime.config_dir(),
@@ -149,7 +151,7 @@ impl NyaTermApp {
self.settings.interaction_command_suggestions_enabled =
!self.settings.interaction_command_suggestions_enabled;
if !self.settings.interaction_command_suggestions_enabled
&& self.settings_draft_snapshot.is_none()
&& self.shell.navigation.settings.draft_snapshot.is_none()
{
self.terminal.assist.clear_command_tracking();
}
@@ -48,7 +48,7 @@ impl NyaTermApp {
) {
let next = (self.settings.terminal_scrollback_lines as i32 + delta).clamp(100, 100_000);
self.settings.terminal_scrollback_lines = next as u32;
if self.settings_draft_snapshot.is_none() {
if self.shell.navigation.settings.draft_snapshot.is_none() {
self.enforce_terminal_scrollback_limit();
}
self.save_terminal_settings(cx);
@@ -24,10 +24,18 @@ impl SettingsWindow {
impl Render for SettingsWindow {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
if self.app.read(cx).settings_draft_snapshot.is_none() {
if self
.app
.read(cx)
.shell
.navigation
.settings
.draft_snapshot
.is_none()
{
self.app.update(cx, |app, cx| {
app.settings_window = None;
app.settings_window_open_pending = false;
app.shell.navigation.settings.window = None;
app.shell.navigation.settings.window_open_pending = false;
cx.notify();
});
window.defer(cx, |window, _| window.remove_window());
@@ -77,7 +85,7 @@ impl Render for SettingsWindow {
impl NyaTermApp {
pub(in crate::features) fn activate_settings_window(&mut self, cx: &mut Context<Self>) -> bool {
let Some(handle) = self.settings_window else {
let Some(handle) = self.shell.navigation.settings.window else {
return false;
};
let app = cx.entity();
@@ -87,8 +95,14 @@ impl NyaTermApp {
.is_err()
{
let _ = app.update(cx, |app, cx| {
if app.settings_window.is_some_and(|current| current == handle) {
app.settings_window = None;
if app
.shell
.navigation
.settings
.window
.is_some_and(|current| current == handle)
{
app.shell.navigation.settings.window = None;
cx.notify();
}
});
@@ -101,15 +115,15 @@ impl NyaTermApp {
if self.activate_settings_window(cx) {
return true;
}
if self.settings_window_open_pending {
if self.shell.navigation.settings.window_open_pending {
return true;
}
self.settings_window_open_pending = true;
self.shell.navigation.settings.window_open_pending = true;
cx.notify();
let app = cx.entity();
cx.defer(move |cx| {
let should_open = app.read(cx).settings_window_open_pending;
let should_open = app.read(cx).shell.navigation.settings.window_open_pending;
if should_open {
open_settings_window_now_from_app(app, cx);
}
@@ -119,17 +133,24 @@ impl NyaTermApp {
}
fn open_settings_window_now_from_app(app: Entity<NyaTermApp>, cx: &mut App) {
if app.read(cx).settings_window.is_some() {
if app.read(cx).shell.navigation.settings.window.is_some() {
let _ = app.update(cx, |app, cx| {
app.settings_window_open_pending = false;
app.shell.navigation.settings.window_open_pending = false;
app.activate_settings_window(cx);
cx.notify();
});
return;
}
if app.read(cx).settings_draft_snapshot.is_none() {
if app
.read(cx)
.shell
.navigation
.settings
.draft_snapshot
.is_none()
{
let _ = app.update(cx, |app, cx| {
app.settings_window_open_pending = false;
app.shell.navigation.settings.window_open_pending = false;
cx.notify();
});
return;
@@ -152,8 +173,8 @@ fn open_settings_window_now_from_app(app: Entity<NyaTermApp>, cx: &mut App) {
window.on_window_should_close(cx, move |_, cx| {
close_app.update(cx, |app, cx| {
app.cancel_settings(cx);
app.settings_window = None;
app.settings_window_open_pending = false;
app.shell.navigation.settings.window = None;
app.shell.navigation.settings.window_open_pending = false;
});
true
});
@@ -163,19 +184,19 @@ fn open_settings_window_now_from_app(app: Entity<NyaTermApp>, cx: &mut App) {
let _ = app.update(cx, |app, cx| match result {
Ok(handle) => {
app.settings_window = Some(handle);
app.settings_window_open_pending = false;
app.settings_previous_left_collapsed = None;
app.settings_previous_right_collapsed = None;
app.shell.navigation.settings.window = Some(handle);
app.shell.navigation.settings.window_open_pending = false;
app.shell.navigation.settings.previous_left_collapsed = None;
app.shell.navigation.settings.previous_right_collapsed = None;
cx.notify();
}
Err(error) => {
app.settings_window = None;
app.settings_window_open_pending = false;
app.main_mode = MainMode::Page;
app.selected_nav = crate::models::NavItem::Settings;
app.left_sidebar_collapsed = true;
app.right_inspector_collapsed = true;
app.shell.navigation.settings.window = None;
app.shell.navigation.settings.window_open_pending = false;
app.shell.navigation.main_mode = MainMode::Page;
app.shell.navigation.selected_nav = crate::models::NavItem::Settings;
app.shell.panels.left_collapsed = true;
app.shell.panels.right_collapsed = true;
app.terminal.view.status = format!("failed to open settings window: {error}");
cx.notify();
}
@@ -73,14 +73,16 @@ impl NyaTermApp {
};
zones
.into_iter()
.any(|zone| !self.activity_bar_layout.zone(zone).is_empty())
.any(|zone| !self.shell.chrome.activity_bar_layout.zone(zone).is_empty())
}
pub(in crate::features) fn activity_entries_for_zone(
&self,
zone: ActivityBarZone,
) -> Vec<ActivityBarEntry> {
self.activity_bar_layout
self.shell
.chrome
.activity_bar_layout
.zone(zone)
.iter()
.filter_map(|id| ActivityBarEntry::from_persistence_id(id))
@@ -88,15 +90,20 @@ impl NyaTermApp {
}
pub(in crate::features) fn sync_activity_layout_to_settings(&mut self) {
self.settings.ui_activity_bar_left_top = self.activity_bar_layout.left_top.clone();
self.settings.ui_activity_bar_left_bottom = self.activity_bar_layout.left_bottom.clone();
self.settings.ui_activity_bar_right_top = self.activity_bar_layout.right_top.clone();
self.settings.ui_activity_bar_right_bottom = self.activity_bar_layout.right_bottom.clone();
self.settings.ui_activity_bar_show_labels = self.activity_bar_layout.show_labels;
self.settings.ui_activity_bar_left_top =
self.shell.chrome.activity_bar_layout.left_top.clone();
self.settings.ui_activity_bar_left_bottom =
self.shell.chrome.activity_bar_layout.left_bottom.clone();
self.settings.ui_activity_bar_right_top =
self.shell.chrome.activity_bar_layout.right_top.clone();
self.settings.ui_activity_bar_right_bottom =
self.shell.chrome.activity_bar_layout.right_bottom.clone();
self.settings.ui_activity_bar_show_labels =
self.shell.chrome.activity_bar_layout.show_labels;
}
pub(in crate::features) fn apply_activity_layout_from_settings(&mut self) {
self.activity_bar_layout = ActivityBarLayoutState {
self.shell.chrome.activity_bar_layout = ActivityBarLayoutState {
left_top: self.settings.ui_activity_bar_left_top.clone(),
left_bottom: self.settings.ui_activity_bar_left_bottom.clone(),
right_top: self.settings.ui_activity_bar_right_top.clone(),
@@ -110,7 +117,14 @@ impl NyaTermApp {
let mut seen = std::collections::HashSet::new();
for zone in ActivityBarZone::all() {
let mut next = Vec::new();
for id in self.activity_bar_layout.zone(zone).iter().cloned() {
for id in self
.shell
.chrome
.activity_bar_layout
.zone(zone)
.iter()
.cloned()
{
if id == "fileTransfer" {
continue;
}
@@ -118,7 +132,7 @@ impl NyaTermApp {
next.push(id);
}
}
*self.activity_bar_layout.zone_mut(zone) = next;
*self.shell.chrome.activity_bar_layout.zone_mut(zone) = next;
}
// Keep intentionally empty zones empty. Tauri only restores missing entries;
// it does not repopulate a zone after the user moves its last item away.
@@ -131,15 +145,20 @@ impl NyaTermApp {
.cloned()
.collect::<Vec<_>>();
if !missing.is_empty() {
self.activity_bar_layout.zone_mut(zone).extend(missing);
self.shell
.chrome
.activity_bar_layout
.zone_mut(zone)
.extend(missing);
seen.extend(defaults.zone(zone).iter().cloned());
}
}
}
pub(in crate::features) fn toggle_activity_bar_labels(&mut self, cx: &mut Context<Self>) {
self.activity_bar_layout.show_labels = !self.activity_bar_layout.show_labels;
self.terminal.view.status = if self.activity_bar_layout.show_labels {
self.shell.chrome.activity_bar_layout.show_labels =
!self.shell.chrome.activity_bar_layout.show_labels;
self.terminal.view.status = if self.shell.chrome.activity_bar_layout.show_labels {
"activity labels shown".to_string()
} else {
"activity labels hidden".to_string()
@@ -156,8 +175,8 @@ impl NyaTermApp {
event: &MouseDownEvent,
cx: &mut Context<Self>,
) {
self.title_menu_open = None;
self.activity_bar_context_menu = Some(ActivityBarContextMenuState {
self.shell.chrome.title_menu_open = None;
self.shell.chrome.activity_bar_context_menu = Some(ActivityBarContextMenuState {
entry_id,
zone,
index,
@@ -169,7 +188,7 @@ impl NyaTermApp {
}
pub(in crate::features) fn open_activity_bar_move_submenu(&mut self, cx: &mut Context<Self>) {
let Some(menu) = self.activity_bar_context_menu.as_mut() else {
let Some(menu) = self.shell.chrome.activity_bar_context_menu.as_mut() else {
return;
};
if !menu.move_submenu_open {
@@ -179,7 +198,7 @@ impl NyaTermApp {
}
pub(in crate::features) fn close_activity_bar_context_menu(&mut self, cx: &mut Context<Self>) {
self.activity_bar_context_menu = None;
self.shell.chrome.activity_bar_context_menu = None;
cx.notify();
}
@@ -190,7 +209,8 @@ impl NyaTermApp {
target_index: Option<usize>,
cx: &mut Context<Self>,
) {
let Some((source_zone, source_index)) = self.activity_bar_layout.find_entry(&entry_id)
let Some((source_zone, source_index)) =
self.shell.chrome.activity_bar_layout.find_entry(&entry_id)
else {
self.terminal.view.status = "activity item not found".to_string();
cx.notify();
@@ -199,14 +219,19 @@ impl NyaTermApp {
// Same entry dropped on itself — no-op.
if source_zone == target_zone {
let len = self.activity_bar_layout.zone(target_zone).len();
let len = self
.shell
.chrome
.activity_bar_layout
.zone(target_zone)
.len();
let mut insert_at = target_index.unwrap_or(len);
if source_index < insert_at {
insert_at = insert_at.saturating_sub(1);
}
insert_at = insert_at.min(len.saturating_sub(1));
if insert_at == source_index {
self.activity_bar_context_menu = None;
self.shell.chrome.activity_bar_context_menu = None;
cx.notify();
return;
}
@@ -214,16 +239,31 @@ impl NyaTermApp {
// Remove from source.
let removed = self
.shell
.chrome
.activity_bar_layout
.zone_mut(source_zone)
.remove(source_index);
let mut insert_at =
target_index.unwrap_or_else(|| self.activity_bar_layout.zone(target_zone).len());
let mut insert_at = target_index.unwrap_or_else(|| {
self.shell
.chrome
.activity_bar_layout
.zone(target_zone)
.len()
});
if source_zone == target_zone && source_index < insert_at {
insert_at = insert_at.saturating_sub(1);
}
insert_at = insert_at.min(self.activity_bar_layout.zone(target_zone).len());
self.activity_bar_layout
insert_at = insert_at.min(
self.shell
.chrome
.activity_bar_layout
.zone(target_zone)
.len(),
);
self.shell
.chrome
.activity_bar_layout
.zone_mut(target_zone)
.insert(insert_at, removed);
@@ -234,7 +274,7 @@ impl NyaTermApp {
self.clear_activity_entry_from_side(&entry_id, source_side);
}
self.activity_bar_context_menu = None;
self.shell.chrome.activity_bar_context_menu = None;
self.terminal.view.status = format!(
"moved {} to {}",
entry_id,
@@ -254,27 +294,34 @@ impl NyaTermApp {
fn clear_activity_entry_from_side(&mut self, entry_id: &str, side: PanelSide) {
match side {
PanelSide::Left => {
self.left_open_panels.retain(|id| id != entry_id);
self.shell.panels.left_open.retain(|id| id != entry_id);
if self
.active_left_panel
.shell
.panels
.active_left
.is_some_and(|item| item.persistence_id() == entry_id)
{
self.active_left_panel = None;
self.shell.panels.active_left = None;
}
if self.left_open_panels.is_empty() && self.active_left_panel.is_none() {
self.left_sidebar_collapsed = true;
if self.shell.panels.left_open.is_empty() && self.shell.panels.active_left.is_none()
{
self.shell.panels.left_collapsed = true;
}
}
PanelSide::Right => {
self.right_open_panels.retain(|id| id != entry_id);
self.shell.panels.right_open.retain(|id| id != entry_id);
if self
.active_right_panel
.shell
.panels
.active_right
.is_some_and(|item| item.persistence_id() == entry_id)
{
self.active_right_panel = None;
self.shell.panels.active_right = None;
}
if self.right_open_panels.is_empty() && self.active_right_panel.is_none() {
self.right_inspector_collapsed = true;
if self.shell.panels.right_open.is_empty()
&& self.shell.panels.active_right.is_none()
{
self.shell.panels.right_collapsed = true;
}
}
}
@@ -293,11 +340,11 @@ impl NyaTermApp {
self.open_panel(item, cx);
if !cfg!(target_os = "macos") {
match side {
Some(PanelSide::Left) if self.last_viewport_size.0 < 1024. => {
self.mobile_left_open = true;
Some(PanelSide::Left) if self.shell.viewport.size.0 < 1024. => {
self.shell.panels.mobile_left_open = true;
}
Some(PanelSide::Right) if self.last_viewport_size.0 < 768. => {
self.mobile_right_open = true;
Some(PanelSide::Right) if self.shell.viewport.size.0 < 768. => {
self.shell.panels.mobile_right_open = true;
}
_ => {}
}
@@ -327,11 +374,11 @@ impl NyaTermApp {
self.open_panel(NavItem::Recording, cx);
if !cfg!(target_os = "macos") {
match side {
Some(PanelSide::Left) if self.last_viewport_size.0 < 1024. => {
self.mobile_left_open = true;
Some(PanelSide::Left) if self.shell.viewport.size.0 < 1024. => {
self.shell.panels.mobile_left_open = true;
}
Some(PanelSide::Right) if self.last_viewport_size.0 < 768. => {
self.mobile_right_open = true;
Some(PanelSide::Right) if self.shell.viewport.size.0 < 768. => {
self.shell.panels.mobile_right_open = true;
}
_ => {}
}
@@ -345,7 +392,8 @@ impl NyaTermApp {
pub(in crate::features) fn activity_entry_selected(&self, entry: ActivityBarEntry) -> bool {
match entry {
ActivityBarEntry::Panel(NavItem::Settings) => {
self.settings_window.is_some() || self.main_mode == MainMode::Page
self.shell.navigation.settings.window.is_some()
|| self.shell.navigation.main_mode == MainMode::Page
}
ActivityBarEntry::Panel(item) => self.panel_entry_selected(item),
ActivityBarEntry::QuickCommands => {
@@ -365,13 +413,13 @@ impl NyaTermApp {
let Some(side) = self.panel_side_for_item(item) else {
return false;
};
if self.panel_multi_open {
if self.shell.panels.multi_open {
let id = item.persistence_id();
self.side_open_panel_ids(side).iter().any(|open| open == id)
|| self.side_overlay_panel(side) == Some(item)
|| match side {
PanelSide::Left => self.active_left_panel == Some(item),
PanelSide::Right => self.active_right_panel == Some(item),
PanelSide::Left => self.shell.panels.active_left == Some(item),
PanelSide::Right => self.shell.panels.active_right == Some(item),
}
} else {
match side {
@@ -174,10 +174,6 @@ pub(super) fn window_geometry_churn_active(
})
}
pub(super) fn title_drag_active(title_drag_active_until: Option<Instant>, now: Instant) -> bool {
title_drag_active_until.is_some_and(|until| now < until)
}
pub(super) fn connect_settle_active(until: Option<Instant>, now: Instant) -> bool {
until.is_some_and(|until| now < until)
}
@@ -439,7 +435,7 @@ mod tests {
terminal_input_idle_remaining_delay, terminal_log_plain_text,
terminal_output_dropped_marker, terminal_performance_tick_session_ids,
terminal_render_work_pressure_active, terminal_user_scroll_frame_apply_pending,
title_drag_active, viewport_change_terminal_session_ids, window_geometry_churn_active,
viewport_change_terminal_session_ids, window_geometry_churn_active,
};
#[test]
@@ -545,14 +541,6 @@ mod tests {
));
}
#[test]
fn title_drag_active_until_deadline() {
let now = Instant::now();
assert!(!title_drag_active(None, now));
assert!(title_drag_active(Some(now + Duration::from_millis(1)), now));
assert!(!title_drag_active(Some(now), now));
}
#[test]
fn connect_settle_active_until_deadline() {
let now = Instant::now();
@@ -10,7 +10,7 @@ use crate::features::shell::event_pump::helpers::{
connect_settle_active, connect_settle_deadline, diagnostic_log_due,
pending_session_status_message, remote_refresh_due, runtime_output_pressure_active_from_counts,
runtime_tick_interval_for_pressure, runtime_ui_notify_allowed,
terminal_cell_metrics_refresh_needed, terminal_input_idle_remaining_delay, title_drag_active,
terminal_cell_metrics_refresh_needed, terminal_input_idle_remaining_delay,
viewport_change_terminal_session_ids, window_geometry_churn_active,
};
use crate::features::{
@@ -59,13 +59,14 @@ impl NyaTermApp {
window: &mut Window,
cx: &mut Context<Self>,
) -> bool {
let before_viewport = self.last_viewport_size;
let before_metrics = self.terminal.layout.cell_metrics;
let vs = window.viewport_size();
self.last_viewport_size = (f32::from(vs.width), f32::from(vs.height));
if self.last_viewport_size != before_viewport {
let viewport_changed = self
.shell
.viewport
.update_size((f32::from(vs.width), f32::from(vs.height)), Instant::now());
if viewport_changed {
// Geometry churn (resize / some window managers during move).
self.last_viewport_change_at = Some(Instant::now());
self.notify_terminal_surfaces_for_viewport_change(cx);
}
if terminal_cell_metrics_refresh_needed(self.terminal.layout.cell_metrics) {
@@ -76,8 +77,7 @@ impl NyaTermApp {
self.resize_all_known_terminal_surfaces();
self.refresh_visible_terminal_surfaces(cx);
}
self.last_viewport_size != before_viewport
|| self.terminal.layout.cell_metrics != before_metrics
viewport_changed || self.terminal.layout.cell_metrics != before_metrics
}
fn notify_terminal_surfaces_for_viewport_change(&mut self, cx: &mut Context<Self>) {
@@ -219,11 +219,13 @@ impl NyaTermApp {
}
pub(in crate::features) fn mark_title_drag_activity(&mut self) {
self.title_drag_active_until = Some(Instant::now() + TITLE_DRAG_ACTIVE_HOLD);
self.shell
.viewport
.mark_title_drag(Instant::now(), TITLE_DRAG_ACTIVE_HOLD);
}
pub(in crate::features) fn title_drag_active(&self, now: Instant) -> bool {
title_drag_active(self.title_drag_active_until, now)
self.shell.viewport.title_drag_active(now)
}
pub(in crate::features) fn should_log_slow_diagnostic(
@@ -305,7 +307,7 @@ impl NyaTermApp {
// idle cadence so full plane ticks do not stack on compositor paints.
let now = Instant::now();
if self.title_drag_active(now)
|| window_geometry_churn_active(self.last_viewport_change_at, now)
|| window_geometry_churn_active(self.shell.viewport.last_change_at, now)
{
return RUNTIME_IDLE_TICK_INTERVAL;
}
@@ -323,7 +325,7 @@ impl NyaTermApp {
if !self.terminal.view.runtime.event_pump_started {
return false;
}
if self.last_viewport_size != viewport_size
if self.shell.viewport.size != viewport_size
|| terminal_cell_metrics_refresh_needed(self.terminal.layout.cell_metrics)
{
return true;
@@ -161,7 +161,7 @@ impl NyaTermApp {
}
return true;
}
let geometry_churn = window_geometry_churn_active(self.last_viewport_change_at, now);
let geometry_churn = window_geometry_churn_active(self.shell.viewport.last_change_at, now);
let calm_tick = self.runtime_quiet_tick_allowed();
if geometry_churn && calm_tick {
dirty |= self.drive_pending_focus(window, cx);
@@ -603,7 +603,7 @@ impl NyaTermApp {
// Idle-plane work does not drain output; one pressure sample is enough for the stage.
let output_pressure = self.runtime_output_pressure_active();
let now = Instant::now();
let geometry_churn = window_geometry_churn_active(self.last_viewport_change_at, now);
let geometry_churn = window_geometry_churn_active(self.shell.viewport.last_change_at, now);
let connect_settle =
connect_settle_active(self.terminal.view.runtime.connect_settle_until, now);
// Geometry churn / connect settle: keep focus only (no remote/layout/DB).
@@ -32,7 +32,7 @@ impl NyaTermApp {
// Dismiss strip menus with Escape (Tauri dropdown dismiss).
if event.keystroke.key.as_str() == "escape"
&& (self.open_tabs_menu_open || self.new_session_menu_open)
&& (self.shell.chrome.open_tabs_menu_open || self.shell.chrome.new_session_menu_open)
{
self.close_open_tabs_menu(cx);
self.close_new_session_menu(cx);
@@ -20,7 +20,7 @@ pub(in crate::features) use activity_bar_runtime::{
pub(in crate::features) use appearance::{
appearance_font_options, appearance_font_stack, gpui_code_font_family,
};
pub(in crate::features) use state::ShellFeatureState;
pub(in crate::features) use state::{ShellFeatureInit, ShellFeatureState};
pub(in crate::features) use tab_mouse::{
ChromeTooltip, SessionTabDragPayload, SessionTabDragPreview, SessionTabTooltip,
TAB_MOUSE_ACTIONS, TabMouseActionTarget,
@@ -7,23 +7,35 @@ impl NyaTermApp {
pub(in crate::features) fn open_page(&mut self, item: NavItem, cx: &mut Context<Self>) {
if item == NavItem::Settings || item.opens_settings() {
self.begin_settings_draft();
if let Some(group) = self.settings_active_tab.expandable_group_id() {
self.settings_expanded_groups.insert(group.to_string());
if let Some(group) = self
.shell
.navigation
.settings
.active_tab
.expandable_group_id()
{
self.shell
.navigation
.settings
.expanded_groups
.insert(group.to_string());
}
if self.main_mode != MainMode::Page {
self.settings_previous_left_collapsed = Some(self.left_sidebar_collapsed);
self.settings_previous_right_collapsed = Some(self.right_inspector_collapsed);
if self.shell.navigation.main_mode != MainMode::Page {
self.shell.navigation.settings.previous_left_collapsed =
Some(self.shell.panels.left_collapsed);
self.shell.navigation.settings.previous_right_collapsed =
Some(self.shell.panels.right_collapsed);
}
if self.open_settings_window(cx) {
self.main_mode = MainMode::Workspace;
self.shell.navigation.main_mode = MainMode::Workspace;
self.terminal.view.status = "settings opened".to_string();
cx.notify();
return;
}
self.main_mode = MainMode::Page;
self.selected_nav = NavItem::Settings;
self.left_sidebar_collapsed = true;
self.right_inspector_collapsed = true;
self.shell.navigation.main_mode = MainMode::Page;
self.shell.navigation.selected_nav = NavItem::Settings;
self.shell.panels.left_collapsed = true;
self.shell.panels.right_collapsed = true;
self.terminal.view.status = "settings opened".to_string();
cx.notify();
return;
@@ -43,14 +55,14 @@ impl NyaTermApp {
return;
}
if self.panel_multi_open && self.panel_side_for_item(item).is_some() {
if self.shell.panels.multi_open && self.panel_side_for_item(item).is_some() {
self.open_or_toggle_panel(item, cx);
return;
}
self.main_mode = MainMode::Workspace;
self.selected_nav = item;
self.right_focus = if item == NavItem::Recording {
self.shell.navigation.main_mode = MainMode::Workspace;
self.shell.navigation.selected_nav = item;
self.shell.panels.right_focus = if item == NavItem::Recording {
RightFocus::Recording
} else {
RightFocus::Default
@@ -58,35 +70,35 @@ impl NyaTermApp {
match self.panel_side_for_item(item) {
Some(PanelSide::Left) => {
let already_open =
self.active_left_panel == Some(item) && !self.left_sidebar_collapsed;
let already_open = self.shell.panels.active_left == Some(item)
&& !self.shell.panels.left_collapsed;
if already_open {
self.left_sidebar_collapsed = true;
self.active_left_panel = None;
self.shell.panels.left_collapsed = true;
self.shell.panels.active_left = None;
self.terminal.view.status = format!("{} closed", item.label());
} else {
self.active_left_panel = Some(item);
self.left_sidebar_collapsed = false;
self.shell.panels.active_left = Some(item);
self.shell.panels.left_collapsed = false;
self.terminal.view.status = format!("{} opened", item.label());
}
}
Some(PanelSide::Right) => {
let already_open =
self.active_right_panel == Some(item) && !self.right_inspector_collapsed;
let already_open = self.shell.panels.active_right == Some(item)
&& !self.shell.panels.right_collapsed;
if already_open {
self.right_inspector_collapsed = true;
self.active_right_panel = None;
self.right_focus = RightFocus::Default;
self.shell.panels.right_collapsed = true;
self.shell.panels.active_right = None;
self.shell.panels.right_focus = RightFocus::Default;
self.terminal.view.status = format!("{} closed", item.label());
} else {
self.active_right_panel = Some(item);
self.right_inspector_collapsed = false;
self.shell.panels.active_right = Some(item);
self.shell.panels.right_collapsed = false;
self.terminal.view.status = format!("{} opened", item.label());
}
}
None => {
self.left_sidebar_collapsed = false;
self.right_inspector_collapsed = false;
self.shell.panels.left_collapsed = false;
self.shell.panels.right_collapsed = false;
}
}
@@ -95,21 +107,21 @@ impl NyaTermApp {
}
pub(in crate::features) fn ensure_panel_open(&mut self, item: NavItem) {
if self.panel_multi_open && self.panel_side_for_item(item).is_some() {
if self.shell.panels.multi_open && self.panel_side_for_item(item).is_some() {
self.ensure_panel_in_stack(item);
return;
}
self.main_mode = MainMode::Workspace;
self.selected_nav = item;
self.shell.navigation.main_mode = MainMode::Workspace;
self.shell.navigation.selected_nav = item;
match self.panel_side_for_item(item) {
Some(PanelSide::Left) => {
self.active_left_panel = Some(item);
self.left_sidebar_collapsed = false;
self.shell.panels.active_left = Some(item);
self.shell.panels.left_collapsed = false;
}
Some(PanelSide::Right) => {
self.active_right_panel = Some(item);
self.right_inspector_collapsed = false;
self.right_focus = if item == NavItem::Recording {
self.shell.panels.active_right = Some(item);
self.shell.panels.right_collapsed = false;
self.shell.panels.right_focus = if item == NavItem::Recording {
RightFocus::Recording
} else {
RightFocus::Default
@@ -124,34 +136,40 @@ impl NyaTermApp {
}
pub(in crate::features) fn toggle_left_sidebar(&mut self, cx: &mut Context<Self>) {
if self.panel_multi_open {
if self.shell.panels.multi_open {
if self.left_side_open() {
self.left_open_panels.clear();
self.active_left_panel = None;
self.left_sidebar_collapsed = true;
self.shell.panels.left_open.clear();
self.shell.panels.active_left = None;
self.shell.panels.left_collapsed = true;
self.terminal.view.status = "left sidebar collapsed".to_string();
} else if let Some(panel) = self
.shell
.chrome
.activity_bar_layout
.first_panel_on_side(PanelSide::Left)
{
self.active_left_panel = Some(panel);
self.left_open_panels.clear();
self.shell.panels.active_left = Some(panel);
self.shell.panels.left_open.clear();
if Self::is_stackable_panel_id(panel.persistence_id()) {
self.left_open_panels
self.shell
.panels
.left_open
.push(panel.persistence_id().to_string());
}
self.left_sidebar_collapsed = false;
self.shell.panels.left_collapsed = false;
self.terminal.view.status = "left sidebar expanded".to_string();
}
} else if self.left_sidebar_collapsed || self.active_left_panel.is_none() {
self.active_left_panel = self
} else if self.shell.panels.left_collapsed || self.shell.panels.active_left.is_none() {
self.shell.panels.active_left = self
.shell
.chrome
.activity_bar_layout
.first_panel_on_side(PanelSide::Left);
self.left_sidebar_collapsed = false;
self.shell.panels.left_collapsed = false;
self.terminal.view.status = "left sidebar expanded".to_string();
} else {
self.active_left_panel = None;
self.left_sidebar_collapsed = true;
self.shell.panels.active_left = None;
self.shell.panels.left_collapsed = true;
self.terminal.view.status = "left sidebar collapsed".to_string();
}
self.persist_ui_layout();
@@ -159,34 +177,40 @@ impl NyaTermApp {
}
pub(in crate::features) fn toggle_right_inspector(&mut self, cx: &mut Context<Self>) {
if self.panel_multi_open {
if self.shell.panels.multi_open {
if self.right_side_open() {
self.right_open_panels.clear();
self.active_right_panel = None;
self.right_inspector_collapsed = true;
self.shell.panels.right_open.clear();
self.shell.panels.active_right = None;
self.shell.panels.right_collapsed = true;
self.terminal.view.status = "right sidebar collapsed".to_string();
} else if let Some(panel) = self
.shell
.chrome
.activity_bar_layout
.first_panel_on_side(PanelSide::Right)
{
self.active_right_panel = Some(panel);
self.right_open_panels.clear();
self.shell.panels.active_right = Some(panel);
self.shell.panels.right_open.clear();
if Self::is_stackable_panel_id(panel.persistence_id()) {
self.right_open_panels
self.shell
.panels
.right_open
.push(panel.persistence_id().to_string());
}
self.right_inspector_collapsed = false;
self.shell.panels.right_collapsed = false;
self.terminal.view.status = "right sidebar expanded".to_string();
}
} else if self.right_inspector_collapsed || self.active_right_panel.is_none() {
self.active_right_panel = self
} else if self.shell.panels.right_collapsed || self.shell.panels.active_right.is_none() {
self.shell.panels.active_right = self
.shell
.chrome
.activity_bar_layout
.first_panel_on_side(PanelSide::Right);
self.right_inspector_collapsed = false;
self.shell.panels.right_collapsed = false;
self.terminal.view.status = "right sidebar expanded".to_string();
} else {
self.active_right_panel = None;
self.right_inspector_collapsed = true;
self.shell.panels.active_right = None;
self.shell.panels.right_collapsed = true;
self.terminal.view.status = "right sidebar collapsed".to_string();
}
self.persist_ui_layout();
@@ -194,46 +218,50 @@ impl NyaTermApp {
}
pub(in crate::features) fn toggle_mobile_left_drawer(&mut self, cx: &mut Context<Self>) {
if self.mobile_left_open {
self.mobile_left_open = false;
if self.shell.panels.mobile_left_open {
self.shell.panels.mobile_left_open = false;
} else {
if self.active_left_panel.is_none() {
self.active_left_panel = self
if self.shell.panels.active_left.is_none() {
self.shell.panels.active_left = self
.shell
.chrome
.activity_bar_layout
.first_panel_on_side(PanelSide::Left);
}
self.left_sidebar_collapsed = false;
self.mobile_left_open = true;
self.shell.panels.left_collapsed = false;
self.shell.panels.mobile_left_open = true;
}
cx.notify();
}
pub(in crate::features) fn toggle_mobile_right_drawer(&mut self, cx: &mut Context<Self>) {
if self.mobile_right_open {
self.mobile_right_open = false;
if self.shell.panels.mobile_right_open {
self.shell.panels.mobile_right_open = false;
} else {
if self.active_right_panel.is_none() {
self.active_right_panel = self
if self.shell.panels.active_right.is_none() {
self.shell.panels.active_right = self
.shell
.chrome
.activity_bar_layout
.first_panel_on_side(PanelSide::Right);
}
self.right_inspector_collapsed = false;
self.mobile_right_open = true;
self.shell.panels.right_collapsed = false;
self.shell.panels.mobile_right_open = true;
}
cx.notify();
}
pub(in crate::features) fn current_left_panel(&self) -> Option<NavItem> {
if self.left_sidebar_collapsed {
if self.shell.panels.left_collapsed {
return None;
}
if self.panel_multi_open {
if self.shell.panels.multi_open {
if self.side_overlay_panel(PanelSide::Left).is_some()
|| !self.side_open_panel_ids(PanelSide::Left).is_empty()
{
return self
.side_overlay_panel(PanelSide::Left)
.or(self.active_left_panel)
.or(self.shell.panels.active_left)
.or_else(|| {
self.side_open_panel_ids(PanelSide::Left)
.first()
@@ -242,52 +270,55 @@ impl NyaTermApp {
}
return None;
}
self.active_left_panel
self.shell.panels.active_left
}
pub(in crate::features) fn current_right_panel(&self) -> Option<NavItem> {
if self.right_inspector_collapsed {
if self.shell.panels.right_collapsed {
return None;
}
if self.panel_multi_open {
if self.shell.panels.multi_open {
if self.side_overlay_panel(PanelSide::Right).is_some()
|| !self.side_open_panel_ids(PanelSide::Right).is_empty()
{
return self
.side_overlay_panel(PanelSide::Right)
.or(self.active_right_panel)
.or(self.shell.panels.active_right)
.or_else(|| {
self.side_open_panel_ids(PanelSide::Right)
.first()
.and_then(|id| NavItem::from_persistence_id(id))
})
.or(if self.right_focus == RightFocus::Recording {
.or(if self.shell.panels.right_focus == RightFocus::Recording {
Some(NavItem::Recording)
} else {
None
});
}
return if self.right_focus == RightFocus::Recording {
return if self.shell.panels.right_focus == RightFocus::Recording {
Some(NavItem::Recording)
} else {
None
};
}
self.active_right_panel
.or(if self.right_focus == RightFocus::Recording {
self.shell.panels.active_right.or(
if self.shell.panels.right_focus == RightFocus::Recording {
Some(NavItem::Recording)
} else {
None
})
},
)
}
pub(in crate::features) fn left_side_open(&self) -> bool {
self.current_left_panel().is_some()
|| (self.panel_multi_open && !self.side_open_panel_ids(PanelSide::Left).is_empty())
|| (self.shell.panels.multi_open
&& !self.side_open_panel_ids(PanelSide::Left).is_empty())
}
pub(in crate::features) fn right_side_open(&self) -> bool {
self.current_right_panel().is_some()
|| (self.panel_multi_open && !self.side_open_panel_ids(PanelSide::Right).is_empty())
|| (self.shell.panels.multi_open
&& !self.side_open_panel_ids(PanelSide::Right).is_empty())
}
}
@@ -6,14 +6,10 @@ use nyaterm_core::ConnectionStore;
use crate::features::NyaTermApp;
use crate::models::{
BottomPanelMode, NavItem, PanelResizeSide, PanelResizeState, PanelSide,
TransferHeightResizeState, panel_collapsed_from_persistence,
BottomPanelMode, NavItem, PanelResizeSide, PanelSide, TransferHeightResizeState,
panel_collapsed_from_persistence,
};
const LEFT_PANEL_MIN: f32 = 160.;
const LEFT_PANEL_MAX: f32 = 720.;
const RIGHT_PANEL_MIN: f32 = 200.;
const RIGHT_PANEL_MAX: f32 = 720.;
const QUICK_CMD_HEIGHT_MIN: f32 = 36.;
const SERIAL_SEND_HEIGHT_MIN: f32 = 60.;
const BOTTOM_PANEL_HEIGHT_MAX: f32 = 520.;
@@ -32,15 +28,7 @@ impl NyaTermApp {
event: &MouseDownEvent,
cx: &mut Context<Self>,
) {
let start_width = match side {
PanelResizeSide::Left => px(self.left_panel_width),
PanelResizeSide::Right => px(self.right_panel_width),
};
self.panel_resize = Some(PanelResizeState {
side,
start_x: event.position.x,
start_width,
});
self.shell.panels.start_resize(side, event.position.x);
self.terminal.view.status = match side {
PanelResizeSide::Left => "resizing left panel".to_string(),
PanelResizeSide::Right => "resizing right panel".to_string(),
@@ -53,22 +41,16 @@ impl NyaTermApp {
event: &MouseMoveEvent,
cx: &mut Context<Self>,
) {
let Some(state) = self.panel_resize else {
let Some((side, width)) = self.shell.panels.update_resize(event.position.x) else {
return;
};
let delta = f32::from(event.position.x - state.start_x);
let start = f32::from(state.start_width);
match state.side {
match side {
PanelResizeSide::Left => {
self.left_panel_width = (start + delta).clamp(LEFT_PANEL_MIN, LEFT_PANEL_MAX);
self.terminal.view.status =
format!("left panel: {:.0}px", self.left_panel_width.round());
self.terminal.view.status = format!("left panel: {:.0}px", width.round());
}
PanelResizeSide::Right => {
// Right handle sits on the left edge of the right panel: drag left grows width.
self.right_panel_width = (start - delta).clamp(RIGHT_PANEL_MIN, RIGHT_PANEL_MAX);
self.terminal.view.status =
format!("right panel: {:.0}px", self.right_panel_width.round());
self.terminal.view.status = format!("right panel: {:.0}px", width.round());
}
}
cx.notify();
@@ -79,12 +61,12 @@ impl NyaTermApp {
_event: &MouseUpEvent,
cx: &mut Context<Self>,
) {
if self.panel_resize.take().is_some() {
if self.shell.panels.finish_resize() {
self.persist_panel_widths();
self.terminal.view.status = format!(
"panel sizes L{:.0}/R{:.0}",
self.left_panel_width.round(),
self.right_panel_width.round()
self.shell.panels.left_width.round(),
self.shell.panels.right_width.round()
);
cx.notify();
}
@@ -95,34 +77,34 @@ impl NyaTermApp {
}
pub(in crate::features) fn apply_ui_layout_from_settings(&mut self) {
self.left_panel_width = self.settings.ui_left_panel_width as f32;
self.right_panel_width = self.settings.ui_right_panel_width as f32;
self.shell.panels.left_width = self.settings.ui_left_panel_width as f32;
self.shell.panels.right_width = self.settings.ui_right_panel_width as f32;
self.transfer.panel.height = self.settings.ui_transfer_height as f32;
self.shell.bottom_panel.quick_commands_height = self.settings.ui_quick_cmd_height as f32;
self.shell.bottom_panel.command_send_height = self.settings.ui_serial_send_height as f32;
self.apply_activity_layout_from_settings();
self.active_left_panel = self
self.shell.panels.active_left = self
.settings
.ui_active_left_panel
.as_deref()
.and_then(NavItem::from_persistence_id)
.filter(|item| self.panel_side_for_item(*item) == Some(PanelSide::Left));
self.active_right_panel = self
self.shell.panels.active_right = self
.settings
.ui_active_right_panel
.as_deref()
.and_then(NavItem::from_persistence_id)
.filter(|item| self.panel_side_for_item(*item) == Some(PanelSide::Right));
self.left_sidebar_collapsed = panel_collapsed_from_persistence(
self.shell.panels.left_collapsed = panel_collapsed_from_persistence(
self.settings.ui_left_panel_collapsed,
self.settings.ui_panel_multi_open,
self.active_left_panel.is_some(),
self.shell.panels.active_left.is_some(),
!self.settings.ui_left_open_panels.is_empty(),
);
self.right_inspector_collapsed = panel_collapsed_from_persistence(
self.shell.panels.right_collapsed = panel_collapsed_from_persistence(
self.settings.ui_right_panel_collapsed,
self.settings.ui_panel_multi_open,
self.active_right_panel.is_some(),
self.shell.panels.active_right.is_some(),
!self.settings.ui_right_open_panels.is_empty(),
);
self.apply_panel_stack_from_settings();
@@ -132,9 +114,10 @@ impl NyaTermApp {
}
pub(in crate::features) fn persist_ui_layout(&mut self) {
self.settings.ui_left_panel_width = self.left_panel_width.round().clamp(160., 720.) as u32;
self.settings.ui_left_panel_width =
self.shell.panels.left_width.round().clamp(160., 720.) as u32;
self.settings.ui_right_panel_width =
self.right_panel_width.round().clamp(200., 720.) as u32;
self.shell.panels.right_width.round().clamp(200., 720.) as u32;
self.settings.ui_transfer_height =
self.transfer.panel.height.round().clamp(60., 600.) as u32;
self.settings.ui_quick_cmd_height =
@@ -150,13 +133,17 @@ impl NyaTermApp {
.round()
.clamp(SERIAL_SEND_HEIGHT_MIN, BOTTOM_PANEL_HEIGHT_MAX) as u32;
self.settings.ui_active_left_panel = self
.active_left_panel
.shell
.panels
.active_left
.map(|item| item.persistence_id().to_string());
self.settings.ui_active_right_panel = self
.active_right_panel
.shell
.panels
.active_right
.map(|item| item.persistence_id().to_string());
self.settings.ui_left_panel_collapsed = self.left_sidebar_collapsed;
self.settings.ui_right_panel_collapsed = self.right_inspector_collapsed;
self.settings.ui_left_panel_collapsed = self.shell.panels.left_collapsed;
self.settings.ui_right_panel_collapsed = self.shell.panels.right_collapsed;
self.settings.ui_saved_connections_sort_mode = self
.connection_state
.list_sort_mode()
@@ -10,8 +10,8 @@ use nyaterm_core::{AgentCommandExecutionMode, truncate_preview};
use crate::features::{ChromeTooltip, NyaTermApp, TextInputSetup, panel_header_with_actions};
use crate::models::{
ActivityBarZone, MainMode, NavItem, NetworkTab, PanelSide, PanelStackResizeState, RightFocus,
SecurityAuthTab, SettingsTab,
ActivityBarZone, MainMode, NavItem, NetworkTab, PanelSide, RightFocus, SecurityAuthTab,
SettingsTab,
};
use crate::theme::ThemePalette;
@@ -28,41 +28,45 @@ impl NyaTermApp {
}
pub(in crate::features) fn toggle_panel_multi_open(&mut self, cx: &mut Context<Self>) {
self.panel_multi_open = !self.panel_multi_open;
if self.panel_multi_open {
if self.left_open_panels.is_empty() {
if let Some(panel) = self.active_left_panel {
self.shell.panels.multi_open = !self.shell.panels.multi_open;
if self.shell.panels.multi_open {
if self.shell.panels.left_open.is_empty() {
if let Some(panel) = self.shell.panels.active_left {
let id = panel.persistence_id().to_string();
if Self::is_stackable_panel_id(&id) {
self.left_open_panels.push(id);
self.shell.panels.left_open.push(id);
}
}
}
if self.right_open_panels.is_empty() {
if let Some(panel) = self.active_right_panel {
if self.shell.panels.right_open.is_empty() {
if let Some(panel) = self.shell.panels.active_right {
let id = panel.persistence_id().to_string();
if Self::is_stackable_panel_id(&id) {
self.right_open_panels.push(id);
self.shell.panels.right_open.push(id);
}
}
}
self.terminal.view.status = "multi-open panels enabled".to_string();
} else {
// Collapse to active-only mode.
if self.active_left_panel.is_none() {
self.active_left_panel = self
.left_open_panels
if self.shell.panels.active_left.is_none() {
self.shell.panels.active_left = self
.shell
.panels
.left_open
.first()
.and_then(|id| NavItem::from_persistence_id(id));
}
if self.active_right_panel.is_none() {
self.active_right_panel = self
.right_open_panels
if self.shell.panels.active_right.is_none() {
self.shell.panels.active_right = self
.shell
.panels
.right_open
.first()
.and_then(|id| NavItem::from_persistence_id(id));
}
self.left_open_panels.clear();
self.right_open_panels.clear();
self.shell.panels.left_open.clear();
self.shell.panels.right_open.clear();
self.terminal.view.status = "single panel mode".to_string();
}
self.persist_ui_layout();
@@ -70,10 +74,10 @@ impl NyaTermApp {
}
pub(in crate::features) fn side_open_panel_ids(&self, side: PanelSide) -> Vec<String> {
if !self.panel_multi_open {
if !self.shell.panels.multi_open {
let active = match side {
PanelSide::Left => self.active_left_panel,
PanelSide::Right => self.active_right_panel,
PanelSide::Left => self.shell.panels.active_left,
PanelSide::Right => self.shell.panels.active_right,
};
return active
.map(|item| item.persistence_id().to_string())
@@ -82,8 +86,8 @@ impl NyaTermApp {
}
let open = match side {
PanelSide::Left => &self.left_open_panels,
PanelSide::Right => &self.right_open_panels,
PanelSide::Left => &self.shell.panels.left_open,
PanelSide::Right => &self.shell.panels.right_open,
};
if open.is_empty() {
return Vec::new();
@@ -95,7 +99,7 @@ impl NyaTermApp {
};
let mut ordered = Vec::new();
for zone in zones {
for id in self.activity_bar_layout.zone(zone) {
for id in self.shell.chrome.activity_bar_layout.zone(zone) {
if open_set.contains(id) && Self::is_stackable_panel_id(id) {
ordered.push(id.clone());
}
@@ -105,27 +109,25 @@ impl NyaTermApp {
}
pub(in crate::features) fn side_overlay_panel(&self, side: PanelSide) -> Option<NavItem> {
if !self.panel_multi_open {
if !self.shell.panels.multi_open {
return None;
}
let active = match side {
PanelSide::Left => self.active_left_panel,
PanelSide::Right => self.active_right_panel,
PanelSide::Left => self.shell.panels.active_left,
PanelSide::Right => self.shell.panels.active_right,
}?;
let id = active.persistence_id();
Self::is_exclusive_panel_id(id).then_some(active)
}
pub(in crate::features) fn panel_stack_weight(&self, panel_id: &str) -> f32 {
self.panel_stack_sizes
.get(panel_id)
.copied()
.filter(|value| value.is_finite() && *value > 0.)
.unwrap_or(1.)
self.shell.panels.stack_weight(panel_id)
}
pub(in crate::features) fn panel_side_for_item(&self, item: NavItem) -> Option<PanelSide> {
self.activity_bar_layout
self.shell
.chrome
.activity_bar_layout
.side_for_entry(item.persistence_id())
.or_else(|| item.is_left_panel().then_some(PanelSide::Left))
.or_else(|| item.is_right_panel().then_some(PanelSide::Right))
@@ -140,7 +142,7 @@ impl NyaTermApp {
self.open_page(NavItem::Settings, cx);
return;
}
if !self.panel_multi_open {
if !self.shell.panels.multi_open {
self.open_panel(item, cx);
return;
}
@@ -151,18 +153,18 @@ impl NyaTermApp {
return;
};
self.main_mode = MainMode::Workspace;
self.selected_nav = item;
self.shell.navigation.main_mode = MainMode::Workspace;
self.shell.navigation.selected_nav = item;
if item == NavItem::Recording {
self.right_focus = RightFocus::Recording;
self.shell.panels.right_focus = RightFocus::Recording;
} else {
self.right_focus = RightFocus::Default;
self.shell.panels.right_focus = RightFocus::Default;
}
if Self::is_exclusive_panel_id(&id) {
let active = match side {
PanelSide::Left => self.active_left_panel,
PanelSide::Right => self.active_right_panel,
PanelSide::Left => self.shell.panels.active_left,
PanelSide::Right => self.shell.panels.active_right,
};
if active == Some(item) {
// Dismiss exclusive overlay to stack.
@@ -172,24 +174,24 @@ impl NyaTermApp {
.find_map(|open_id| NavItem::from_persistence_id(&open_id));
match side {
PanelSide::Left => {
self.active_left_panel = fallback;
self.left_sidebar_collapsed = fallback.is_none();
self.shell.panels.active_left = fallback;
self.shell.panels.left_collapsed = fallback.is_none();
}
PanelSide::Right => {
self.active_right_panel = fallback;
self.right_inspector_collapsed = fallback.is_none();
self.shell.panels.active_right = fallback;
self.shell.panels.right_collapsed = fallback.is_none();
}
}
self.terminal.view.status = format!("{} closed", item.label());
} else {
match side {
PanelSide::Left => {
self.active_left_panel = Some(item);
self.left_sidebar_collapsed = false;
self.shell.panels.active_left = Some(item);
self.shell.panels.left_collapsed = false;
}
PanelSide::Right => {
self.active_right_panel = Some(item);
self.right_inspector_collapsed = false;
self.shell.panels.active_right = Some(item);
self.shell.panels.right_collapsed = false;
}
}
self.terminal.view.status = format!("{} opened", item.label());
@@ -200,13 +202,13 @@ impl NyaTermApp {
}
let open_list = match side {
PanelSide::Left => &mut self.left_open_panels,
PanelSide::Right => &mut self.right_open_panels,
PanelSide::Left => &mut self.shell.panels.left_open,
PanelSide::Right => &mut self.shell.panels.right_open,
};
let is_open = open_list.iter().any(|value| value == &id);
let active = match side {
PanelSide::Left => self.active_left_panel,
PanelSide::Right => self.active_right_panel,
PanelSide::Left => self.shell.panels.active_left,
PanelSide::Right => self.shell.panels.active_right,
};
// If exclusive overlay is showing and stacked panel already open, reveal stack.
@@ -217,12 +219,12 @@ impl NyaTermApp {
{
match side {
PanelSide::Left => {
self.active_left_panel = Some(item);
self.left_sidebar_collapsed = false;
self.shell.panels.active_left = Some(item);
self.shell.panels.left_collapsed = false;
}
PanelSide::Right => {
self.active_right_panel = Some(item);
self.right_inspector_collapsed = false;
self.shell.panels.active_right = Some(item);
self.shell.panels.right_collapsed = false;
}
}
self.terminal.view.status = format!("{} focused", item.label());
@@ -247,14 +249,14 @@ impl NyaTermApp {
};
match side {
PanelSide::Left => {
self.active_left_panel = next_active;
self.left_sidebar_collapsed =
next_active.is_none() && self.left_open_panels.is_empty();
self.shell.panels.active_left = next_active;
self.shell.panels.left_collapsed =
next_active.is_none() && self.shell.panels.left_open.is_empty();
}
PanelSide::Right => {
self.active_right_panel = next_active;
self.right_inspector_collapsed =
next_active.is_none() && self.right_open_panels.is_empty();
self.shell.panels.active_right = next_active;
self.shell.panels.right_collapsed =
next_active.is_none() && self.shell.panels.right_open.is_empty();
}
}
self.terminal.view.status = format!("{} closed", item.label());
@@ -262,12 +264,12 @@ impl NyaTermApp {
open_list.push(id);
match side {
PanelSide::Left => {
self.active_left_panel = Some(item);
self.left_sidebar_collapsed = false;
self.shell.panels.active_left = Some(item);
self.shell.panels.left_collapsed = false;
}
PanelSide::Right => {
self.active_right_panel = Some(item);
self.right_inspector_collapsed = false;
self.shell.panels.active_right = Some(item);
self.shell.panels.right_collapsed = false;
}
}
self.terminal.view.status = format!("{} opened", item.label());
@@ -277,39 +279,45 @@ impl NyaTermApp {
}
pub(in crate::features) fn ensure_panel_in_stack(&mut self, item: NavItem) {
self.main_mode = MainMode::Workspace;
self.selected_nav = item;
if !self.panel_multi_open {
self.shell.navigation.main_mode = MainMode::Workspace;
self.shell.navigation.selected_nav = item;
if !self.shell.panels.multi_open {
self.ensure_panel_open(item);
return;
}
let id = item.persistence_id().to_string();
match self.panel_side_for_item(item) {
Some(PanelSide::Left) => {
self.left_sidebar_collapsed = false;
self.shell.panels.left_collapsed = false;
if Self::is_exclusive_panel_id(&id) {
self.active_left_panel = Some(item);
self.shell.panels.active_left = Some(item);
} else {
if !self.left_open_panels.iter().any(|value| value == &id) {
self.left_open_panels.push(id);
if !self.shell.panels.left_open.iter().any(|value| value == &id) {
self.shell.panels.left_open.push(id);
}
self.active_left_panel = Some(item);
self.shell.panels.active_left = Some(item);
}
}
Some(PanelSide::Right) => {
self.right_inspector_collapsed = false;
self.right_focus = if item == NavItem::Recording {
self.shell.panels.right_collapsed = false;
self.shell.panels.right_focus = if item == NavItem::Recording {
RightFocus::Recording
} else {
RightFocus::Default
};
if Self::is_exclusive_panel_id(&id) {
self.active_right_panel = Some(item);
self.shell.panels.active_right = Some(item);
} else {
if !self.right_open_panels.iter().any(|value| value == &id) {
self.right_open_panels.push(id);
if !self
.shell
.panels
.right_open
.iter()
.any(|value| value == &id)
{
self.shell.panels.right_open.push(id);
}
self.active_right_panel = Some(item);
self.shell.panels.active_right = Some(item);
}
}
None => {}
@@ -325,15 +333,13 @@ impl NyaTermApp {
container_height: f32,
cx: &mut Context<Self>,
) {
self.panel_stack_resize = Some(PanelStackResizeState {
self.shell.panels.start_stack_resize(
side,
above_id: above_id.clone(),
below_id: below_id.clone(),
start_y: event.position.y,
above_weight: self.panel_stack_weight(&above_id),
below_weight: self.panel_stack_weight(&below_id),
container_height: container_height.max(1.),
});
above_id,
below_id,
event.position.y,
container_height,
);
self.terminal.view.status = "resizing panel stack".to_string();
cx.notify();
}
@@ -343,24 +349,9 @@ impl NyaTermApp {
event: &MouseMoveEvent,
cx: &mut Context<Self>,
) {
let Some(state) = self.panel_stack_resize.clone() else {
return;
};
let delta_px = f32::from(event.position.y - state.start_y);
let pair = state.above_weight + state.below_weight;
if pair <= 0. || state.container_height <= 0. {
return;
if self.shell.panels.update_stack_resize(event.position.y) {
cx.notify();
}
let px_per_weight = state.container_height / pair;
let min_weight = (48. / px_per_weight).min(pair / 2.).max(0.05);
let next_above =
(state.above_weight + delta_px / px_per_weight).clamp(min_weight, pair - min_weight);
let next_below = pair - next_above;
self.panel_stack_sizes
.insert(state.above_id.clone(), next_above);
self.panel_stack_sizes
.insert(state.below_id.clone(), next_below);
cx.notify();
}
pub(in crate::features) fn finish_panel_stack_resize(
@@ -368,7 +359,7 @@ impl NyaTermApp {
_event: &MouseUpEvent,
cx: &mut Context<Self>,
) {
if self.panel_stack_resize.take().is_some() {
if self.shell.panels.finish_stack_resize() {
self.persist_ui_layout();
self.terminal.view.status = "panel stack sizes saved".to_string();
cx.notify();
@@ -376,11 +367,13 @@ impl NyaTermApp {
}
pub(in crate::features) fn sync_panel_stack_to_settings(&mut self) {
self.settings.ui_panel_multi_open = self.panel_multi_open;
self.settings.ui_left_open_panels = self.left_open_panels.clone();
self.settings.ui_right_open_panels = self.right_open_panels.clone();
self.settings.ui_panel_multi_open = self.shell.panels.multi_open;
self.settings.ui_left_open_panels = self.shell.panels.left_open.clone();
self.settings.ui_right_open_panels = self.shell.panels.right_open.clone();
self.settings.ui_panel_stack_sizes = self
.panel_stack_sizes
.shell
.panels
.stack_sizes
.iter()
.filter_map(|(key, value)| {
let scaled = (*value * 1000.).round();
@@ -390,29 +383,29 @@ impl NyaTermApp {
}
pub(in crate::features) fn apply_panel_stack_from_settings(&mut self) {
self.panel_multi_open = self.settings.ui_panel_multi_open;
self.left_open_panels = self.settings.ui_left_open_panels.clone();
self.right_open_panels = self.settings.ui_right_open_panels.clone();
self.panel_stack_sizes = self
self.shell.panels.multi_open = self.settings.ui_panel_multi_open;
self.shell.panels.left_open = self.settings.ui_left_open_panels.clone();
self.shell.panels.right_open = self.settings.ui_right_open_panels.clone();
self.shell.panels.stack_sizes = self
.settings
.ui_panel_stack_sizes
.iter()
.filter_map(|(key, value)| (*value > 0).then(|| (key.clone(), (*value as f32) / 1000.)))
.collect();
if self.panel_multi_open {
if self.left_open_panels.is_empty() {
if let Some(panel) = self.active_left_panel {
if self.shell.panels.multi_open {
if self.shell.panels.left_open.is_empty() {
if let Some(panel) = self.shell.panels.active_left {
let id = panel.persistence_id().to_string();
if Self::is_stackable_panel_id(&id) {
self.left_open_panels.push(id);
self.shell.panels.left_open.push(id);
}
}
}
if self.right_open_panels.is_empty() {
if let Some(panel) = self.active_right_panel {
if self.shell.panels.right_open.is_empty() {
if let Some(panel) = self.shell.panels.active_right {
let id = panel.persistence_id().to_string();
if Self::is_stackable_panel_id(&id) {
self.right_open_panels.push(id);
self.shell.panels.right_open.push(id);
}
}
}
@@ -433,14 +426,24 @@ impl NyaTermApp {
PanelSide::Left => self.current_left_panel(),
PanelSide::Right => self.current_right_panel(),
}
.or_else(|| self.activity_bar_layout.first_panel_on_side(side))
.or_else(|| {
self.shell
.chrome
.activity_bar_layout
.first_panel_on_side(side)
})
.unwrap_or(NavItem::Workspace);
self.single_side_panel(side, fallback, window, cx)
} else if open_ids.len() == 1 || !self.panel_multi_open {
} else if open_ids.len() == 1 || !self.shell.panels.multi_open {
let panel = open_ids
.first()
.and_then(|id| NavItem::from_persistence_id(id))
.or_else(|| self.activity_bar_layout.first_panel_on_side(side))
.or_else(|| {
self.shell
.chrome
.activity_bar_layout
.first_panel_on_side(side)
})
.unwrap_or(NavItem::Workspace);
self.single_side_panel(side, panel, window, cx)
} else {
@@ -701,7 +704,7 @@ impl NyaTermApp {
cx.listener(|this, _, _, cx| {
this.ai.history.open = false;
this.ai.panel.execution_menu_open = false;
this.settings_active_tab = SettingsTab::AiGeneral;
this.shell.navigation.settings.active_tab = SettingsTab::AiGeneral;
this.open_page(NavItem::Settings, cx);
}),
))
@@ -4,12 +4,44 @@
//! lifecycles that span those views so the composition root does not retain
//! independently mutable mirrors.
use gpui::Pixels;
use std::collections::{HashMap, HashSet};
use std::time::{Duration, Instant};
use crate::models::{BottomPanelMode, BottomPanelResizeState};
use gpui::{Pixels, ScrollHandle, WindowHandle};
use super::super::app_state::SettingsDraftSnapshot;
use super::super::settings_window::SettingsWindow;
use crate::models::{
ActivityBarContextMenuState, ActivityBarLayoutState, BottomPanelMode, BottomPanelResizeState,
HeaderStatusState, MainMode, NavItem, PanelResizeSide, PanelResizeState, PanelSide,
PanelStackResizeState, RightFocus, SettingsTab, TitleMenu, TitleMenuSubmenu, WorkspacePaneNode,
WorkspaceSplitResizeState, WorkspaceSplitState,
};
pub(in crate::features) struct ShellFeatureState {
pub bottom_panel: ShellBottomPanelState,
pub viewport: ShellViewportState,
pub navigation: ShellNavigationState,
pub panels: ShellPanelState,
pub chrome: ShellChromeState,
pub workspace: ShellWorkspaceState,
}
pub(in crate::features) struct ShellFeatureInit {
pub bottom_panel_mode: BottomPanelMode,
pub quick_commands_height: f32,
pub command_send_height: f32,
pub active_left_panel: Option<NavItem>,
pub active_right_panel: Option<NavItem>,
pub left_open_panels: Vec<String>,
pub right_open_panels: Vec<String>,
pub panel_stack_sizes: HashMap<String, f32>,
pub panel_multi_open: bool,
pub left_sidebar_collapsed: bool,
pub right_inspector_collapsed: bool,
pub left_panel_width: f32,
pub right_panel_width: f32,
pub activity_bar_layout: ActivityBarLayoutState,
}
pub(in crate::features) struct ShellBottomPanelState {
@@ -19,19 +51,145 @@ pub(in crate::features) struct ShellBottomPanelState {
pub resize: Option<BottomPanelResizeState>,
}
/// Window geometry and viewport-derived caches.
pub(in crate::features) struct ShellViewportState {
pub size: (f32, f32),
pub wallpaper_tile_dimensions: Option<(String, u32, u32)>,
pub last_change_at: Option<Instant>,
pub title_drag_active_until: Option<Instant>,
}
/// Top-level page navigation and the settings-page/window lifecycle.
pub(in crate::features) struct ShellNavigationState {
pub selected_nav: NavItem,
pub main_mode: MainMode,
pub settings: ShellSettingsNavigationState,
}
pub(in crate::features) struct ShellSettingsNavigationState {
pub active_tab: SettingsTab,
pub expanded_groups: HashSet<String>,
pub draft_snapshot: Option<SettingsDraftSnapshot>,
pub window: Option<WindowHandle<SettingsWindow>>,
pub window_open_pending: bool,
pub previous_left_collapsed: Option<bool>,
pub previous_right_collapsed: Option<bool>,
}
/// Side-panel selection, stack layout and resize interaction state.
pub(in crate::features) struct ShellPanelState {
pub active_left: Option<NavItem>,
pub active_right: Option<NavItem>,
pub left_open: Vec<String>,
pub right_open: Vec<String>,
pub stack_sizes: HashMap<String, f32>,
pub multi_open: bool,
pub right_focus: RightFocus,
pub left_collapsed: bool,
pub right_collapsed: bool,
pub mobile_left_open: bool,
pub mobile_right_open: bool,
pub left_width: f32,
pub right_width: f32,
pub resize: Option<PanelResizeState>,
pub stack_resize: Option<PanelStackResizeState>,
}
/// Activity bar, title menus, tab-strip menus and connection-failure chrome.
pub(in crate::features) struct ShellChromeState {
pub activity_bar_layout: ActivityBarLayoutState,
pub activity_bar_context_menu: Option<ActivityBarContextMenuState>,
pub title_menu_open: Option<TitleMenu>,
pub title_menu_submenu: Option<TitleMenuSubmenu>,
pub header_status: HeaderStatusState,
pub open_tabs_menu_open: bool,
pub new_session_menu_open: bool,
pub new_session_all_sessions_open: bool,
pub new_session_group_menu_path: Vec<String>,
pub session_tab_strip_scroll: ScrollHandle,
pub session_tab_scroll_into_view_pending: bool,
pub last_connect_failure_name: Option<String>,
pub last_connect_failure_error: Option<String>,
}
/// Global and per-tab pane trees for the workspace surface.
pub(in crate::features) struct ShellWorkspaceState {
pub split: Option<WorkspaceSplitState>,
pub split_resize: Option<WorkspaceSplitResizeState>,
pub pane_roots: HashMap<String, WorkspacePaneNode>,
pub tab_owner: HashMap<String, String>,
pub focused_terminal_leaf_id: Option<String>,
pub pane_layout_restored: bool,
}
impl ShellFeatureState {
pub(in crate::features) fn new(
bottom_panel_mode: BottomPanelMode,
quick_commands_height: f32,
command_send_height: f32,
) -> Self {
pub(in crate::features) fn new(init: ShellFeatureInit) -> Self {
Self {
bottom_panel: ShellBottomPanelState {
mode: bottom_panel_mode,
quick_commands_height,
command_send_height,
mode: init.bottom_panel_mode,
quick_commands_height: init.quick_commands_height,
command_send_height: init.command_send_height,
resize: None,
},
viewport: ShellViewportState {
size: (1280., 800.),
wallpaper_tile_dimensions: None,
last_change_at: None,
title_drag_active_until: None,
},
navigation: ShellNavigationState {
selected_nav: NavItem::Workspace,
main_mode: MainMode::Workspace,
settings: ShellSettingsNavigationState {
active_tab: SettingsTab::General,
expanded_groups: HashSet::from(["workspace".to_string()]),
draft_snapshot: None,
window: None,
window_open_pending: false,
previous_left_collapsed: None,
previous_right_collapsed: None,
},
},
panels: ShellPanelState {
active_left: init.active_left_panel,
active_right: init.active_right_panel,
left_open: init.left_open_panels,
right_open: init.right_open_panels,
stack_sizes: init.panel_stack_sizes,
multi_open: init.panel_multi_open,
right_focus: RightFocus::Default,
left_collapsed: init.left_sidebar_collapsed,
right_collapsed: init.right_inspector_collapsed,
mobile_left_open: false,
mobile_right_open: false,
left_width: init.left_panel_width,
right_width: init.right_panel_width,
resize: None,
stack_resize: None,
},
chrome: ShellChromeState {
activity_bar_layout: init.activity_bar_layout,
activity_bar_context_menu: None,
title_menu_open: None,
title_menu_submenu: None,
header_status: HeaderStatusState::default(),
open_tabs_menu_open: false,
new_session_menu_open: false,
new_session_all_sessions_open: false,
new_session_group_menu_path: Vec::new(),
session_tab_strip_scroll: ScrollHandle::new(),
session_tab_scroll_into_view_pending: false,
last_connect_failure_name: None,
last_connect_failure_error: None,
},
workspace: ShellWorkspaceState {
split: None,
split_resize: None,
pane_roots: HashMap::new(),
tab_owner: HashMap::new(),
focused_terminal_leaf_id: None,
pane_layout_restored: false,
},
}
}
}
@@ -77,16 +235,225 @@ impl ShellBottomPanelState {
}
}
impl ShellViewportState {
pub(in crate::features) fn update_size(&mut self, size: (f32, f32), now: Instant) -> bool {
if self.size == size {
return false;
}
self.size = size;
self.last_change_at = Some(now);
true
}
pub(in crate::features) fn mark_title_drag(&mut self, now: Instant, hold: Duration) {
self.title_drag_active_until = Some(now + hold);
}
pub(in crate::features) fn title_drag_active(&self, now: Instant) -> bool {
self.title_drag_active_until
.is_some_and(|until| now < until)
}
}
impl ShellPanelState {
const LEFT_WIDTH_MIN: f32 = 160.;
const LEFT_WIDTH_MAX: f32 = 720.;
const RIGHT_WIDTH_MIN: f32 = 200.;
const RIGHT_WIDTH_MAX: f32 = 720.;
pub(in crate::features) fn start_resize(&mut self, side: PanelResizeSide, start_x: Pixels) {
let start_width = match side {
PanelResizeSide::Left => self.left_width,
PanelResizeSide::Right => self.right_width,
};
self.resize = Some(PanelResizeState {
side,
start_x,
start_width: gpui::px(start_width),
});
}
pub(in crate::features) fn update_resize(
&mut self,
current_x: Pixels,
) -> Option<(PanelResizeSide, f32)> {
let state = self.resize?;
let delta = f32::from(current_x - state.start_x);
let start = f32::from(state.start_width);
let width = match state.side {
PanelResizeSide::Left => {
self.left_width = (start + delta).clamp(Self::LEFT_WIDTH_MIN, Self::LEFT_WIDTH_MAX);
self.left_width
}
PanelResizeSide::Right => {
self.right_width =
(start - delta).clamp(Self::RIGHT_WIDTH_MIN, Self::RIGHT_WIDTH_MAX);
self.right_width
}
};
Some((state.side, width))
}
pub(in crate::features) fn finish_resize(&mut self) -> bool {
self.resize.take().is_some()
}
pub(in crate::features) fn stack_weight(&self, panel_id: &str) -> f32 {
self.stack_sizes
.get(panel_id)
.copied()
.filter(|value| value.is_finite() && *value > 0.)
.unwrap_or(1.)
}
pub(in crate::features) fn start_stack_resize(
&mut self,
side: PanelSide,
above_id: String,
below_id: String,
start_y: Pixels,
container_height: f32,
) {
self.stack_resize = Some(PanelStackResizeState {
side,
above_weight: self.stack_weight(&above_id),
below_weight: self.stack_weight(&below_id),
above_id,
below_id,
start_y,
container_height: container_height.max(1.),
});
}
pub(in crate::features) fn update_stack_resize(&mut self, current_y: Pixels) -> bool {
let Some(state) = self.stack_resize.as_ref() else {
return false;
};
let delta_px = f32::from(current_y - state.start_y);
let pair = state.above_weight + state.below_weight;
if pair <= 0. || state.container_height <= 0. {
return false;
}
let px_per_weight = state.container_height / pair;
let min_weight = (48. / px_per_weight).min(pair / 2.).max(0.05);
let next_above =
(state.above_weight + delta_px / px_per_weight).clamp(min_weight, pair - min_weight);
let next_below = pair - next_above;
self.stack_sizes.insert(state.above_id.clone(), next_above);
self.stack_sizes.insert(state.below_id.clone(), next_below);
true
}
pub(in crate::features) fn finish_stack_resize(&mut self) -> bool {
self.stack_resize.take().is_some()
}
}
impl ShellChromeState {
pub(in crate::features) fn prepare_session_switch(&mut self) {
self.open_tabs_menu_open = false;
self.close_new_session_menu();
self.session_tab_scroll_into_view_pending = true;
}
pub(in crate::features) fn toggle_open_tabs_menu(&mut self) {
self.open_tabs_menu_open = !self.open_tabs_menu_open;
if self.open_tabs_menu_open {
self.close_new_session_menu();
self.title_menu_open = None;
}
}
pub(in crate::features) fn close_open_tabs_menu(&mut self) -> bool {
std::mem::take(&mut self.open_tabs_menu_open)
}
pub(in crate::features) fn toggle_new_session_menu(&mut self) {
self.new_session_menu_open = !self.new_session_menu_open;
if self.new_session_menu_open {
self.open_tabs_menu_open = false;
self.title_menu_open = None;
}
self.new_session_all_sessions_open = false;
self.new_session_group_menu_path.clear();
}
pub(in crate::features) fn close_new_session_menu(&mut self) -> bool {
let changed = self.new_session_menu_open
|| self.new_session_all_sessions_open
|| !self.new_session_group_menu_path.is_empty();
self.new_session_menu_open = false;
self.new_session_all_sessions_open = false;
self.new_session_group_menu_path.clear();
changed
}
}
impl ShellWorkspaceState {
pub(in crate::features) fn rebuild_tab_owners(&mut self) {
let mut owners = HashMap::new();
for (tab_root, tree) in &self.pane_roots {
for leaf in tree.session_ids() {
owners.insert(leaf, tab_root.clone());
}
}
self.tab_owner = owners;
}
pub(in crate::features) fn replace_session_id(&mut self, old_id: &str, new_id: &str) {
for root in self.pane_roots.values_mut() {
root.replace_session_id(old_id, new_id);
}
if let Some(root) = self.pane_roots.remove(old_id) {
self.pane_roots.insert(new_id.to_string(), root);
}
if let Some(root) = self.split.as_mut() {
root.replace_session_id(old_id, new_id);
}
self.rebuild_tab_owners();
}
pub(in crate::features) fn remove_session(&mut self, session_id: &str) {
self.tab_owner.remove(session_id);
self.pane_roots.remove(session_id);
}
}
#[cfg(test)]
mod tests {
use std::collections::HashMap;
use std::time::{Duration, Instant};
use gpui::px;
use super::ShellFeatureState;
use crate::models::BottomPanelMode;
use super::{ShellFeatureInit, ShellFeatureState};
use crate::models::{
ActivityBarLayoutState, BottomPanelMode, PanelResizeSide, PanelSide, WorkspacePaneNode,
WorkspaceSplitDirection,
};
fn shell(mode: BottomPanelMode) -> ShellFeatureState {
ShellFeatureState::new(ShellFeatureInit {
bottom_panel_mode: mode,
quick_commands_height: 120.,
command_send_height: 180.,
active_left_panel: None,
active_right_panel: None,
left_open_panels: Vec::new(),
right_open_panels: Vec::new(),
panel_stack_sizes: HashMap::new(),
panel_multi_open: false,
left_sidebar_collapsed: true,
right_inspector_collapsed: true,
left_panel_width: 240.,
right_panel_width: 320.,
activity_bar_layout: ActivityBarLayoutState::default(),
})
}
#[test]
fn bottom_panel_resize_updates_only_the_mode_that_started_the_drag() {
let mut shell = ShellFeatureState::new(BottomPanelMode::QuickCommands, 120., 180.);
let mut shell = shell(BottomPanelMode::QuickCommands);
assert!(shell.bottom_panel.start_resize(px(400.)));
shell.bottom_panel.mode = BottomPanelMode::CommandSend;
@@ -99,9 +466,109 @@ mod tests {
#[test]
fn hidden_bottom_panel_does_not_start_resize() {
let mut shell = ShellFeatureState::new(BottomPanelMode::Hidden, 120., 180.);
let mut shell = shell(BottomPanelMode::Hidden);
assert!(!shell.bottom_panel.start_resize(px(400.)));
assert!(shell.bottom_panel.resize.is_none());
}
#[test]
fn panel_resize_clamps_each_side_and_finishes_once() {
let mut shell = shell(BottomPanelMode::Hidden);
shell.panels.start_resize(PanelResizeSide::Left, px(100.));
assert_eq!(
shell.panels.update_resize(px(-100.)),
Some((PanelResizeSide::Left, 160.))
);
assert!(shell.panels.finish_resize());
assert!(!shell.panels.finish_resize());
shell.panels.start_resize(PanelResizeSide::Right, px(100.));
assert_eq!(
shell.panels.update_resize(px(-500.)),
Some((PanelResizeSide::Right, 720.))
);
}
#[test]
fn panel_stack_resize_preserves_pair_weight() {
let mut shell = shell(BottomPanelMode::Hidden);
shell.panels.stack_sizes.insert("above".to_string(), 2.);
shell.panels.stack_sizes.insert("below".to_string(), 1.);
shell.panels.start_stack_resize(
PanelSide::Left,
"above".to_string(),
"below".to_string(),
px(100.),
300.,
);
assert!(shell.panels.update_stack_resize(px(150.)));
let total = shell.panels.stack_sizes["above"] + shell.panels.stack_sizes["below"];
assert!((total - 3.).abs() < f32::EPSILON);
assert!(shell.panels.finish_stack_resize());
}
#[test]
fn chrome_menu_transitions_are_mutually_exclusive() {
let mut shell = shell(BottomPanelMode::Hidden);
shell.chrome.new_session_menu_open = true;
shell.chrome.new_session_all_sessions_open = true;
shell
.chrome
.new_session_group_menu_path
.push("group".to_string());
shell.chrome.toggle_open_tabs_menu();
assert!(shell.chrome.open_tabs_menu_open);
assert!(!shell.chrome.new_session_menu_open);
assert!(!shell.chrome.new_session_all_sessions_open);
assert!(shell.chrome.new_session_group_menu_path.is_empty());
shell.chrome.toggle_new_session_menu();
assert!(!shell.chrome.open_tabs_menu_open);
assert!(shell.chrome.new_session_menu_open);
}
#[test]
fn viewport_tracks_only_real_geometry_changes_and_title_drag_deadline() {
let mut shell = shell(BottomPanelMode::Hidden);
let now = Instant::now();
assert!(!shell.viewport.update_size((1280., 800.), now));
assert!(shell.viewport.update_size((1024., 768.), now));
assert_eq!(shell.viewport.last_change_at, Some(now));
shell
.viewport
.mark_title_drag(now, Duration::from_millis(10));
assert!(shell.viewport.title_drag_active(now));
assert!(
!shell
.viewport
.title_drag_active(now + Duration::from_millis(10))
);
}
#[test]
fn workspace_rebuilds_and_renames_tab_ownership() {
let mut shell = shell(BottomPanelMode::Hidden);
shell.workspace.pane_roots.insert(
"root".to_string(),
WorkspacePaneNode::Split {
id: "split".to_string(),
direction: WorkspaceSplitDirection::Vertical,
ratio_percent: 50,
first: Box::new(WorkspacePaneNode::leaf("root".to_string())),
second: Box::new(WorkspacePaneNode::leaf("leaf".to_string())),
},
);
shell.workspace.rebuild_tab_owners();
assert_eq!(shell.workspace.tab_owner["leaf"], "root");
shell.workspace.replace_session_id("root", "renamed");
assert!(shell.workspace.pane_roots.contains_key("renamed"));
assert_eq!(shell.workspace.tab_owner["leaf"], "renamed");
assert_eq!(shell.workspace.tab_owner["renamed"], "renamed");
}
}
@@ -25,7 +25,7 @@ impl NyaTermApp {
.collect::<Vec<_>>();
if live_ids.is_empty() {
self.terminal.windows.tree = None;
self.focused_terminal_window_leaf_id = None;
self.shell.workspace.focused_terminal_leaf_id = None;
return;
}
@@ -44,7 +44,7 @@ impl NyaTermApp {
}
if let Some(root) = self.terminal.windows.tree.as_mut() {
let preferred = self.focused_terminal_window_leaf_id.clone();
let preferred = self.shell.workspace.focused_terminal_leaf_id.clone();
for tab_id in &live_ids {
root.ensure_tab(tab_id, preferred.as_deref());
}
@@ -52,11 +52,13 @@ impl NyaTermApp {
let _ = root.set_active_tab(&active);
}
if self
.focused_terminal_window_leaf_id
.shell
.workspace
.focused_terminal_leaf_id
.as_ref()
.is_none_or(|id| !root.leaf_ids().iter().any(|leaf| leaf == id))
{
self.focused_terminal_window_leaf_id = root.first_leaf_id();
self.shell.workspace.focused_terminal_leaf_id = root.first_leaf_id();
}
}
}
@@ -75,7 +77,7 @@ impl NyaTermApp {
}
let active = self.session.active_id.clone();
let root = TerminalWindowNode::leaf(tab_ids, active);
self.focused_terminal_window_leaf_id = root.first_leaf_id();
self.shell.workspace.focused_terminal_leaf_id = root.first_leaf_id();
self.terminal.windows.tree = Some(root);
}
@@ -90,10 +92,10 @@ impl NyaTermApp {
let _ = set_leaf_active(root, &leaf_id, &session_id);
let _ = root.set_active_tab(&session_id);
}
self.focused_terminal_window_leaf_id = Some(leaf_id);
self.shell.workspace.focused_terminal_leaf_id = Some(leaf_id);
self.activate_session_id_with_surface_sync(&session_id, cx);
self.selected_nav = NavItem::Workspace;
self.main_mode = MainMode::Workspace;
self.shell.navigation.selected_nav = NavItem::Workspace;
self.shell.navigation.main_mode = MainMode::Workspace;
cx.notify();
}
@@ -115,7 +117,7 @@ impl NyaTermApp {
};
let _ = root.set_active_tab(&tab_id);
if let Some(leaf_id) = find_leaf_with_tab(root, &tab_id) {
self.focused_terminal_window_leaf_id = Some(leaf_id);
self.shell.workspace.focused_terminal_leaf_id = Some(leaf_id);
}
}
@@ -131,7 +133,7 @@ impl NyaTermApp {
};
if root.place_tab_before(&tab_id, &before_tab_id) {
let _ = root.set_active_tab(&tab_id);
self.focused_terminal_window_leaf_id =
self.shell.workspace.focused_terminal_leaf_id =
find_leaf_with_tab(root, &tab_id).or_else(|| root.first_leaf_id());
self.activate_session_id_with_surface_sync(&tab_id, cx);
self.terminal.view.status = format!(
@@ -188,11 +190,11 @@ impl NyaTermApp {
return;
}
let _ = root.set_active_tab(&tab_id);
self.focused_terminal_window_leaf_id =
self.shell.workspace.focused_terminal_leaf_id =
find_leaf_with_tab(root, &tab_id).or_else(|| root.first_leaf_id());
self.activate_session_id_with_surface_sync(&tab_id, cx);
self.selected_nav = NavItem::Workspace;
self.main_mode = MainMode::Workspace;
self.shell.navigation.selected_nav = NavItem::Workspace;
self.shell.navigation.main_mode = MainMode::Workspace;
let zone_label = match zone {
TabDockZone::Center => "merged into leaf".to_string(),
TabDockZone::Edge(edge) => format!("split to {}", edge.label()),
@@ -224,20 +226,20 @@ impl NyaTermApp {
return;
};
// Clear global pane splits so multi-leaf rendering takes precedence cleanly.
self.workspace_split = None;
self.workspace_split_resize = None;
self.shell.workspace.split = None;
self.shell.workspace.split_resize = None;
if let Some(active) = self.session.active_id.clone() {
let mut root = layout;
let _ = root.set_active_tab(&active);
self.focused_terminal_window_leaf_id =
self.shell.workspace.focused_terminal_leaf_id =
find_leaf_with_tab(&root, &active).or_else(|| root.first_leaf_id());
self.terminal.windows.tree = Some(root);
} else {
self.focused_terminal_window_leaf_id = layout.first_leaf_id();
self.shell.workspace.focused_terminal_leaf_id = layout.first_leaf_id();
self.terminal.windows.tree = Some(layout);
}
self.selected_nav = NavItem::Workspace;
self.main_mode = MainMode::Workspace;
self.shell.navigation.selected_nav = NavItem::Workspace;
self.shell.navigation.main_mode = MainMode::Workspace;
self.terminal.view.status = format!("applied {}", mode.label().to_ascii_lowercase());
self.persist_terminal_window_layout();
// Global pane layout is obsolete while multi-leaf is active.
@@ -296,11 +298,11 @@ impl NyaTermApp {
if !matches!(restored, TerminalWindowNode::Split { .. }) {
return;
}
self.focused_terminal_window_leaf_id = restored.first_leaf_id();
self.shell.workspace.focused_terminal_leaf_id = restored.first_leaf_id();
if let Some(active) = self.session.active_id.clone() {
let mut root = restored;
let _ = root.set_active_tab(&active);
self.focused_terminal_window_leaf_id =
self.shell.workspace.focused_terminal_leaf_id =
find_leaf_with_tab(&root, &active).or_else(|| root.first_leaf_id());
self.terminal.windows.tree = Some(root);
} else {
@@ -23,10 +23,10 @@ impl NyaTermApp {
pub(in crate::features) fn prune_workspace_split(&mut self) {
let live_ids = self.live_session_ids_with_disconnected();
let before_roots = self.session_pane_roots.clone();
let root_keys: Vec<String> = self.session_pane_roots.keys().cloned().collect();
let before_roots = self.shell.workspace.pane_roots.clone();
let root_keys: Vec<String> = self.shell.workspace.pane_roots.keys().cloned().collect();
for tab_root in root_keys {
let Some(root) = self.session_pane_roots.remove(&tab_root) else {
let Some(root) = self.shell.workspace.pane_roots.remove(&tab_root) else {
continue;
};
match root.prune(&live_ids) {
@@ -41,7 +41,7 @@ impl NyaTermApp {
.next()
.unwrap_or_else(|| tab_root.clone())
};
self.session_pane_roots.insert(key, node);
self.shell.workspace.pane_roots.insert(key, node);
}
// Single leaf collapses to no stored tree for this tab.
}
@@ -49,13 +49,13 @@ impl NyaTermApp {
}
}
// Also prune legacy workspace_split if roots empty (migration path).
if self.session_pane_roots.is_empty() {
if let Some(root) = self.workspace_split.take() {
if self.shell.workspace.pane_roots.is_empty() {
if let Some(root) = self.shell.workspace.split.take() {
match root.prune(&live_ids) {
Some(node) => {
if node.is_split() {
if let Some(first) = node.session_ids().into_iter().next() {
self.session_pane_roots.insert(first.clone(), node);
self.shell.workspace.pane_roots.insert(first.clone(), node);
}
} else if let WorkspacePaneNode::Leaf { session_id } = node {
if self.session.active_id.is_none() {
@@ -69,7 +69,7 @@ impl NyaTermApp {
}
self.rebuild_session_tab_owners();
self.sync_workspace_split_from_active_tab();
if self.session_pane_roots != before_roots {
if self.shell.workspace.pane_roots != before_roots {
self.persist_workspace_pane_layout();
if self.startup_restore_complete {
self.persist_open_tabs();
@@ -89,29 +89,21 @@ impl NyaTermApp {
/// Rebuild leaf→tab-root ownership from `session_pane_roots`.
pub(in crate::features) fn rebuild_session_tab_owners(&mut self) {
self.session_tab_owner.clear();
let roots: Vec<(String, WorkspacePaneNode)> = self
.session_pane_roots
.iter()
.map(|(k, v)| (k.clone(), v.clone()))
.collect();
for (tab_root, tree) in roots {
for leaf in tree.session_ids() {
self.session_tab_owner.insert(leaf, tab_root.clone());
}
}
self.shell.workspace.rebuild_tab_owners();
}
/// Expose the active tab's pane tree via `workspace_split` for existing renderers.
pub(in crate::features) fn sync_workspace_split_from_active_tab(&mut self) {
let Some(active) = self.session.active_id.clone() else {
self.workspace_split = None;
self.shell.workspace.split = None;
self.sync_terminal_frame_snapshot_priority();
return;
};
let tab_root = self.tab_root_for_session(&active);
self.workspace_split = self
.session_pane_roots
self.shell.workspace.split = self
.shell
.workspace
.pane_roots
.get(&tab_root)
.filter(|root| root.is_split())
.cloned();
@@ -123,9 +115,9 @@ impl NyaTermApp {
return;
};
let tab_root = self.tab_root_for_session(&active);
if let Some(root) = self.workspace_split.clone() {
if let Some(root) = self.shell.workspace.split.clone() {
if root.is_split() {
self.session_pane_roots.insert(tab_root, root);
self.shell.workspace.pane_roots.insert(tab_root, root);
self.rebuild_session_tab_owners();
}
}
@@ -139,7 +131,7 @@ impl NyaTermApp {
) {
let split_id = uuid();
let tab_root = self.tab_root_for_session(&primary_session_id);
if let Some(root) = self.session_pane_roots.get_mut(&tab_root) {
if let Some(root) = self.shell.workspace.pane_roots.get_mut(&tab_root) {
if root.split_leaf(
&primary_session_id,
secondary_session_id.clone(),
@@ -149,8 +141,8 @@ impl NyaTermApp {
self.rebuild_session_tab_owners();
self.activate_session_id(&secondary_session_id);
self.sync_workspace_split_from_active_tab();
self.selected_nav = NavItem::Workspace;
self.main_mode = MainMode::Workspace;
self.shell.navigation.selected_nav = NavItem::Workspace;
self.shell.navigation.main_mode = MainMode::Workspace;
self.persist_workspace_pane_layout();
if self.startup_restore_complete {
self.persist_open_tabs();
@@ -166,12 +158,12 @@ impl NyaTermApp {
first: Box::new(WorkspacePaneNode::leaf(primary_session_id.clone())),
second: Box::new(WorkspacePaneNode::leaf(secondary_session_id.clone())),
};
self.session_pane_roots.insert(tab_root, root);
self.shell.workspace.pane_roots.insert(tab_root, root);
self.rebuild_session_tab_owners();
self.activate_session_id(&secondary_session_id);
self.sync_workspace_split_from_active_tab();
self.selected_nav = NavItem::Workspace;
self.main_mode = MainMode::Workspace;
self.shell.navigation.selected_nav = NavItem::Workspace;
self.shell.navigation.main_mode = MainMode::Workspace;
self.persist_workspace_pane_layout();
if self.startup_restore_complete {
self.persist_open_tabs();
@@ -188,8 +180,8 @@ impl NyaTermApp {
}
self.activate_session_id_with_surface_sync(&session_id, cx);
self.sync_workspace_split_from_active_tab();
self.selected_nav = NavItem::Workspace;
self.main_mode = MainMode::Workspace;
self.shell.navigation.selected_nav = NavItem::Workspace;
self.shell.navigation.main_mode = MainMode::Workspace;
self.terminal.view.status = format!("focused pane {}", short_id(&session_id));
cx.notify();
}
@@ -241,18 +233,18 @@ impl NyaTermApp {
return;
};
let tab_root = self.tab_root_for_session(&active_id);
let Some(root) = self.session_pane_roots.remove(&tab_root) else {
self.workspace_split = None;
let Some(root) = self.shell.workspace.pane_roots.remove(&tab_root) else {
self.shell.workspace.split = None;
self.terminal.view.status = "workspace is not split".to_string();
cx.notify();
return;
};
self.workspace_split_resize = None;
self.shell.workspace.split_resize = None;
if let Some(collapsed) = collapse_around_session(root.clone(), &active_id) {
match collapsed {
WorkspacePaneNode::Split { .. } => {
self.session_pane_roots.insert(tab_root, collapsed);
self.shell.workspace.pane_roots.insert(tab_root, collapsed);
self.terminal.view.status = "collapsed focused split".to_string();
}
WorkspacePaneNode::Leaf { session_id } => {
@@ -295,7 +287,7 @@ impl NyaTermApp {
) {
(Some(direction), Some(start_ratio)) => (direction, start_ratio),
_ => {
let Some(root) = self.workspace_split.as_ref() else {
let Some(root) = self.shell.workspace.split.as_ref() else {
return;
};
let Some(direction) = root.direction_for_split(&split_id) else {
@@ -308,7 +300,7 @@ impl NyaTermApp {
}
}
} else {
let Some(root) = self.workspace_split.as_ref() else {
let Some(root) = self.shell.workspace.split.as_ref() else {
return;
};
let Some(direction) = root.direction_for_split(&split_id) else {
@@ -323,7 +315,7 @@ impl NyaTermApp {
WorkspaceSplitDirection::Horizontal => event.position.y,
WorkspaceSplitDirection::Vertical => event.position.x,
};
self.workspace_split_resize = Some(WorkspaceSplitResizeState {
self.shell.workspace.split_resize = Some(WorkspaceSplitResizeState {
split_id,
direction,
start_pos,
@@ -339,7 +331,7 @@ impl NyaTermApp {
event: &MouseMoveEvent,
cx: &mut Context<Self>,
) {
let Some(state) = self.workspace_split_resize.clone() else {
let Some(state) = self.shell.workspace.split_resize.clone() else {
return;
};
let current = match state.direction {
@@ -365,7 +357,7 @@ impl NyaTermApp {
}
}
if !applied {
if let Some(root) = self.workspace_split.as_mut() {
if let Some(root) = self.shell.workspace.split.as_mut() {
if root.set_ratio_for_split(&state.split_id, next) {
applied = true;
self.write_back_active_tab_pane_root();
@@ -383,7 +375,7 @@ impl NyaTermApp {
_event: &MouseUpEvent,
cx: &mut Context<Self>,
) {
if let Some(state) = self.workspace_split_resize.take() {
if let Some(state) = self.shell.workspace.split_resize.take() {
let ratio = self
.terminal
.windows
@@ -391,7 +383,9 @@ impl NyaTermApp {
.as_ref()
.and_then(|root| root.ratio_for_split(&state.split_id))
.or_else(|| {
self.workspace_split
self.shell
.workspace
.split
.as_ref()
.and_then(|root| root.ratio_for_split(&state.split_id))
});
@@ -401,7 +395,9 @@ impl NyaTermApp {
if self.terminal_windows_is_multi_leaf() {
self.persist_terminal_window_layout();
} else if self
.workspace_split
.shell
.workspace
.split
.as_ref()
.is_some_and(|root| root.is_split())
{
@@ -472,12 +468,16 @@ impl NyaTermApp {
.map(|session| session.id)
.collect::<Vec<_>>();
let layout = self
.workspace_split
.shell
.workspace
.split
.as_ref()
.filter(|root| root.is_split())
.and_then(|root| root.serialize_layout(&ordered))
.or_else(|| {
self.session_pane_roots
self.shell
.workspace
.pane_roots
.values()
.find(|root| root.is_split())
.and_then(|root| root.serialize_layout(&ordered))
@@ -496,16 +496,16 @@ impl NyaTermApp {
}
pub(in crate::features) fn try_restore_workspace_pane_layout(&mut self) {
if self.workspace_pane_layout_restored {
if self.shell.workspace.pane_layout_restored {
return;
}
if !self.settings.startup_restore || !self.settings.startup_restore_window_layout {
self.workspace_pane_layout_restored = true;
self.shell.workspace.pane_layout_restored = true;
return;
}
// Multi-leaf tab windows take visual precedence; skip pane restore when active.
if self.terminal_windows_is_multi_leaf() {
self.workspace_pane_layout_restored = true;
self.shell.workspace.pane_layout_restored = true;
return;
}
let ordered = self
@@ -516,11 +516,11 @@ impl NyaTermApp {
if ordered.len() < 2 {
// After startup finishes, don't keep waiting forever for a second tab.
if self.startup_restore_complete {
self.workspace_pane_layout_restored = true;
self.shell.workspace.pane_layout_restored = true;
}
return;
}
self.workspace_pane_layout_restored = true;
self.shell.workspace.pane_layout_restored = true;
let Ok(store) = ConnectionStore::open_with_portable_key_path(
self.runtime.config_dir(),
self.runtime.portable_key_path().map(ToOwned::to_owned),
@@ -548,12 +548,12 @@ impl NyaTermApp {
}
// Install as a per-tab root under the first leaf (legacy global layout path).
if let Some(first) = restored.session_ids().into_iter().next() {
self.session_pane_roots.insert(first, restored);
self.shell.workspace.pane_roots.insert(first, restored);
self.rebuild_session_tab_owners();
self.sync_workspace_split_from_active_tab();
}
self.selected_nav = NavItem::Workspace;
self.main_mode = MainMode::Workspace;
self.shell.navigation.selected_nav = NavItem::Workspace;
self.shell.navigation.main_mode = MainMode::Workspace;
self.terminal.view.status = "restored workspace pane layout".to_string();
}
}
@@ -27,7 +27,7 @@ impl NyaTermApp {
|| (!self.settings.has_master_password
&& self.settings_master_password_draft.is_empty()))
{
self.settings_active_tab = SettingsTab::Security;
self.shell.navigation.settings.active_tab = SettingsTab::Security;
self.cloud_sync.status =
"configure a master password before enabling cloud sync".to_string();
self.terminal.view.status = self.cloud_sync.status.clone();
@@ -14,7 +14,7 @@ impl NyaTermApp {
let Some(menu) = self.terminal.menus.action_link_menu.clone() else {
return div().into_any_element();
};
let (viewport_w, viewport_h) = self.last_viewport_size;
let (viewport_w, viewport_h) = self.shell.viewport.size;
let (menu_x, menu_y) = clamp_menu_position(
f32::from(menu.x),
f32::from(menu.y),
@@ -143,7 +143,7 @@ impl NyaTermApp {
};
let _ = selection_link_kind;
let (viewport_w, viewport_h) = self.last_viewport_size;
let (viewport_w, viewport_h) = self.shell.viewport.size;
// Approximate height from max-h for clamp (scrollable menus can be shorter).
let (menu_x, menu_y) = clamp_menu_position(
f32::from(menu.x),
@@ -890,7 +890,7 @@ impl NyaTermApp {
}
fn terminal_session_has_visible_surface(&self, session_id: &str) -> bool {
if session_id.is_empty() || self.main_mode != MainMode::Workspace {
if session_id.is_empty() || self.shell.navigation.main_mode != MainMode::Workspace {
return false;
}
self.visible_terminal_session_ids()
@@ -899,7 +899,7 @@ impl NyaTermApp {
}
pub(in crate::features) fn visible_terminal_session_ids(&self) -> Vec<&str> {
if self.main_mode != MainMode::Workspace {
if self.shell.navigation.main_mode != MainMode::Workspace {
return Vec::new();
}
if let Some(root) = self.terminal.windows.tree.as_ref()
@@ -907,7 +907,7 @@ impl NyaTermApp {
{
return terminal_window_node_visible_tab_ids(root);
}
if let Some(root) = self.workspace_split.as_ref() {
if let Some(root) = self.shell.workspace.split.as_ref() {
return workspace_pane_node_visible_session_ids(root);
}
self.session.active_id.iter().map(String::as_str).collect()
@@ -55,7 +55,7 @@ impl NyaTermApp {
// Tauri: closing a strip tab closes the whole tab tree; closing a secondary leaf
// only removes that pane. Strip close uses the tab-root id.
let close_ids = if !self.is_secondary_pane_session(&session_id) {
if let Some(root) = self.session_pane_roots.get(&session_id) {
if let Some(root) = self.shell.workspace.pane_roots.get(&session_id) {
root.session_ids()
} else {
vec![session_id.clone()]
@@ -277,7 +277,9 @@ impl NyaTermApp {
{
return window_root.active_tabs();
}
self.workspace_split
self.shell
.workspace
.split
.as_ref()
.map(|root| root.session_ids())
.or_else(|| self.session.active_id.clone().map(|id| vec![id]))
@@ -189,7 +189,7 @@ impl NyaTermApp {
}
let Some(config) = self.session.active_ssh_config.clone() else {
self.terminal.view.status = "start an SSH session first".to_string();
self.selected_nav = NavItem::Transfers;
self.shell.navigation.selected_nav = NavItem::Transfers;
cx.notify();
return;
};
+22 -12
View File
@@ -9,7 +9,7 @@ Last updated from the working tree on 2026-07-28.
| Metric | Current value | Notes |
| --- | ---: | --- |
| `NyaTermApp` fields | 102 | Counted from `features/app_state/mod.rs`; down from 585, still transitional. |
| `NyaTermApp` fields | 55 | Counted from `features/app_state/mod.rs`; down from 585, still transitional. |
| `impl NyaTermApp` blocks | 238 | Spread across 233 files under `crates/nyaterm-desktop/src`. |
| `#[path = "..."]` declarations in desktop | 0 | Cleared. Every directory is a real module; the boundary script fails on any new occurrence. |
| `use super::*` imports in desktop | 0 | Cleared in production and test modules; guarded crate-wide. |
@@ -331,11 +331,17 @@ these as staged extraction candidates, not as formatting-only refactor targets.
retains session metadata/host queries, status messages, focus and redraw
coordination. This state remains transient and changes no session or settings
persistence format.
- `ShellFeatureState` is the new owner for cross-view shell interaction state,
starting with the bottom-panel mode, quick-command/send-command heights and
resize lifecycle. Render helpers stay on views, and `NyaTermApp` still owns
settings persistence after a drag completes. This creates the boundary for a
later cohesive shell-chrome cut without introducing a mirror.
- `ShellFeatureState` now owns the complete cross-view window interaction
cluster. Its `viewport`, `navigation`, `panels`, `chrome` and `workspace`
children contain geometry bookkeeping, settings navigation/window state,
side-panel stacks and resize lifecycles, title/tab menus and per-tab pane
ownership respectively; the existing `bottom_panel` child still owns its
mode, heights and drag state. Forty-seven top-level app fields became these
five focused children, so `NyaTermApp` dropped from 102 fields to 55 without
creating a writable mirror. Pure viewport, panel resize/stack resize, chrome
menu and workspace ownership transitions execute on the child states.
Render helpers remain on views, while `NyaTermApp` retains settings
persistence, GPUI notification, terminal coordination and event routing.
- The Entity Store projection layer is gone entirely, in two steps.
First, the six domain stores (`Ai`, `CloudSync`, `Connections`, `RemoteOps`,
@@ -1414,7 +1420,7 @@ honest remaining list.
compiler-confirmed final pass also removed `features/prelude.rs`, so modules
cannot regain the same implicit dependency surface through a shared import
bucket.
3. Largely done. `NyaTermApp` is down from 585 fields to 102, across seventeen
3. Largely done. `NyaTermApp` is down from 585 fields to 55, across seventeen
feature-state structs. The latest cohesive cuts moved sixteen terminal
command-assistance and credential-prompt fields into
`TerminalFeatureState::assist`, then seventeen transient settings fields
@@ -1424,9 +1430,11 @@ honest remaining list.
conflict and GitHub device-flow state, recording and SSH-tunnel runtime
resources with their job/UI lifecycle state, then the complete live session
runtime with nested session-start, prompt and dialog ownership, then terminal
presentation runtime followed by sync-input, screen-lock and bottom-panel
interaction lifecycles. What is left is a long tail, and much of it is
genuinely app-level (stores, runtime, services, persisted collections).
presentation runtime followed by sync-input and screen-lock interaction
lifecycles, and finally forty-seven window interaction fields under the
shell's viewport, navigation, panel, chrome and workspace children. What is
left is a long tail, and much of it is genuinely app-level (stores, runtime,
services, persisted collections).
Group by cohesion where a cluster exists; do not force the count down for
its own sake.
Method ownership is now moving too, which is what grouping the fields alone
@@ -1434,7 +1442,7 @@ honest remaining list.
it belongs on that state, and the `NyaTermApp` method becomes a forwarder
that owns `cx.notify()`. That is enforced by the type system rather than by
convention — a handler taking `&mut TransferBrowserState` cannot reach the
session list no matter what a later edit tries. Eighty-four methods or
session list no matter what a later edit tries. More than one hundred methods or
self-contained transitions have moved this way across transfers, security,
the shell, sync input, the send command bar, AI, quick commands, cloud sync,
recording, session starts and terminal paste review;
@@ -1445,7 +1453,9 @@ honest remaining list.
of sync, closing a pending session start cannot update its maps without also
applying the active pending/failed fallback rules, and paste editing cannot
mutate its UTF-8 cursor without also clearing stale selection/IME state.
Those are the kinds of signals to look for.
Shell viewport timing, panel drags, mutually-exclusive tab menus and pane
ownership rebuilding now have the same property. Those are the kinds of
signals to look for.
Two caveats worth keeping. Render helpers stay on the view even when they
read one state — moving element construction onto a data struct trades one
coupling for a worse one. And a method that reads a state plus `self.tr(...)`
+14
View File
@@ -128,6 +128,20 @@ check_no_matches \
'^[[:space:]]*pub\(in crate::features\)[[:space:]]+(bottom_panel|quick_cmd_height|serial_send_height|bottom_panel_resize)[[:space:]]*:' \
crates/nyaterm-desktop/src/features/app_state/mod.rs
check_no_matches \
"window shell state must stay grouped under ShellFeatureState" \
'^[[:space:]]*pub\(in crate::features\)[[:space:]]+(last_viewport_size|wallpaper_tile_dimensions|last_viewport_change_at|title_drag_active_until|selected_nav|main_mode|settings_active_tab|settings_expanded_groups|settings_draft_snapshot|settings_window|settings_window_open_pending|settings_previous_left_collapsed|settings_previous_right_collapsed|active_left_panel|active_right_panel|left_open_panels|right_open_panels|panel_stack_sizes|panel_multi_open|right_focus|left_sidebar_collapsed|right_inspector_collapsed|mobile_left_open|mobile_right_open|left_panel_width|right_panel_width|panel_resize|panel_stack_resize|activity_bar_layout|activity_bar_context_menu|title_menu_open|title_menu_submenu|header_status|open_tabs_menu_open|new_session_menu_open|new_session_all_sessions_open|new_session_group_menu_path|session_tab_strip_scroll|session_tab_scroll_into_view_pending|last_connect_failure_name|last_connect_failure_error|workspace_split|workspace_split_resize|session_pane_roots|session_tab_owner|focused_terminal_window_leaf_id|workspace_pane_layout_restored)[[:space:]]*:' \
crates/nyaterm-desktop/src/features/app_state/mod.rs
check_no_matches \
"panel resize transitions must stay on ShellPanelState" \
'PanelResizeState' \
crates/nyaterm-desktop/src/features/shell/panel_resize_runtime.rs
check_no_matches \
"panel stack resize transitions must stay on ShellPanelState" \
'PanelStackResizeState' \
crates/nyaterm-desktop/src/features/shell/panel_stack_runtime.rs
check_no_matches \
"screen lock fields must stay grouped under SecurityFeatureState" \
'^[[:space:]]*pub\(in crate::features\)[[:space:]]+(lock_focus|lock_password_draft|lock_status|is_locked|last_user_activity_at)[[:space:]]*:' \