refactor(desktop): group background feature state

This commit is contained in:
Kang
2026-07-28 20:35:14 +08:00
parent d2cf0bdbdb
commit fffed38026
24 changed files with 337 additions and 221 deletions
@@ -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::<HashMap<_, _>>();
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,
@@ -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<CommandPersistenceRequest>,
pub(in crate::features) command_persistence_rx: mpsc::Receiver<CommandPersistenceResult>,
@@ -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<TranslationDialogState>,
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<TunnelJobResult>,
pub(in crate::features) tunnel_rx: mpsc::Receiver<TunnelJobResult>,
pub(in crate::features) pending_tunnels: Vec<String>,
pub(in crate::features) translate_tx: mpsc::Sender<TranslateJobResult>,
pub(in crate::features) translate_rx: mpsc::Receiver<TranslateJobResult>,
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<TranslateResult>,
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<UpdateJobResult>,
pub(in crate::features) update_rx: mpsc::Receiver<UpdateJobResult>,
pub(in crate::features) update_status: String,
pub(in crate::features) update_info: Option<NativeUpdateInfo>,
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<WindowHandle<RemoteFileEditorWindow>>,
pub(in crate::features) remote_editor_window_open_pending: bool,
@@ -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")
+4 -1
View File
@@ -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,
@@ -36,25 +36,25 @@ impl NyaTermApp {
cx: &mut Context<Self>,
) -> 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);
}),
)
@@ -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" => {
@@ -13,11 +13,11 @@ impl NyaTermApp {
cx: &mut Context<Self>,
) -> 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| {
+3 -3
View File
@@ -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| {
@@ -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<RemoteStats, String>,
}
#[derive(Debug)]
pub(in crate::features) struct TranslateJobResult {
pub(in crate::features) result: Result<TranslateResult, String>,
}
#[derive(Debug)]
pub(in crate::features) struct UpdateJobResult {
pub(in crate::features) result: Result<NativeUpdateInfo, String>,
}
#[derive(Debug)]
pub(in crate::features) enum CommandPersistenceRequest {
AppendHistory(Vec<String>),
@@ -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
@@ -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};
@@ -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);
@@ -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
@@ -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;
}
@@ -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
@@ -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;
@@ -1,3 +1,6 @@
//! Translation dialog runtime.
mod state;
mod translation_runtime;
pub(in crate::features) use state::TranslationFeatureState;
@@ -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<TranslateResult, String>,
}
pub(in crate::features) struct TranslationFeatureState {
pub dialog: Option<TranslationDialogState>,
pub(super) tx: mpsc::Sender<TranslateJobResult>,
pub(super) rx: mpsc::Receiver<TranslateJobResult>,
pub provider: String,
pub settings: TranslationSettings,
pub secret_draft: TranslationSecretDraft,
pub target_language: String,
pub input: String,
pub result: Option<TranslateResult>,
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());
}
}
@@ -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<Self>,
) {
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<Self>) {
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>,
) {
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<Self>) {
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<Self>,
) -> 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();
}
}
}
@@ -0,0 +1,6 @@
//! Native application update state and runtime.
mod state;
mod update_runtime;
pub(in crate::features) use state::UpdateFeatureState;
@@ -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<NativeUpdateInfo, String>,
}
pub(in crate::features) struct UpdateFeatureState {
pub(super) tx: mpsc::Sender<UpdateJobResult>,
pub(super) rx: mpsc::Receiver<UpdateJobResult>,
pub status: String,
pub info: Option<NativeUpdateInfo>,
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);
}
}
@@ -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>) {
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>) {
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<Self>) {
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;
}
}
}
+16 -6
View File
@@ -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
+15
View File
@@ -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.