feat: fix Windows flashing, UI thread blocking, and idle redraws (fixes #90)

This commit is contained in:
TomZz
2026-07-22 23:58:17 +08:00
parent e7ca7bdc23
commit 3b9ee02cce
8 changed files with 233 additions and 123 deletions
Generated
+11
View File
@@ -363,6 +363,7 @@ dependencies = [
"tracing-subscriber",
"uuid",
"walkdir",
"winreg 0.52.0",
"winres",
"zip",
]
@@ -9643,6 +9644,16 @@ dependencies = [
"winapi",
]
[[package]]
name = "winreg"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a277a57398d4bfa075df44f501a17cfdf8542d224f0d36095a2adc7aee4ef0a5"
dependencies = [
"cfg-if",
"windows-sys 0.48.0",
]
[[package]]
name = "winreg"
version = "0.55.0"
+3
View File
@@ -58,6 +58,9 @@ sha2 = "0.10"
[dev-dependencies]
libc = "0.2"
[target.'cfg(target_os = "windows")'.dependencies]
winreg = "0.52"
[package.metadata.deb]
maintainer = "ashell contributors"
license-file = ["LICENSE"]
+12 -14
View File
@@ -1425,9 +1425,7 @@ impl Ashell {
this.recording_action = None;
this.keybind_error = None;
this.config.set_key_binding(&action, &new_key);
if let Err(err) = this.config.save() {
tracing::error!("failed to save key binding: {err:#}");
}
this.save_preferences_background();
cx.notify();
});
}
@@ -1600,7 +1598,7 @@ impl Ashell {
.checked(current_style == crate::session::config::TitleBarStyle::Native)
.on_click(window.listener_for(&view, |this, _, _, cx| {
this.config.set_title_bar_style(crate::session::config::TitleBarStyle::Native);
let _ = this.config.save();
this.save_preferences_background();
cx.notify();
}))
)
@@ -1609,7 +1607,7 @@ impl Ashell {
.checked(current_style == crate::session::config::TitleBarStyle::Integrated)
.on_click(window.listener_for(&view, |this, _, _, cx| {
this.config.set_title_bar_style(crate::session::config::TitleBarStyle::Integrated);
let _ = this.config.save();
this.save_preferences_background();
cx.notify();
}))
);
@@ -1844,7 +1842,7 @@ impl Ashell {
.checked(view.read(cx).config.right_click_copy_paste())
.on_click(window.listener_for(&view, |this, checked, _, cx| {
this.config.set_right_click_copy_paste(*checked);
let _ = this.config.save();
this.save_preferences_background();
cx.notify();
}))
.into_any_element()
@@ -1863,7 +1861,7 @@ impl Ashell {
.checked(view.read(cx).config.keyword_highlight())
.on_click(window.listener_for(&view, |this, checked, _, cx| {
this.config.set_keyword_highlight(*checked);
let _ = this.config.save();
this.save_preferences_background();
cx.notify();
}))
.into_any_element()
@@ -1882,7 +1880,7 @@ impl Ashell {
.checked(view.read(cx).config.lock_layout())
.on_click(window.listener_for(&view, |this, checked, _, cx| {
this.config.set_lock_layout(*checked);
let _ = this.config.save();
this.save_preferences_background();
cx.notify();
}))
.into_any_element()
@@ -1919,7 +1917,7 @@ impl Ashell {
.checked(pos == "Bottom")
.on_click(window.listener_for(&view, |this, _, _window, cx| {
this.config.set_monitoring_position("Bottom");
let _ = this.config.save();
this.save_preferences_background();
cx.notify();
}))
)
@@ -1928,7 +1926,7 @@ impl Ashell {
.checked(pos == "Sidebar")
.on_click(window.listener_for(&view, |this, _, _window, cx| {
this.config.set_monitoring_position("Sidebar");
let _ = this.config.save();
this.save_preferences_background();
cx.notify();
}))
)
@@ -1937,7 +1935,7 @@ impl Ashell {
.checked(pos == "Hidden")
.on_click(window.listener_for(&view, |this, _, _window, cx| {
this.config.set_monitoring_position("Hidden");
let _ = this.config.save();
this.save_preferences_background();
cx.notify();
}))
);
@@ -2109,7 +2107,7 @@ impl Ashell {
.checked(view.read(cx).config.use_proxy())
.on_click(window.listener_for(&view, |this, checked, _, cx| {
this.config.set_use_proxy(*checked);
let _ = this.config.save();
this.save_preferences_background();
cx.notify();
}))
.into_any_element()
@@ -2128,7 +2126,7 @@ impl Ashell {
.checked(view.read(cx).config.read_env_proxy())
.on_click(window.listener_for(&view, |this, checked, _, cx| {
this.config.set_read_env_proxy(*checked);
let _ = this.config.save();
this.save_preferences_background();
cx.notify();
}))
.into_any_element()
@@ -2197,7 +2195,7 @@ impl Ashell {
this.config.set_global_proxy_port(port);
this.config.set_global_proxy_user(user);
this.config.set_global_proxy_password(password);
let _ = this.config.save();
this.save_preferences_background();
cx.notify();
}))
)
+28 -10
View File
@@ -327,6 +327,8 @@ pub(crate) struct Ashell {
pub(crate) hovered_url: Option<HoveredUrl>,
pub(crate) cmd_ctrl_pressed: bool,
pub(crate) _subscriptions: Vec<gpui::Subscription>,
pub(crate) save_lock: std::sync::Arc<tokio::sync::Mutex<()>>,
pub(crate) save_latest_seq: std::sync::Arc<std::sync::atomic::AtomicU64>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
@@ -718,6 +720,8 @@ impl Ashell {
hovered_url: None,
cmd_ctrl_pressed: false,
_subscriptions,
save_lock: std::sync::Arc::new(tokio::sync::Mutex::new(())),
save_latest_seq: std::sync::Arc::new(std::sync::atomic::AtomicU64::new(0)),
};
this.apply_theme_preferences(window, cx);
@@ -784,9 +788,29 @@ impl Ashell {
cx.notify();
}
pub(crate) fn save_preferences_background(&mut self) {
let local_config = self.config.cache.clone();
let config_store = self.config.clone();
let latest_seq = self.save_latest_seq.clone();
let current_seq = latest_seq.fetch_add(1, std::sync::atomic::Ordering::SeqCst) + 1;
let save_lock = self.save_lock.clone();
self.runtime.spawn(async move {
let _guard = save_lock.lock().await;
if current_seq < latest_seq.load(std::sync::atomic::Ordering::SeqCst) {
return;
}
let _ = tokio::task::spawn_blocking(move || {
if let Err(err) = config_store.save_merged_preferences(local_config) {
tracing::error!("failed to save merged preferences in background: {err:#}");
}
})
.await;
});
}
pub(crate) fn start_event_pump(&self, cx: &mut Context<Self>) {
cx.spawn(async move |this, cx| {
let mut idle_frames = 0u32;
let mut last_blink_time = std::time::Instant::now();
loop {
cx.background_executor()
@@ -808,16 +832,9 @@ impl Ashell {
>= std::time::Duration::from_millis(600);
if changed || system_sampled || blink_due {
cx.notify();
idle_frames = 0;
if blink_due {
last_blink_time = now;
}
} else {
idle_frames += 1;
if idle_frames >= 60 {
cx.notify();
idle_frames = 0;
}
}
})
.is_err()
@@ -1331,8 +1348,9 @@ impl Ashell {
};
let size = bounds.size;
if size.width.as_f32() > 400.0 && size.height.as_f32() > 300.0 {
tracing::info!("[ui] saving layout state...");
let mut config = ConfigStore::load().unwrap_or_else(|_| ConfigStore::in_memory());
self.save_latest_seq
.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
let mut config = self.config.clone();
let saved_bounds = match current_bounds {
gpui::WindowBounds::Fullscreen(b) => {
crate::session::config::SavedWindowBounds::Fullscreen {
+2 -6
View File
@@ -126,9 +126,7 @@ impl Ashell {
}
rust_i18n::set_locale(&active_locale);
gpui_component::set_locale(&active_locale);
if let Err(err) = self.config.save() {
tracing::warn!("failed to save language preferences: {err:#}");
}
self.save_preferences_background();
window.refresh();
cx.notify();
}
@@ -168,8 +166,6 @@ impl Ashell {
self.light_theme_name.to_string(),
self.dark_theme_name.to_string(),
);
if let Err(err) = self.config.save() {
tracing::warn!("failed to save theme preferences: {err:#}");
}
self.save_preferences_background();
}
}
+5 -5
View File
@@ -111,7 +111,7 @@ impl Ashell {
}
self.config
.set_sftp_panel_minimized(self.sftp_panel_minimized);
let _ = self.config.save();
self.save_preferences_background();
cx.notify();
}
@@ -293,7 +293,7 @@ impl Ashell {
.on_click(cx.listener(|this, checked, _, cx| {
this.show_hidden_files = *checked;
this.config.set_show_hidden_files(*checked);
let _ = this.config.save();
this.save_preferences_background();
cx.notify();
})),
)
@@ -1593,7 +1593,7 @@ impl Ashell {
.on_click(cx.listener(|this, _, _, cx| {
this.sidebar_collapsed = true;
this.config.set_sidebar_collapsed(true);
let _ = this.config.save();
this.save_preferences_background();
cx.notify();
})),
)
@@ -1833,7 +1833,7 @@ impl Ashell {
.on_click(cx.listener(|this, _, _, cx| {
this.sidebar_collapsed = false;
this.config.set_sidebar_collapsed(false);
let _ = this.config.save();
this.save_preferences_background();
cx.notify();
})),
),
@@ -2797,7 +2797,7 @@ impl Render for Ashell {
.on_action(cx.listener(|this, _: &crate::ToggleSidebar, _, cx| {
this.sidebar_collapsed = !this.sidebar_collapsed;
this.config.set_sidebar_collapsed(this.sidebar_collapsed);
let _ = this.config.save();
this.save_preferences_background();
cx.notify();
}))
.on_action(cx.listener(|this, _: &crate::ToggleSftpZoom, window, cx| {
+166 -72
View File
@@ -1,4 +1,4 @@
use std::{fs, path::PathBuf};
use std::{fs, path::PathBuf, sync::OnceLock};
use anyhow::{Context, Result};
use argon2::Argon2;
@@ -364,9 +364,10 @@ impl Default for ConfigFile {
}
}
#[derive(Clone)]
pub struct ConfigStore {
path: PathBuf,
cache: ConfigFile,
pub(crate) path: PathBuf,
pub(crate) cache: ConfigFile,
}
impl ConfigStore {
@@ -828,6 +829,65 @@ impl ConfigStore {
Ok(())
}
pub fn save_merged_preferences(&self, local_config: ConfigFile) -> Result<()> {
if self.path.as_os_str().is_empty() {
return Ok(());
}
let hardware_uuid = get_hardware_uuid();
let mut disk_config = if self.path.exists() {
if let Ok(raw_bytes) = fs::read(&self.path) {
match decrypt_config(&raw_bytes, &hardware_uuid) {
Ok(loaded) => loaded,
Err(_) => serde_json::from_slice::<ConfigFile>(&raw_bytes)
.unwrap_or_else(|_| self.cache.clone()),
}
} else {
self.cache.clone()
}
} else {
self.cache.clone()
};
// Merge UI preference fields
disk_config.follow_system_theme = local_config.follow_system_theme;
disk_config.theme_mode = local_config.theme_mode;
disk_config.light_theme_name = local_config.light_theme_name;
disk_config.dark_theme_name = local_config.dark_theme_name;
disk_config.locale = local_config.locale;
disk_config.terminal_font_size = local_config.terminal_font_size;
disk_config.ui_font_size = local_config.ui_font_size;
disk_config.right_click_copy_paste = local_config.right_click_copy_paste;
disk_config.keyword_highlight = local_config.keyword_highlight;
disk_config.ui_font_family = local_config.ui_font_family;
disk_config.terminal_font_family = local_config.terminal_font_family;
disk_config.title_bar_style = local_config.title_bar_style;
disk_config.cursor_style = local_config.cursor_style;
disk_config.window_bounds = local_config.window_bounds;
disk_config.workspace_panels = local_config.workspace_panels;
disk_config.body_panels = local_config.body_panels;
disk_config.show_hidden_files = local_config.show_hidden_files;
disk_config.lock_layout = local_config.lock_layout;
disk_config.monitoring_position = local_config.monitoring_position;
disk_config.sidebar_collapsed = local_config.sidebar_collapsed;
disk_config.sftp_panel_minimized = local_config.sftp_panel_minimized;
let encrypted_bytes = encrypt_config(&disk_config, &hardware_uuid)?;
fs::write(&self.path, encrypted_bytes)
.with_context(|| format!("failed to write {}", self.path.display()))?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
if let Ok(mut perms) = fs::metadata(&self.path).map(|m| m.permissions()) {
perms.set_mode(0o600);
let _ = fs::set_permissions(&self.path, perms);
}
}
Ok(())
}
}
pub trait ProxyStream:
@@ -839,8 +899,6 @@ impl<T: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin + Send + Sync + 'st
{
}
use std::sync::OnceLock;
#[derive(Debug, Clone)]
pub struct EnvProxy {
pub proxy_type: String,
@@ -1033,64 +1091,60 @@ struct EncryptedConfigEnvelope {
payload: String,
}
fn get_hardware_uuid() -> String {
#[cfg(target_os = "macos")]
{
if let Ok(output) = std::process::Command::new("ioreg")
.args(&["-rd1", "-c", "IOPlatformExpertDevice"])
.output()
{
let stdout = String::from_utf8_lossy(&output.stdout);
for line in stdout.lines() {
if line.contains("IOPlatformUUID") {
if let Some(uuid) = line.split('"').nth(3) {
let uuid = uuid.trim().to_string();
if !uuid.is_empty() {
return uuid;
static HARDWARE_UUID_CACHE: OnceLock<String> = OnceLock::new();
pub fn get_hardware_uuid() -> String {
HARDWARE_UUID_CACHE
.get_or_init(|| {
#[cfg(target_os = "macos")]
{
if let Ok(output) = std::process::Command::new("ioreg")
.args(&["-rd1", "-c", "IOPlatformExpertDevice"])
.output()
{
let stdout = String::from_utf8_lossy(&output.stdout);
for line in stdout.lines() {
if line.contains("IOPlatformUUID") {
if let Some(uuid) = line.split('"').nth(3) {
let uuid = uuid.trim().to_string();
if !uuid.is_empty() {
return uuid;
}
}
}
}
}
}
}
}
#[cfg(target_os = "linux")]
{
if let Ok(uuid) = std::fs::read_to_string("/sys/class/dmi/id/product_uuid") {
let uuid = uuid.trim().to_string();
if !uuid.is_empty() {
return uuid;
#[cfg(target_os = "linux")]
{
if let Ok(uuid) = std::fs::read_to_string("/sys/class/dmi/id/product_uuid") {
let uuid = uuid.trim().to_string();
if !uuid.is_empty() {
return uuid;
}
}
if let Ok(id) = std::fs::read_to_string("/etc/machine-id") {
let id = id.trim().to_string();
if !id.is_empty() {
return id;
}
}
if let Ok(id) = std::fs::read_to_string("/var/lib/dbus/machine-id") {
let id = id.trim().to_string();
if !id.is_empty() {
return id;
}
}
}
}
if let Ok(id) = std::fs::read_to_string("/etc/machine-id") {
let id = id.trim().to_string();
if !id.is_empty() {
return id;
}
}
if let Ok(id) = std::fs::read_to_string("/var/lib/dbus/machine-id") {
let id = id.trim().to_string();
if !id.is_empty() {
return id;
}
}
}
#[cfg(target_os = "windows")]
{
if let Ok(output) = std::process::Command::new("reg")
.args(&[
"query",
"HKLM\\SOFTWARE\\Microsoft\\Cryptography",
"/v",
"MachineGuid",
])
.output()
{
let stdout = String::from_utf8_lossy(&output.stdout);
for line in stdout.lines() {
if line.contains("MachineGuid") {
if let Some(guid) = line.split_whitespace().last() {
#[cfg(target_os = "windows")]
{
use winreg::RegKey;
use winreg::enums::HKEY_LOCAL_MACHINE;
let hklm = RegKey::predef(HKEY_LOCAL_MACHINE);
if let Ok(subkey) = hklm.open_subkey("SOFTWARE\\Microsoft\\Cryptography") {
if let Ok(guid) = subkey.get_value::<String, _>("MachineGuid") {
let guid = guid.trim().to_string();
if !guid.is_empty() {
return guid;
@@ -1098,23 +1152,10 @@ fn get_hardware_uuid() -> String {
}
}
}
}
if let Ok(output) = std::process::Command::new("wmic")
.args(&["csproduct", "get", "uuid"])
.output()
{
let stdout = String::from_utf8_lossy(&output.stdout);
let lines: Vec<&str> = stdout.lines().collect();
if lines.len() >= 2 {
let uuid = lines[1].trim().to_string();
if !uuid.is_empty() {
return uuid;
}
}
}
}
"ashell-default-hardware-uuid-fallback".to_string()
"ashell-default-hardware-uuid-fallback".to_string()
})
.clone()
}
fn encrypt_config(config: &ConfigFile, password: &str) -> Result<Vec<u8>> {
@@ -1207,4 +1248,57 @@ mod tests {
// Decrypt with wrong password should fail
assert!(decrypt_config(&encrypted, "wrong-password").is_err());
}
#[test]
fn test_save_merged_preferences() {
let temp_dir = std::env::temp_dir();
let path = temp_dir.join(format!("ashell-test-config-{}.json", Uuid::new_v4()));
let mut store = ConfigStore {
path: path.clone(),
cache: ConfigFile::default(),
};
let session = Session {
id: "test-session-id".to_string(),
name: "Test Session".to_string(),
host: "1.2.3.4".to_string(),
port: 22,
user: "root".to_string(),
auth: AuthMethod::Password,
password: "pwd".to_string(),
private_key_path: String::new(),
private_key_inline: String::new(),
passphrase: String::new(),
last_used: None,
proxy_type: String::new(),
proxy_host: String::new(),
proxy_port: None,
proxy_user: String::new(),
proxy_password: String::new(),
protocol: "ssh".to_string(),
baud_rate: 115200,
};
store.cache.sessions.push(session.clone());
store.save().unwrap();
let mut local_config = ConfigFile::default();
local_config.ui_font_size = 18.0;
local_config.terminal_font_size = 20.0;
local_config.show_hidden_files = true;
store.save_merged_preferences(local_config).unwrap();
let loaded_bytes = fs::read(&path).unwrap();
let decrypted = decrypt_config(&loaded_bytes, &get_hardware_uuid()).unwrap();
assert_eq!(decrypted.ui_font_size, 18.0);
assert_eq!(decrypted.terminal_font_size, 20.0);
assert!(decrypted.show_hidden_files);
assert_eq!(decrypted.sessions.len(), 1);
assert_eq!(decrypted.sessions[0].name, "Test Session");
assert_eq!(decrypted.sessions[0].host, "1.2.3.4");
let _ = fs::remove_file(&path);
}
}
+6 -16
View File
@@ -371,9 +371,7 @@ impl Ashell {
pub(crate) fn change_terminal_font_size(&mut self, delta: f32, cx: &mut Context<Self>) {
self.terminal_font_size = (self.terminal_font_size + delta).clamp(10.0, 24.0);
self.config.set_terminal_font_size(self.terminal_font_size);
if let Err(err) = self.config.save() {
tracing::warn!("failed to save terminal font size: {err:#}");
}
self.save_preferences_background();
self.status = format!("terminal font size: {:.0}px", self.terminal_font_size).into();
cx.notify();
}
@@ -381,9 +379,7 @@ impl Ashell {
pub(crate) fn change_ui_font_size(&mut self, delta: f32, cx: &mut Context<Self>) {
self.ui_font_size = (self.ui_font_size + delta).clamp(8.0, 24.0);
self.config.set_ui_font_size(self.ui_font_size);
if let Err(err) = self.config.save() {
tracing::warn!("failed to save UI font size: {err:#}");
}
self.save_preferences_background();
Theme::global_mut(cx).font_size = px(self.ui_font_size);
self.status = format!("UI font size: {:.0}px", self.ui_font_size).into();
cx.notify();
@@ -397,9 +393,7 @@ impl Ashell {
) {
self.ui_font_family = family.into();
self.config.set_ui_font_family(family);
if let Err(err) = self.config.save() {
tracing::warn!("failed to save UI font family: {err:#}");
}
self.save_preferences_background();
crate::app::theme::set_theme_font_names(Theme::global_mut(cx), &self.ui_font_family);
cx.notify();
window.refresh();
@@ -408,9 +402,7 @@ impl Ashell {
pub(crate) fn change_terminal_font_family(&mut self, family: &str, cx: &mut Context<Self>) {
self.terminal_font_family = family.into();
self.config.set_terminal_font_family(family);
if let Err(err) = self.config.save() {
tracing::warn!("failed to save terminal font family: {err:#}");
}
self.save_preferences_background();
cx.notify();
}
@@ -421,15 +413,13 @@ impl Ashell {
) {
self.cursor_style = style;
self.config.set_cursor_style(style);
if let Err(err) = self.config.save() {
tracing::warn!("failed to save cursor style: {err:#}");
}
self.save_preferences_background();
cx.notify();
}
pub(crate) fn reset_layout(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
self.config.set_layout_state(None, None, None);
let _ = self.config.save();
self.save_preferences_background();
self.is_layout_reset = true;
self.workspace_panels = cx.new(|_| crate::app::resizable::ResizableState::default());