From fffed38026449e46cea0b7df424ddfa6990866cd Mon Sep 17 00:00:00 2001 From: Kang Date: Tue, 28 Jul 2026 20:35:14 +0800 Subject: [PATCH] refactor(desktop): group background feature state --- .../src/features/app_state/construct.rs | 32 +---- .../src/features/app_state/mod.rs | 37 ++--- .../src/features/layout/title_bar/menu.rs | 4 +- crates/nyaterm-desktop/src/features/mod.rs | 5 +- .../features/pages/settings/translation.rs | 43 +++--- .../panels/terminal_actions_overlay.rs | 2 +- .../src/features/panels/update_overlay.rs | 8 +- crates/nyaterm-desktop/src/features/root.rs | 6 +- .../src/features/runtime_jobs.rs | 12 +- .../src/features/settings/config_runtime.rs | 23 +-- .../src/features/settings/mod.rs | 1 - .../settings/settings_runtime/draft.rs | 39 +++--- .../src/features/shell/event_pump/mod.rs | 4 +- .../features/shell/global_shortcut_runtime.rs | 4 +- .../terminal_context_menu_runtime/menu.rs | 2 +- .../action_links.rs | 2 +- .../src/features/translation/mod.rs | 3 + .../src/features/translation/state.rs | 70 ++++++++++ .../translation/translation_runtime.rs | 132 +++++++++--------- .../src/features/update/mod.rs | 6 + .../src/features/update/state.rs | 48 +++++++ .../{settings => update}/update_runtime.rs | 38 ++--- docs/architecture/gpui-migration-status.md | 22 ++- scripts/check-architecture-boundaries.sh | 15 ++ 24 files changed, 337 insertions(+), 221 deletions(-) create mode 100644 crates/nyaterm-desktop/src/features/translation/state.rs create mode 100644 crates/nyaterm-desktop/src/features/update/mod.rs create mode 100644 crates/nyaterm-desktop/src/features/update/state.rs rename crates/nyaterm-desktop/src/features/{settings => update}/update_runtime.rs (62%) diff --git a/crates/nyaterm-desktop/src/features/app_state/construct.rs b/crates/nyaterm-desktop/src/features/app_state/construct.rs index 8770e66ae..8e62e19a2 100644 --- a/crates/nyaterm-desktop/src/features/app_state/construct.rs +++ b/crates/nyaterm-desktop/src/features/app_state/construct.rs @@ -6,7 +6,7 @@ use crate::models::{ ActivityBarLayoutState, BottomPanelMode, CloudSyncInputField, CloudSyncSecretDraft, GithubGistAuthState, HeaderStatusState, MainMode, NavItem, PanelSide, RecordingWritePipeline, RightFocus, SessionEventBridge, SettingsTab, StartupCommandAction, StoreStatus, - TerminalFramePipeline, TranslateInputField, TranslationSecretDraft, + TerminalFramePipeline, }; use crate::terminal::initial_terminal_screen; use gpui::{Context, ScrollHandle}; @@ -26,9 +26,10 @@ use super::super::{ QuickCommandFeatureState, RemoteOpsFeatureFocus, RemoteOpsFeatureState, SecurityFeatureFocus, SecurityFeatureState, SendCommandFeatureFocus, SendCommandFeatureState, SftpDuplicatePromptBroker, TerminalFeatureFocus, TerminalFeatureState, TextInputRegistry, - TransferFeatureFocus, TransferFeatureState, 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, + TransferFeatureFocus, TransferFeatureState, TranslationFeatureState, 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; use crate::models::panel_collapsed_from_persistence; @@ -60,8 +61,6 @@ impl NyaTermApp { }; let (session_start_tx, session_start_rx) = mpsc::channel(); let (tunnel_tx, tunnel_rx) = mpsc::channel(); - let (translate_tx, translate_rx) = mpsc::channel(); - let (update_tx, update_rx) = mpsc::channel(); let (github_gist_auth_tx, github_gist_auth_rx) = mpsc::channel(); let (command_persistence_tx, command_persistence_rx) = spawn_command_persistence_worker( runtime.config_dir().to_path_buf(), @@ -289,7 +288,6 @@ impl NyaTermApp { .collect::>(); let panel_multi_open = settings.ui_panel_multi_open; let settings_master_password_enabled = settings.has_master_password; - let translate_target_language = translation_settings.target_language.clone(); let mut terminal_output_decoder = TerminalOutputDecoder::default(); terminal_output_decoder.set_encoding(&settings.interaction_default_encoding); let mut terminal_screen = initial_terminal_screen(); @@ -427,6 +425,8 @@ impl NyaTermApp { }, ), remote_ops: RemoteOpsFeatureState::new(RemoteOpsFeatureFocus {}), + translation: TranslationFeatureState::new(translation_settings), + update: UpdateFeatureState::new(), command_history: Arc::from(command_history), command_persistence_tx, command_persistence_rx, @@ -439,7 +439,6 @@ impl NyaTermApp { action_link_menu: None, action_link_tooltip: None, action_link_hover_pending: None, - translation_dialog: None, bottom_panel: if settings.ui_serial_send_visible { BottomPanelMode::CommandSend } else if settings.ui_quick_cmd_visible { @@ -476,23 +475,6 @@ impl NyaTermApp { tunnel_tx, tunnel_rx, pending_tunnels: Vec::new(), - translate_tx, - translate_rx, - translate_provider: "google".to_string(), - translation_settings, - translation_secret_draft: TranslationSecretDraft::default(), - translate_target_language, - translate_input: String::new(), - translate_result: None, - translate_status: "Google translation ready".to_string(), - translate_pending: false, - translate_focused_field: TranslateInputField::Text, - update_tx, - update_rx, - update_status: format!("Current version {}", env!("CARGO_PKG_VERSION")), - update_info: None, - update_pending: false, - update_dialog_open: false, about_open: false, remote_editor_window: None, remote_editor_window_open_pending: false, diff --git a/crates/nyaterm-desktop/src/features/app_state/mod.rs b/crates/nyaterm-desktop/src/features/app_state/mod.rs index 4094f3ff0..d1283528f 100644 --- a/crates/nyaterm-desktop/src/features/app_state/mod.rs +++ b/crates/nyaterm-desktop/src/features/app_state/mod.rs @@ -5,9 +5,9 @@ use std::time::Instant; use gpui::{FocusHandle, ScrollHandle, WindowHandle}; use nyaterm_core::{ AiExecutionProfile, AppRuntime, AppSettingsSummary, CloudSyncHistoryEntry, CloudSyncSettings, - CloudSyncState, CommandHistoryEntry, Group, KeywordHighlightConfig, NativeServices, - NativeUpdateInfo, OtpEntry, ProxyConfig, ProxyGroup, QuickCommand, QuickCommandCategory, - SavedConnection, SavedCredential, SavedPassword, SshKey, TunnelConfig, TunnelGroup, + CloudSyncState, CommandHistoryEntry, Group, KeywordHighlightConfig, NativeServices, OtpEntry, + ProxyConfig, ProxyGroup, QuickCommand, QuickCommandCategory, SavedConnection, SavedCredential, + SavedPassword, SshKey, TunnelConfig, TunnelGroup, }; use nyaterm_legacy::MigrationInventory; use nyaterm_transport::{ @@ -22,8 +22,7 @@ use super::panels::SendCommandFeatureState; use super::remote::RemoteOpsFeatureState; use super::remote_editor_window::RemoteFileEditorWindow; use super::runtime_jobs::{ - CommandPersistenceRequest, CommandPersistenceResult, SessionStartResult, TranslateJobResult, - TunnelJobResult, UpdateJobResult, + CommandPersistenceRequest, CommandPersistenceResult, SessionStartResult, TunnelJobResult, }; use super::session::{ CredentialPromptBroker, CredentialPromptState, HostKeyPromptBroker, HostKeyPromptRequest, @@ -35,6 +34,8 @@ use super::settings_window::SettingsWindow; use super::terminal::TerminalFeatureState; use super::text_inputs::TextInputRegistry; use super::transfers::TransferFeatureState; +use super::translation::TranslationFeatureState; +use super::update::UpdateFeatureState; use crate::models::{ ActionLinkMenuState, ActionLinkTooltipState, ActiveSessionMenuState, ActivityBarContextMenuState, ActivityBarLayoutState, BottomPanelMode, BottomPanelResizeState, @@ -44,11 +45,9 @@ use crate::models::{ PanelStackResizeState, RecordingPathPromptKind, RecordingWritePipeline, RightFocus, SessionEventBridge, SessionRuntimeMetadata, SettingsTab, SnapshotPasswordPromptState, StartupCommandAction, StoreStatus, SyncInputGroup, TabActionsSubmenu, TerminalFrameEvent, - TitleMenu, TitleMenuSubmenu, TranslateInputField, TranslationDialogState, - TranslationSecretDraft, WorkspacePaneNode, WorkspaceSplitDirection, WorkspaceSplitResizeState, - WorkspaceSplitState, + TitleMenu, TitleMenuSubmenu, WorkspacePaneNode, WorkspaceSplitDirection, + WorkspaceSplitResizeState, WorkspaceSplitState, }; -use nyaterm_core::{TranslateResult, TranslationSettings}; mod construct; mod types; @@ -89,6 +88,8 @@ pub struct NyaTermApp { pub(in crate::features) terminal: TerminalFeatureState, pub(in crate::features) send_command: SendCommandFeatureState, pub(in crate::features) transfer: TransferFeatureState, + pub(in crate::features) translation: TranslationFeatureState, + pub(in crate::features) update: UpdateFeatureState, pub(in crate::features) command_history: Arc<[CommandHistoryEntry]>, pub(in crate::features) command_persistence_tx: mpsc::Sender, pub(in crate::features) command_persistence_rx: mpsc::Receiver, @@ -105,7 +106,6 @@ pub struct NyaTermApp { pub(in crate::features) action_link_hover_pending: Option<(String, Instant, ActionLinkTooltipState)>, - pub(in crate::features) translation_dialog: Option, pub(in crate::features) bottom_panel: BottomPanelMode, pub(in crate::features) quick_cmd_height: f32, pub(in crate::features) serial_send_height: f32, @@ -140,23 +140,6 @@ pub struct NyaTermApp { pub(in crate::features) tunnel_tx: mpsc::Sender, pub(in crate::features) tunnel_rx: mpsc::Receiver, pub(in crate::features) pending_tunnels: Vec, - pub(in crate::features) translate_tx: mpsc::Sender, - pub(in crate::features) translate_rx: mpsc::Receiver, - pub(in crate::features) translate_provider: String, - pub(in crate::features) translation_settings: TranslationSettings, - pub(in crate::features) translation_secret_draft: TranslationSecretDraft, - pub(in crate::features) translate_target_language: String, - pub(in crate::features) translate_input: String, - pub(in crate::features) translate_result: Option, - pub(in crate::features) translate_status: String, - pub(in crate::features) translate_pending: bool, - pub(in crate::features) translate_focused_field: TranslateInputField, - pub(in crate::features) update_tx: mpsc::Sender, - pub(in crate::features) update_rx: mpsc::Receiver, - pub(in crate::features) update_status: String, - pub(in crate::features) update_info: Option, - pub(in crate::features) update_pending: bool, - pub(in crate::features) update_dialog_open: bool, pub(in crate::features) about_open: bool, pub(in crate::features) remote_editor_window: Option>, pub(in crate::features) remote_editor_window_open_pending: bool, diff --git a/crates/nyaterm-desktop/src/features/layout/title_bar/menu.rs b/crates/nyaterm-desktop/src/features/layout/title_bar/menu.rs index 1fd2effbd..2ac531175 100644 --- a/crates/nyaterm-desktop/src/features/layout/title_bar/menu.rs +++ b/crates/nyaterm-desktop/src/features/layout/title_bar/menu.rs @@ -206,9 +206,9 @@ impl NyaTermApp { )); } TitleMenu::Help => { - let update_label = if self.update_pending { + let update_label = if self.update.pending { tr("updater.checking") - } else if self.update_info.as_ref().is_some_and(|info| info.available) { + } else if self.update.info.as_ref().is_some_and(|info| info.available) { tr("updater.newVersionAvailable") } else { tr("menu.checkForUpdates") diff --git a/crates/nyaterm-desktop/src/features/mod.rs b/crates/nyaterm-desktop/src/features/mod.rs index d70bfd7e2..aa128df2e 100644 --- a/crates/nyaterm-desktop/src/features/mod.rs +++ b/crates/nyaterm-desktop/src/features/mod.rs @@ -29,6 +29,7 @@ mod transfer_external_sync_window; mod transfers; mod translation; mod tunnels; +mod update; mod view_widgets; #[allow(unused_imports)] @@ -77,7 +78,7 @@ pub(in crate::features) use runtime_jobs::{ AiChatJobOutput, AiChatJobResult, AiChatWorkerEvent, AiDiscoveryJobResult, CommandPersistenceRequest, CommandPersistenceResult, DockerJobResult, ProcessJobResult, SessionStartResult, SessionStartSuccess, StatsJobResult, TunnelJobOutput, TunnelJobResult, - UpdateJobResult, spawn_command_persistence_worker, + spawn_command_persistence_worker, }; pub(in crate::features) use session::{ CredentialPromptBroker, HostKeyPromptBroker, NativeOtpProvider, SftpDuplicatePromptBroker, @@ -101,6 +102,8 @@ pub(in crate::features) use transfers::{ TransferFeatureFocus, TransferFeatureState, duplicate_decision_label, duplicate_policy_label, format_file_size, transfer_job_title, transfer_status_label, }; +pub(in crate::features) use translation::TranslationFeatureState; +pub(in crate::features) use update::UpdateFeatureState; pub(in crate::features) use view_widgets::{ child_window_header, child_window_titlebar, color_icon, connection_type_icon, dialog_action_button, logo_mark, metric, modal_close_icon_button, diff --git a/crates/nyaterm-desktop/src/features/pages/settings/translation.rs b/crates/nyaterm-desktop/src/features/pages/settings/translation.rs index c1cbb5ee2..d0f349f6b 100644 --- a/crates/nyaterm-desktop/src/features/pages/settings/translation.rs +++ b/crates/nyaterm-desktop/src/features/pages/settings/translation.rs @@ -36,25 +36,25 @@ impl NyaTermApp { cx: &mut Context, ) -> impl IntoElement { let palette = self.theme_palette(); - let deepl_key_value = self.translation_secret_draft.deepl_api_key.clone(); - let baidu_app_id_value = self.translation_settings.baidu_app_id.clone(); - let baidu_key_value = self.translation_secret_draft.baidu_app_key.clone(); - let ali_app_id_value = self.translation_settings.ali_app_id.clone(); - let ali_key_value = self.translation_secret_draft.ali_app_key.clone(); - let youdao_app_id_value = self.translation_settings.youdao_app_id.clone(); - let youdao_key_value = self.translation_secret_draft.youdao_app_key.clone(); + let deepl_key_value = self.translation.secret_draft.deepl_api_key.clone(); + let baidu_app_id_value = self.translation.settings.baidu_app_id.clone(); + let baidu_key_value = self.translation.secret_draft.baidu_app_key.clone(); + let ali_app_id_value = self.translation.settings.ali_app_id.clone(); + let ali_key_value = self.translation.secret_draft.ali_app_key.clone(); + let youdao_app_id_value = self.translation.settings.youdao_app_id.clone(); + let youdao_key_value = self.translation.secret_draft.youdao_app_key.clone(); - let deepl_configured = !self.translation_settings.deepl_api_key.trim().is_empty() - || !self.translation_secret_draft.deepl_api_key.is_empty(); - let baidu_configured = !self.translation_settings.baidu_app_id.trim().is_empty() - && (!self.translation_settings.baidu_app_key.trim().is_empty() - || !self.translation_secret_draft.baidu_app_key.is_empty()); - let ali_configured = !self.translation_settings.ali_app_id.trim().is_empty() - && (!self.translation_settings.ali_app_key.trim().is_empty() - || !self.translation_secret_draft.ali_app_key.is_empty()); - let youdao_configured = !self.translation_settings.youdao_app_id.trim().is_empty() - && (!self.translation_settings.youdao_app_key.trim().is_empty() - || !self.translation_secret_draft.youdao_app_key.is_empty()); + let deepl_configured = !self.translation.settings.deepl_api_key.trim().is_empty() + || !self.translation.secret_draft.deepl_api_key.is_empty(); + let baidu_configured = !self.translation.settings.baidu_app_id.trim().is_empty() + && (!self.translation.settings.baidu_app_key.trim().is_empty() + || !self.translation.secret_draft.baidu_app_key.is_empty()); + let ali_configured = !self.translation.settings.ali_app_id.trim().is_empty() + && (!self.translation.settings.ali_app_key.trim().is_empty() + || !self.translation.secret_draft.ali_app_key.is_empty()); + let youdao_configured = !self.translation.settings.youdao_app_id.trim().is_empty() + && (!self.translation.settings.youdao_app_key.trim().is_empty() + || !self.translation.secret_draft.youdao_app_key.is_empty()); let target_language_label = self.tr("settings.targetLanguage"); let target_language_desc = self.tr("settings.targetLanguageDesc"); @@ -103,7 +103,8 @@ impl NyaTermApp { let code = *code; let label = *label; let selected = self - .translation_settings + .translation + .settings .target_language .eq_ignore_ascii_case(code); settings_choice_chip( @@ -112,8 +113,8 @@ impl NyaTermApp { label, selected, cx.listener(move |this, _, _, cx| { - this.translation_settings.target_language = code.to_string(); - this.translate_target_language = code.to_string(); + this.translation.settings.target_language = code.to_string(); + this.translation.target_language = code.to_string(); this.save_translation_settings(cx); }), ) diff --git a/crates/nyaterm-desktop/src/features/panels/terminal_actions_overlay.rs b/crates/nyaterm-desktop/src/features/panels/terminal_actions_overlay.rs index d3d75a4d6..2f4a345f1 100644 --- a/crates/nyaterm-desktop/src/features/panels/terminal_actions_overlay.rs +++ b/crates/nyaterm-desktop/src/features/panels/terminal_actions_overlay.rs @@ -217,7 +217,7 @@ impl NyaTermApp { this.terminal.view.status = "terminal visible screen is empty".to_string(); } else { - let provider = this.translate_provider.clone(); + let provider = this.translation.provider.clone(); let provider_label = match provider.as_str() { "google" => this.tr("translation.google"), "microsoft" => { diff --git a/crates/nyaterm-desktop/src/features/panels/update_overlay.rs b/crates/nyaterm-desktop/src/features/panels/update_overlay.rs index a864c49cc..8c3515b85 100644 --- a/crates/nyaterm-desktop/src/features/panels/update_overlay.rs +++ b/crates/nyaterm-desktop/src/features/panels/update_overlay.rs @@ -13,11 +13,11 @@ impl NyaTermApp { cx: &mut Context, ) -> impl IntoElement { let palette = self.theme_palette(); - let update_info = self.update_info.clone(); - let checking = self.update_pending; + let update_info = self.update.info.clone(); + let checking = self.update.pending; let failed = !checking && update_info.is_none() - && self.update_status.starts_with("update check failed:"); + && self.update.status.starts_with("update check failed:"); let available = !checking && !failed && update_info.as_ref().is_some_and(|info| info.available); let portable = self.runtime.mode() == RuntimeMode::Portable; @@ -137,7 +137,7 @@ impl NyaTermApp { .text_xs() .line_height(px(18.)) .text_color(rgb(palette.danger)) - .child(self.update_status.clone()), + .child(self.update.status.clone()), ) }) .when(available && portable, |this| { diff --git a/crates/nyaterm-desktop/src/features/root.rs b/crates/nyaterm-desktop/src/features/root.rs index 34a2226bb..29bab6ece 100644 --- a/crates/nyaterm-desktop/src/features/root.rs +++ b/crates/nyaterm-desktop/src/features/root.rs @@ -567,10 +567,10 @@ impl NyaTermApp { overlay.action_link_tooltip_open && !overlay.action_link_menu_open && !overlay.terminal_context_menu_open - && self.translation_dialog.is_none(), + && self.translation.dialog.is_none(), |this| this.child(self.action_link_tooltip_overlay(cx)), ) - .when(self.translation_dialog.is_some(), |this| { + .when(self.translation.dialog.is_some(), |this| { this.child(self.translation_dialog_overlay(cx)) }) .when(overlay.command_suggestions_open, |this| { @@ -639,7 +639,7 @@ impl NyaTermApp { this.child(self.close_all_sessions_confirm_overlay(cx)) }) .when(self.about_open, |this| this.child(self.about_overlay(cx))) - .when(self.update_dialog_open, |this| { + .when(self.update.dialog_open, |this| { this.child(self.update_overlay(cx)) }) .when(self.modal_child_window_open(), |this| { diff --git a/crates/nyaterm-desktop/src/features/runtime_jobs.rs b/crates/nyaterm-desktop/src/features/runtime_jobs.rs index 4722f1d81..c8dab2866 100644 --- a/crates/nyaterm-desktop/src/features/runtime_jobs.rs +++ b/crates/nyaterm-desktop/src/features/runtime_jobs.rs @@ -4,7 +4,7 @@ use std::time::Instant; use nyaterm_core::{ AiCommandCard, AiMode, AiModelDiscovery, CommandHistoryEntry, CommandObservation, - ConnectionStore, NativeUpdateInfo, TranslateResult, + ConnectionStore, }; use nyaterm_transport::{ DockerComposeService, DockerContainerDetails, RemoteDockerOverview, RemoteProcess, RemoteStats, @@ -54,16 +54,6 @@ pub(in crate::features) struct StatsJobResult { pub(in crate::features) result: Result, } -#[derive(Debug)] -pub(in crate::features) struct TranslateJobResult { - pub(in crate::features) result: Result, -} - -#[derive(Debug)] -pub(in crate::features) struct UpdateJobResult { - pub(in crate::features) result: Result, -} - #[derive(Debug)] pub(in crate::features) enum CommandPersistenceRequest { AppendHistory(Vec), diff --git a/crates/nyaterm-desktop/src/features/settings/config_runtime.rs b/crates/nyaterm-desktop/src/features/settings/config_runtime.rs index b054e5335..7a0c7566b 100644 --- a/crates/nyaterm-desktop/src/features/settings/config_runtime.rs +++ b/crates/nyaterm-desktop/src/features/settings/config_runtime.rs @@ -541,15 +541,15 @@ impl NyaTermApp { store.load_app_settings_summary().unwrap_or_default(), ); self.apply_ui_layout_from_settings(); - self.translation_settings = store + self.translation.settings = store .load_translation_settings() .unwrap_or_else(|_| TranslationSettings { target_language: self.settings.language.clone(), ..TranslationSettings::default() }); - self.translation_secret_draft = TranslationSecretDraft::default(); - self.translate_target_language = - self.translation_settings.target_language.clone(); + self.translation.secret_draft = TranslationSecretDraft::default(); + self.translation.target_language = + self.translation.settings.target_language.clone(); self.recording_manager .set_memory_limit(self.settings.recording_memory_limit_bytes as usize); self.cloud_sync_settings = store @@ -592,10 +592,10 @@ impl NyaTermApp { self.command_history = Arc::default(); self.keyword_highlights = KeywordHighlightConfig::default(); self.apply_gpui_settings(AppSettingsSummary::default()); - self.translation_settings = TranslationSettings::default(); - self.translation_secret_draft = TranslationSecretDraft::default(); - self.translate_target_language = - self.translation_settings.target_language.clone(); + self.translation.settings = TranslationSettings::default(); + self.translation.secret_draft = TranslationSecretDraft::default(); + self.translation.target_language = + self.translation.settings.target_language.clone(); self.store_status = StoreStatus { path, message: format!("failed to load sessions: {error}"), @@ -614,9 +614,10 @@ impl NyaTermApp { self.quick_command_categories.clear(); self.command_history = Arc::default(); self.apply_gpui_settings(AppSettingsSummary::default()); - self.translation_settings = TranslationSettings::default(); - self.translation_secret_draft = TranslationSecretDraft::default(); - self.translate_target_language = self.translation_settings.target_language.clone(); + self.translation.settings = TranslationSettings::default(); + self.translation.secret_draft = TranslationSecretDraft::default(); + self.translation.target_language = + self.translation.settings.target_language.clone(); self.store_status = StoreStatus { path: self .runtime diff --git a/crates/nyaterm-desktop/src/features/settings/mod.rs b/crates/nyaterm-desktop/src/features/settings/mod.rs index 482aded67..d20a61a81 100644 --- a/crates/nyaterm-desktop/src/features/settings/mod.rs +++ b/crates/nyaterm-desktop/src/features/settings/mod.rs @@ -6,7 +6,6 @@ mod security_runtime; mod security_state; mod settings_runtime; mod state; -mod update_runtime; pub(in crate::features) use security_state::{SecurityFeatureFocus, SecurityFeatureState}; pub(in crate::features) use state::{SettingsFeatureFocus, SettingsFeatureState}; diff --git a/crates/nyaterm-desktop/src/features/settings/settings_runtime/draft.rs b/crates/nyaterm-desktop/src/features/settings/settings_runtime/draft.rs index a2e819e1c..a6c8c7c30 100644 --- a/crates/nyaterm-desktop/src/features/settings/settings_runtime/draft.rs +++ b/crates/nyaterm-desktop/src/features/settings/settings_runtime/draft.rs @@ -19,8 +19,8 @@ impl NyaTermApp { ai_secret_draft: self.ai.settings.secret_draft.clone(), cloud_sync_settings: self.cloud_sync_settings.clone(), cloud_sync_secret_draft: self.cloud_sync_secret_draft.clone(), - translation_settings: self.translation_settings.clone(), - translation_secret_draft: self.translation_secret_draft.clone(), + translation_settings: self.translation.settings.clone(), + translation_secret_draft: self.translation.secret_draft.clone(), keyword_highlights: self.keyword_highlights.clone(), master_password_enabled: self.settings_master_password_enabled, master_password_draft: self.settings_master_password_draft.clone(), @@ -38,8 +38,8 @@ impl NyaTermApp { || snapshot.ai_secret_draft != self.ai.settings.secret_draft || snapshot.cloud_sync_settings != self.cloud_sync_settings || snapshot.cloud_sync_secret_draft != self.cloud_sync_secret_draft - || snapshot.translation_settings != self.translation_settings - || snapshot.translation_secret_draft != self.translation_secret_draft + || snapshot.translation_settings != self.translation.settings + || snapshot.translation_secret_draft != self.translation.secret_draft || snapshot.keyword_highlights != self.keyword_highlights || snapshot.master_password_enabled != self.settings_master_password_enabled || snapshot.master_password_draft != self.settings_master_password_draft @@ -61,18 +61,18 @@ impl NyaTermApp { } pub(in crate::features) fn pending_translation_settings(&self) -> TranslationSettings { - let mut next = self.translation_settings.clone(); - if !self.translation_secret_draft.deepl_api_key.is_empty() { - next.deepl_api_key = self.translation_secret_draft.deepl_api_key.clone(); + let mut next = self.translation.settings.clone(); + if !self.translation.secret_draft.deepl_api_key.is_empty() { + next.deepl_api_key = self.translation.secret_draft.deepl_api_key.clone(); } - if !self.translation_secret_draft.baidu_app_key.is_empty() { - next.baidu_app_key = self.translation_secret_draft.baidu_app_key.clone(); + if !self.translation.secret_draft.baidu_app_key.is_empty() { + next.baidu_app_key = self.translation.secret_draft.baidu_app_key.clone(); } - if !self.translation_secret_draft.ali_app_key.is_empty() { - next.ali_app_key = self.translation_secret_draft.ali_app_key.clone(); + if !self.translation.secret_draft.ali_app_key.is_empty() { + next.ali_app_key = self.translation.secret_draft.ali_app_key.clone(); } - if !self.translation_secret_draft.youdao_app_key.is_empty() { - next.youdao_app_key = self.translation_secret_draft.youdao_app_key.clone(); + if !self.translation.secret_draft.youdao_app_key.is_empty() { + next.youdao_app_key = self.translation.secret_draft.youdao_app_key.clone(); } next } @@ -326,13 +326,14 @@ impl NyaTermApp { self.settings_master_password_draft.clear(); self.ai.settings.config = saved_ai_settings; self.cloud_sync_settings = saved_cloud_sync_settings; - self.translation_settings = saved_translation_settings; + self.translation.settings = saved_translation_settings; self.keyword_highlights = saved_keyword_highlights; - self.translation_secret_draft = TranslationSecretDraft::default(); + self.translation.secret_draft = TranslationSecretDraft::default(); self.cloud_sync_secret_draft = CloudSyncSecretDraft::default(); self.ai.settings.secret_draft.clear(); self.sync_ai_drafts_from_active_profile(); - self.translate_target_language = self.translation_settings.target_language.clone(); + self.translation.target_language = + self.translation.settings.target_language.clone(); self.recording_manager .set_memory_limit(self.settings.recording_memory_limit_bytes as usize); self.transfer.paths.duplicate_policy = SftpDuplicatePolicy::from_legacy_value( @@ -384,8 +385,8 @@ impl NyaTermApp { self.ai.settings.secret_draft = snapshot.ai_secret_draft; self.cloud_sync_settings = snapshot.cloud_sync_settings; self.cloud_sync_secret_draft = snapshot.cloud_sync_secret_draft; - self.translation_settings = snapshot.translation_settings; - self.translation_secret_draft = snapshot.translation_secret_draft; + self.translation.settings = snapshot.translation_settings; + self.translation.secret_draft = snapshot.translation_secret_draft; self.keyword_highlights = snapshot.keyword_highlights; self.settings_master_password_enabled = snapshot.master_password_enabled; self.settings_master_password_draft = snapshot.master_password_draft; @@ -397,7 +398,7 @@ impl NyaTermApp { self.invalidate_terminal_cell_metrics(cx); self.invalidate_paint_theme_caches(); self.sync_ai_drafts_from_active_profile(); - self.translate_target_language = self.translation_settings.target_language.clone(); + self.translation.target_language = self.translation.settings.target_language.clone(); self.refresh_visible_terminal_surfaces(cx); } self.finish_settings_page(cx); diff --git a/crates/nyaterm-desktop/src/features/shell/event_pump/mod.rs b/crates/nyaterm-desktop/src/features/shell/event_pump/mod.rs index 81c8c719f..e30cece95 100644 --- a/crates/nyaterm-desktop/src/features/shell/event_pump/mod.rs +++ b/crates/nyaterm-desktop/src/features/shell/event_pump/mod.rs @@ -499,8 +499,8 @@ impl NyaTermApp { && !self.remote_ops.stats.pending && !self.remote_ops.process.pending && !self.remote_ops.docker.pending - && !self.translate_pending - && !self.update_pending + && !self.translation.pending + && !self.update.pending && !self.ai.chat.focus_pending && !self.transfer.file_ops.rename_focus_pending && !self.credential_prompt_focus_pending diff --git a/crates/nyaterm-desktop/src/features/shell/global_shortcut_runtime.rs b/crates/nyaterm-desktop/src/features/shell/global_shortcut_runtime.rs index 8ea29892f..e9e2d2e86 100644 --- a/crates/nyaterm-desktop/src/features/shell/global_shortcut_runtime.rs +++ b/crates/nyaterm-desktop/src/features/shell/global_shortcut_runtime.rs @@ -16,7 +16,7 @@ impl NyaTermApp { } if event.keystroke.key.as_str() == "escape" { - if self.update_dialog_open { + if self.update.dialog_open { self.close_update_dialog(cx); return true; } @@ -24,7 +24,7 @@ impl NyaTermApp { self.close_about(cx); return true; } - if self.translation_dialog.is_some() { + if self.translation.dialog.is_some() { self.close_translation_dialog(cx); return true; } diff --git a/crates/nyaterm-desktop/src/features/terminal/terminal_context_menu_runtime/menu.rs b/crates/nyaterm-desktop/src/features/terminal/terminal_context_menu_runtime/menu.rs index 750c89cc7..e518115e8 100644 --- a/crates/nyaterm-desktop/src/features/terminal/terminal_context_menu_runtime/menu.rs +++ b/crates/nyaterm-desktop/src/features/terminal/terminal_context_menu_runtime/menu.rs @@ -99,7 +99,7 @@ impl NyaTermApp { Vec::new() }; let translation_providers: Vec<(String, String)> = - available_translation_providers(&self.translation_settings) + available_translation_providers(&self.translation.settings) .into_iter() .map(|(id, _)| { let label = self diff --git a/crates/nyaterm-desktop/src/features/terminal/terminal_selection_runtime/action_links.rs b/crates/nyaterm-desktop/src/features/terminal/terminal_selection_runtime/action_links.rs index 7b0061c91..09c97ec9c 100644 --- a/crates/nyaterm-desktop/src/features/terminal/terminal_selection_runtime/action_links.rs +++ b/crates/nyaterm-desktop/src/features/terminal/terminal_selection_runtime/action_links.rs @@ -83,7 +83,7 @@ impl NyaTermApp { if self.action_link_menu.is_some() || self.terminal.menus.context_menu.is_some() || self.terminal.selection.dragging - || self.translation_dialog.is_some() + || self.translation.dialog.is_some() { self.clear_action_link_tooltip(cx); return; diff --git a/crates/nyaterm-desktop/src/features/translation/mod.rs b/crates/nyaterm-desktop/src/features/translation/mod.rs index 99a84593c..d4e92325f 100644 --- a/crates/nyaterm-desktop/src/features/translation/mod.rs +++ b/crates/nyaterm-desktop/src/features/translation/mod.rs @@ -1,3 +1,6 @@ //! Translation dialog runtime. +mod state; mod translation_runtime; + +pub(in crate::features) use state::TranslationFeatureState; diff --git a/crates/nyaterm-desktop/src/features/translation/state.rs b/crates/nyaterm-desktop/src/features/translation/state.rs new file mode 100644 index 000000000..5a0035f1b --- /dev/null +++ b/crates/nyaterm-desktop/src/features/translation/state.rs @@ -0,0 +1,70 @@ +//! Authoritative transient state for translation settings and jobs. + +use std::sync::mpsc; + +use nyaterm_core::{TranslateResult, TranslationSettings}; + +use crate::models::{TranslateInputField, TranslationDialogState, TranslationSecretDraft}; + +pub(super) struct TranslateJobResult { + pub result: Result, +} + +pub(in crate::features) struct TranslationFeatureState { + pub dialog: Option, + pub(super) tx: mpsc::Sender, + pub(super) rx: mpsc::Receiver, + pub provider: String, + pub settings: TranslationSettings, + pub secret_draft: TranslationSecretDraft, + pub target_language: String, + pub input: String, + pub result: Option, + pub status: String, + pub pending: bool, + pub focused_field: TranslateInputField, +} + +impl TranslationFeatureState { + pub(in crate::features) fn new(settings: TranslationSettings) -> Self { + let (tx, rx) = mpsc::channel(); + let target_language = settings.target_language.clone(); + Self { + dialog: None, + tx, + rx, + provider: "google".to_string(), + settings, + secret_draft: TranslationSecretDraft::default(), + target_language, + input: String::new(), + result: None, + status: "Google translation ready".to_string(), + pending: false, + focused_field: TranslateInputField::Text, + } + } +} + +#[cfg(test)] +mod tests { + use nyaterm_core::TranslationSettings; + + use super::TranslationFeatureState; + + #[test] + fn translation_state_owns_job_channel_and_loaded_settings() { + let settings = TranslationSettings { + target_language: "ja".to_string(), + ..TranslationSettings::default() + }; + + let state = TranslationFeatureState::new(settings.clone()); + + assert_eq!(state.settings, settings); + assert_eq!(state.target_language, "ja"); + assert!(state.rx.try_recv().is_err()); + assert!(!state.pending); + assert!(state.dialog.is_none()); + } +} diff --git a/crates/nyaterm-desktop/src/features/translation/translation_runtime.rs b/crates/nyaterm-desktop/src/features/translation/translation_runtime.rs index 50ba69c38..4387530d0 100644 --- a/crates/nyaterm-desktop/src/features/translation/translation_runtime.rs +++ b/crates/nyaterm-desktop/src/features/translation/translation_runtime.rs @@ -5,11 +5,12 @@ use gpui::{ use nyaterm_core::ConnectionStore; use crate::features::NyaTermApp; -use crate::features::runtime_jobs::TranslateJobResult; use crate::http::translation::translate_text; use crate::models::{TranslateInputField, TranslationDialogState, TranslationSecretDraft}; use crate::widgets::small_button; +use super::state::TranslateJobResult; + const TRANSLATE_EVENT_DRAIN_LIMIT: usize = 8; impl NyaTermApp { @@ -18,24 +19,24 @@ impl NyaTermApp { _window: &mut Window, cx: &mut Context, ) { - if self.translate_pending { - self.translate_status = "translation already running".to_string(); + if self.translation.pending { + self.translation.status = "translation already running".to_string(); cx.notify(); return; } - if self.translate_input.trim().is_empty() { - self.translate_status = "type text before translating".to_string(); + if self.translation.input.trim().is_empty() { + self.translation.status = "type text before translating".to_string(); cx.notify(); return; } - self.translate_pending = true; - self.translate_status = format!("translating with {}", self.translate_provider); - let tx = self.translate_tx.clone(); - let provider = self.translate_provider.clone(); - let target_language = self.translate_target_language.clone(); - let text = self.translate_input.clone(); - let settings = self.translation_settings.clone(); + self.translation.pending = true; + self.translation.status = format!("translating with {}", self.translation.provider); + let tx = self.translation.tx.clone(); + let provider = self.translation.provider.clone(); + let target_language = self.translation.target_language.clone(); + let text = self.translation.input.clone(); + let settings = self.translation.settings.clone(); std::thread::spawn(move || { let result = translate_text(&provider, &text, &target_language, &settings); let _ = tx.send(TranslateJobResult { result }); @@ -46,10 +47,10 @@ impl NyaTermApp { pub(in crate::features) fn save_translation_settings(&mut self, cx: &mut Context) { let next = self.pending_translation_settings(); if self.defer_settings_persistence(cx) { - self.translation_settings = next; - self.translation_secret_draft = TranslationSecretDraft::default(); - self.translate_target_language = self.translation_settings.target_language.clone(); - self.translate_status = "translation settings staged".to_string(); + self.translation.settings = next; + self.translation.secret_draft = TranslationSecretDraft::default(); + self.translation.target_language = self.translation.settings.target_language.clone(); + self.translation.status = "translation settings staged".to_string(); return; } @@ -60,16 +61,17 @@ impl NyaTermApp { .and_then(|store| store.save_translation_settings(next)) { Ok(saved) => { - self.translation_settings = saved; - self.translation_secret_draft = TranslationSecretDraft::default(); - self.translate_target_language = self.translation_settings.target_language.clone(); - self.translate_status = "translation settings saved".to_string(); + self.translation.settings = saved; + self.translation.secret_draft = TranslationSecretDraft::default(); + self.translation.target_language = + self.translation.settings.target_language.clone(); + self.translation.status = "translation settings saved".to_string(); self.store_status.message = "translation settings saved".to_string(); self.store_status.ready = true; } Err(error) => { - self.translate_status = format!("translation settings save failed: {error}"); - self.store_status.message = self.translate_status.clone(); + self.translation.status = format!("translation settings save failed: {error}"); + self.store_status.message = self.translation.status.clone(); self.store_status.ready = false; } } @@ -83,24 +85,24 @@ impl NyaTermApp { ) { match provider { "deepl" => { - self.translation_settings.deepl_api_key.clear(); - self.translation_secret_draft.deepl_api_key.clear(); + self.translation.settings.deepl_api_key.clear(); + self.translation.secret_draft.deepl_api_key.clear(); } "baidu" => { - self.translation_settings.baidu_app_key.clear(); - self.translation_secret_draft.baidu_app_key.clear(); + self.translation.settings.baidu_app_key.clear(); + self.translation.secret_draft.baidu_app_key.clear(); } "ali" => { - self.translation_settings.ali_app_key.clear(); - self.translation_secret_draft.ali_app_key.clear(); + self.translation.settings.ali_app_key.clear(); + self.translation.secret_draft.ali_app_key.clear(); } "youdao" => { - self.translation_settings.youdao_app_key.clear(); - self.translation_secret_draft.youdao_app_key.clear(); + self.translation.settings.youdao_app_key.clear(); + self.translation.secret_draft.youdao_app_key.clear(); } _ => {} } - self.translate_status = format!("{provider} translation secret cleared; save to persist"); + self.translation.status = format!("{provider} translation secret cleared; save to persist"); cx.notify(); } @@ -111,9 +113,9 @@ impl NyaTermApp { text: String, cx: &mut Context, ) { - self.translate_focused_field = field; + self.translation.focused_field = field; *self.translate_input_value_mut() = text; - self.translate_status = if field.is_settings_field() { + self.translation.status = if field.is_settings_field() { "translation settings edited".to_string() } else { "translation input edited".to_string() @@ -122,16 +124,16 @@ impl NyaTermApp { } fn translate_input_value_mut(&mut self) -> &mut String { - match self.translate_focused_field { - TranslateInputField::TargetLanguage => &mut self.translate_target_language, - TranslateInputField::Text => &mut self.translate_input, - TranslateInputField::DeeplApiKey => &mut self.translation_secret_draft.deepl_api_key, - TranslateInputField::BaiduAppId => &mut self.translation_settings.baidu_app_id, - TranslateInputField::BaiduAppKey => &mut self.translation_secret_draft.baidu_app_key, - TranslateInputField::AliAppId => &mut self.translation_settings.ali_app_id, - TranslateInputField::AliAppKey => &mut self.translation_secret_draft.ali_app_key, - TranslateInputField::YoudaoAppId => &mut self.translation_settings.youdao_app_id, - TranslateInputField::YoudaoAppKey => &mut self.translation_secret_draft.youdao_app_key, + match self.translation.focused_field { + TranslateInputField::TargetLanguage => &mut self.translation.target_language, + TranslateInputField::Text => &mut self.translation.input, + TranslateInputField::DeeplApiKey => &mut self.translation.secret_draft.deepl_api_key, + TranslateInputField::BaiduAppId => &mut self.translation.settings.baidu_app_id, + TranslateInputField::BaiduAppKey => &mut self.translation.secret_draft.baidu_app_key, + TranslateInputField::AliAppId => &mut self.translation.settings.ali_app_id, + TranslateInputField::AliAppKey => &mut self.translation.secret_draft.ali_app_key, + TranslateInputField::YoudaoAppId => &mut self.translation.settings.youdao_app_id, + TranslateInputField::YoudaoAppKey => &mut self.translation.secret_draft.youdao_app_key, } } @@ -145,21 +147,21 @@ impl NyaTermApp { ) { let text = text.trim().to_string(); if text.is_empty() { - self.translate_status = "no text to translate".to_string(); + self.translation.status = "no text to translate".to_string(); cx.notify(); return; } - self.translation_dialog = Some(TranslationDialogState { + self.translation.dialog = Some(TranslationDialogState { source_text: text.clone(), provider: provider.clone(), provider_label, }); - self.translate_provider = provider; - self.translate_input = text; - self.translate_result = None; - self.translate_status = format!("translating with {}", self.translate_provider); + self.translation.provider = provider; + self.translation.input = text; + self.translation.result = None; + self.translation.status = format!("translating with {}", self.translation.provider); // Kick off immediately (Tauri TranslationDialog behavior). - if !self.translate_pending { + if !self.translation.pending { self.run_translation(window, cx); } else { cx.notify(); @@ -167,7 +169,7 @@ impl NyaTermApp { } pub(in crate::features) fn close_translation_dialog(&mut self, cx: &mut Context) { - if self.translation_dialog.take().is_some() { + if self.translation.dialog.take().is_some() { cx.notify(); } } @@ -177,13 +179,13 @@ impl NyaTermApp { cx: &mut Context, ) -> impl IntoElement { let palette = self.theme_palette(); - let Some(dialog) = self.translation_dialog.clone() else { + let Some(dialog) = self.translation.dialog.clone() else { return div().into_any_element(); }; let provider_label = dialog.provider_label.clone(); let source = dialog.source_text.clone(); - let pending = self.translate_pending; - let status = self.translate_status.clone(); + let pending = self.translation.pending; + let status = self.translation.status.clone(); let title_label = self.tr("translation.title"); let source_label = self.tr("translation.sourceText"); let translated_label = self.tr("translation.translatedText"); @@ -192,7 +194,7 @@ impl NyaTermApp { let copy_label = self.tr("translation.copy"); let close_label = self.tr("translation.close"); let copied_label = self.tr("translation.copied"); - let result = self.translate_result.clone(); + let result = self.translation.result.clone(); let detected = result .as_ref() .map(|item| item.detected_language.clone()) @@ -355,11 +357,11 @@ impl NyaTermApp { "translation-dialog-copy", copy_label, cx.listener(|this, _, _, cx| { - if let Some(result) = this.translate_result.clone() { + if let Some(result) = this.translation.result.clone() { cx.write_to_clipboard(ClipboardItem::new_string( result.translated, )); - this.translate_status = copied_label.to_string(); + this.translation.status = copied_label.to_string(); cx.notify(); } }), @@ -379,29 +381,29 @@ impl NyaTermApp { } pub(in crate::features) fn drain_translate_events(&mut self) -> bool { - if !self.translate_pending { + if !self.translation.pending { return false; } let mut dirty = false; for _ in 0..TRANSLATE_EVENT_DRAIN_LIMIT { - let Ok(event) = self.translate_rx.try_recv() else { + let Ok(event) = self.translation.rx.try_recv() else { break; }; dirty = true; - self.translate_pending = false; + self.translation.pending = false; match event.result { Ok(result) => { - self.translate_status = format!( + self.translation.status = format!( "translated {} character(s) from {}", result.original.chars().count(), result.detected_language ); - self.terminal.view.status = self.translate_status.clone(); - self.translate_result = Some(result); + self.terminal.view.status = self.translation.status.clone(); + self.translation.result = Some(result); } Err(error) => { - self.translate_status = format!("translation failed: {error}"); - self.terminal.view.status = self.translate_status.clone(); + self.translation.status = format!("translation failed: {error}"); + self.terminal.view.status = self.translation.status.clone(); } } } diff --git a/crates/nyaterm-desktop/src/features/update/mod.rs b/crates/nyaterm-desktop/src/features/update/mod.rs new file mode 100644 index 000000000..5abc1b1de --- /dev/null +++ b/crates/nyaterm-desktop/src/features/update/mod.rs @@ -0,0 +1,6 @@ +//! Native application update state and runtime. + +mod state; +mod update_runtime; + +pub(in crate::features) use state::UpdateFeatureState; diff --git a/crates/nyaterm-desktop/src/features/update/state.rs b/crates/nyaterm-desktop/src/features/update/state.rs new file mode 100644 index 000000000..b78e30acc --- /dev/null +++ b/crates/nyaterm-desktop/src/features/update/state.rs @@ -0,0 +1,48 @@ +//! Authoritative transient state for native update checks. + +use std::sync::mpsc; + +use nyaterm_core::NativeUpdateInfo; + +pub(super) struct UpdateJobResult { + pub result: Result, +} + +pub(in crate::features) struct UpdateFeatureState { + pub(super) tx: mpsc::Sender, + pub(super) rx: mpsc::Receiver, + pub status: String, + pub info: Option, + pub pending: bool, + pub dialog_open: bool, +} + +impl UpdateFeatureState { + pub(in crate::features) fn new() -> Self { + let (tx, rx) = mpsc::channel(); + Self { + tx, + rx, + status: format!("Current version {}", env!("CARGO_PKG_VERSION")), + info: None, + pending: false, + dialog_open: false, + } + } +} + +#[cfg(test)] +mod tests { + use super::UpdateFeatureState; + + #[test] + fn update_state_owns_job_channel_and_initial_status() { + let state = UpdateFeatureState::new(); + + assert!(state.status.contains(env!("CARGO_PKG_VERSION"))); + assert!(state.rx.try_recv().is_err()); + assert!(state.info.is_none()); + assert!(!state.pending); + assert!(!state.dialog_open); + } +} diff --git a/crates/nyaterm-desktop/src/features/settings/update_runtime.rs b/crates/nyaterm-desktop/src/features/update/update_runtime.rs similarity index 62% rename from crates/nyaterm-desktop/src/features/settings/update_runtime.rs rename to crates/nyaterm-desktop/src/features/update/update_runtime.rs index 7bfc14279..994c8bbc6 100644 --- a/crates/nyaterm-desktop/src/features/settings/update_runtime.rs +++ b/crates/nyaterm-desktop/src/features/update/update_runtime.rs @@ -1,31 +1,33 @@ use gpui::Context; -use crate::features::{NyaTermApp, UpdateJobResult}; +use crate::features::NyaTermApp; use crate::http::update::check_native_update; +use super::state::UpdateJobResult; + const UPDATE_EVENT_DRAIN_LIMIT: usize = 4; impl NyaTermApp { pub(in crate::features) fn open_update_dialog(&mut self, cx: &mut Context) { - self.update_dialog_open = true; + self.update.dialog_open = true; self.start_update_check(cx); } pub(in crate::features) fn close_update_dialog(&mut self, cx: &mut Context) { - self.update_dialog_open = false; + self.update.dialog_open = false; cx.notify(); } pub(in crate::features) fn start_update_check(&mut self, cx: &mut Context) { - if self.update_pending { - self.update_status = "update check already running".to_string(); + if self.update.pending { + self.update.status = "update check already running".to_string(); cx.notify(); return; } - self.update_pending = true; - self.update_status = "checking GitHub releases...".to_string(); - self.update_info = None; - let tx = self.update_tx.clone(); + self.update.pending = true; + self.update.status = "checking GitHub releases...".to_string(); + self.update.info = None; + let tx = self.update.tx.clone(); std::thread::spawn(move || { let result = check_native_update(); let _ = tx.send(UpdateJobResult { result }); @@ -34,19 +36,19 @@ impl NyaTermApp { } pub(in crate::features) fn drain_update_events(&mut self) -> bool { - if !self.update_pending { + if !self.update.pending { return false; } let mut dirty = false; for _ in 0..UPDATE_EVENT_DRAIN_LIMIT { - let Ok(event) = self.update_rx.try_recv() else { + let Ok(event) = self.update.rx.try_recv() else { break; }; dirty = true; - self.update_pending = false; + self.update.pending = false; match event.result { Ok(info) => { - self.update_status = if info.available { + self.update.status = if info.available { format!( "update available: {} -> {}", info.current_version, info.latest_version @@ -54,13 +56,13 @@ impl NyaTermApp { } else { format!("NyaTerm is up to date ({})", info.current_version) }; - self.terminal.view.status = self.update_status.clone(); - self.update_info = Some(info); + self.terminal.view.status = self.update.status.clone(); + self.update.info = Some(info); } Err(error) => { - self.update_status = format!("update check failed: {error}"); - self.terminal.view.status = self.update_status.clone(); - self.update_info = None; + self.update.status = format!("update check failed: {error}"); + self.terminal.view.status = self.update.status.clone(); + self.update.info = None; } } } diff --git a/docs/architecture/gpui-migration-status.md b/docs/architecture/gpui-migration-status.md index ae4d349c3..d75b08bd5 100644 --- a/docs/architecture/gpui-migration-status.md +++ b/docs/architecture/gpui-migration-status.md @@ -9,7 +9,7 @@ Last updated from the working tree on 2026-07-28. | Metric | Current value | Notes | | --- | ---: | --- | -| `NyaTermApp` fields | 229 | Counted from `features/app_state/mod.rs`; down from 585, still transitional. | +| `NyaTermApp` fields | 213 | 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. | @@ -202,6 +202,15 @@ these as staged extraction candidates, not as formatting-only refactor targets. one composition-root field. Persisted `AppSettingsSummary` and `KeywordHighlightConfig` remain on `NyaTermApp`; their save/load formats and storage paths are unchanged. +- Translation and native-update background state now have authoritative + `TranslationFeatureState` and `UpdateFeatureState` owners. Eighteen app fields + became two feature fields; each owner constructs and retains its own job + channel together with pending/status/result and dialog state. Translation + settings and the secret draft moved as one compatibility-sensitive unit, + while their existing load/save, masking and fallback paths are unchanged. + Native update runtime also moved out of the settings module into its own + normal module tree, and both feature-specific job event types left the shared + `runtime_jobs.rs` bucket. - Transfer state is grouped into `TransferFeatureState`. Seventy-eight fields turned out to be five separate things sharing one panel: the job `queue`, the SFTP `browser`, the file operation dialogs (`file_ops`), the built-in remote @@ -1345,14 +1354,15 @@ 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 229, across nine +3. Largely done. `NyaTermApp` is down from 585 fields to 213, across eleven feature-state structs. The latest cohesive cuts moved sixteen terminal command-assistance and credential-prompt fields into `TerminalFeatureState::assist`, then seventeen transient settings fields - into four `SettingsFeatureState` 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. + into four `SettingsFeatureState` children, then moved translation and native + update channels, job state and dialogs into two authoritative feature-state + owners. 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 did not buy. The rule: if a method only reads and writes one feature state, it belongs on that state, and the `NyaTermApp` method becomes a forwarder diff --git a/scripts/check-architecture-boundaries.sh b/scripts/check-architecture-boundaries.sh index 4a8b9712a..145ae24cd 100755 --- a/scripts/check-architecture-boundaries.sh +++ b/scripts/check-architecture-boundaries.sh @@ -118,6 +118,21 @@ check_no_matches \ '^[[:space:]]*pub\(in crate::features\)[[:space:]]+(search_engine_edit_index|search_engine_expanded_index|search_engine_icon_picker_index|search_engine_actions_index|search_engine_edit_field|search_engine_focus|keyword_highlight_expanded_id|keyword_highlight_edit_id|keyword_highlight_edit_field|keyword_highlight_focus|appearance_menu_open|appearance_ui_font_options|appearance_terminal_font_options|keybinding_recording_id|keybinding_pending_keys|keybinding_search_draft|keybindings_focus)[[:space:]]*:' \ crates/nyaterm-desktop/src/features/app_state/mod.rs +check_no_matches \ + "translation state must stay grouped under TranslationFeatureState" \ + '^[[:space:]]*pub\(in crate::features\)[[:space:]]+(translation_dialog|translate_tx|translate_rx|translate_provider|translation_settings|translation_secret_draft|translate_target_language|translate_input|translate_result|translate_status|translate_pending|translate_focused_field)[[:space:]]*:' \ + crates/nyaterm-desktop/src/features/app_state/mod.rs + +check_no_matches \ + "update state must stay grouped under UpdateFeatureState" \ + '^[[:space:]]*pub\(in crate::features\)[[:space:]]+(update_tx|update_rx|update_status|update_info|update_pending|update_dialog_open)[[:space:]]*:' \ + crates/nyaterm-desktop/src/features/app_state/mod.rs + +check_no_matches \ + "translation and update job events must stay in their owning features" \ + 'struct[[:space:]]+(TranslateJobResult|UpdateJobResult)' \ + crates/nyaterm-desktop/src/features/runtime_jobs.rs + # These low-frequency transport helpers have explicit imports at their call # sites. Keep them out of the shared feature prelude so new modules do not # acquire unrelated transport dependencies implicitly.