Compare commits

..
3 Commits
Author SHA1 Message Date
TomZz 7504bf6448 feat: persist window bounds and layout sizes across restarts 2026-06-11 17:27:37 +08:00
TomZz 09ecd67ca6 feat: localize sftp status messages 2026-06-11 17:02:46 +08:00
TomZz 4c5148fccf ci: auto-generate release notes with commits since last release
- Enabled `generate_release_notes: true` in the release workflow. This will automatically include the changelog (commits and PRs) between the current and previous release tags on GitHub.
2026-06-11 02:36:11 +08:00
6 changed files with 215 additions and 20 deletions
+1
View File
@@ -156,4 +156,5 @@ jobs:
uses: softprops/action-gh-release@v2
with:
files: ${{ env.ASSET }}
generate_release_notes: true
fail_on_unmatched_files: true
+16
View File
@@ -58,3 +58,19 @@ local_terminal_opened: "local terminal opened"
connecting: "Connecting"
starting_connection: "starting connection..."
connection_failed: "Connection Failed"
preview_failed: "Preview failed: %{err}"
downloading_file: "Downloading %{base}..."
download_failed: "Download failed: %{err}"
uploading: "Uploading..."
upload_failed: "Upload failed: %{err}"
downloaded_folder: "Downloaded folder to %{path}"
downloaded_file: "Downloaded file to %{path}"
uploaded_file: "Uploaded file"
uploaded_folder: "Uploaded folder"
uploaded_n_files: "Uploaded %{files} files"
uploaded_n_folders: "Uploaded %{folders} folders"
uploaded_files_and_folders: "Uploaded %{files} files and %{folders} folders"
reset: "Reset"
reset_layout: "Reset Default Layout"
reset_layout_success: "Layout reset. Window size takes effect on restart."
+16
View File
@@ -58,3 +58,19 @@ local_terminal_opened: "已打开本地终端"
connecting: "正在连接"
starting_connection: "开始连接..."
connection_failed: "连接失败"
preview_failed: "预览失败: %{err}"
downloading_file: "正在下载 %{base}..."
download_failed: "下载失败: %{err}"
uploading: "正在上传..."
upload_failed: "上传失败: %{err}"
downloaded_folder: "已下载文件夹至 %{path}"
downloaded_file: "已下载文件至 %{path}"
uploaded_file: "已上传文件"
uploaded_folder: "已上传文件夹"
uploaded_n_files: "已上传 %{files} 个文件"
uploaded_n_folders: "已上传 %{folders} 个文件夹"
uploaded_files_and_folders: "已上传 %{files} 个文件及 %{folders} 个文件夹"
reset: "重置"
reset_layout: "恢复默认布局"
reset_layout_success: "布局占比已恢复默认。窗口大小将在下次启动时生效。"
+52
View File
@@ -70,6 +70,29 @@ impl Session {
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum SavedWindowBounds {
Fullscreen {
x: f32,
y: f32,
width: f32,
height: f32,
},
Maximized {
x: f32,
y: f32,
width: f32,
height: f32,
},
Windowed {
x: f32,
y: f32,
width: f32,
height: f32,
},
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct ConfigFile {
#[serde(default)]
@@ -86,6 +109,12 @@ pub struct ConfigFile {
pub terminal_font_size: f32,
#[serde(default)]
pub sessions: Vec<Session>,
#[serde(default)]
pub window_bounds: Option<SavedWindowBounds>,
#[serde(default)]
pub workspace_panels: Option<Vec<f32>>,
#[serde(default)]
pub body_panels: Option<Vec<f32>>,
}
fn default_locale() -> String {
@@ -189,6 +218,29 @@ impl ConfigStore {
self.cache.dark_theme_name = dark_theme_name.into();
}
pub fn window_bounds(&self) -> Option<&SavedWindowBounds> {
self.cache.window_bounds.as_ref()
}
pub fn workspace_panels(&self) -> Option<&Vec<f32>> {
self.cache.workspace_panels.as_ref()
}
pub fn body_panels(&self) -> Option<&Vec<f32>> {
self.cache.body_panels.as_ref()
}
pub fn set_layout_state(
&mut self,
window_bounds: Option<SavedWindowBounds>,
workspace_panels: Option<Vec<f32>>,
body_panels: Option<Vec<f32>>,
) {
self.cache.window_bounds = window_bounds;
self.cache.workspace_panels = workspace_panels;
self.cache.body_panels = body_panels;
}
pub fn set_terminal_font_size(&mut self, terminal_font_size: f32) {
self.cache.terminal_font_size = terminal_font_size.max(10.0);
}
+109 -6
View File
@@ -1035,7 +1035,7 @@ impl Ashell {
window.open_dialog(cx, move |dialog: Dialog, _window, _| {
dialog
.title(t!("settings").to_string())
.w(px(480.))
.w(px(560.))
.content({
let view = view.clone();
move |content, window, cx| {
@@ -1048,7 +1048,7 @@ impl Ashell {
.gap_3()
.child(
div()
.w(px(180.))
.w(px(240.))
.child(t!("terminal_font_size").to_string()),
)
.child(Button::new("font-size-down").label("-").on_click(
@@ -1070,7 +1070,7 @@ impl Ashell {
h_flex()
.items_center()
.gap_3()
.child(div().w(px(180.)).child(t!("language").to_string()))
.child(div().w(px(240.)).child(t!("language").to_string()))
.child(
Button::new("language-dropdown")
.small()
@@ -1144,6 +1144,24 @@ impl Ashell {
}),
),
)
.child(
h_flex()
.items_center()
.gap_3()
.child(
div().w(px(240.)).child(t!("reset_layout").to_string()),
)
.child(
Button::new("reset-layout")
.label(t!("reset").to_string())
.on_click(window.listener_for(
&view,
|this, _, window, cx| {
this.reset_layout(window, cx);
},
)),
),
)
.child(
div()
.text_size(px(12.))
@@ -1156,6 +1174,17 @@ impl Ashell {
});
}
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.workspace_panels = cx.new(|_| gpui_component::resizable::ResizableState::default());
self.body_panels = cx.new(|_| gpui_component::resizable::ResizableState::default());
self.status = t!("reset_layout_success").into();
cx.notify();
}
fn set_ssh_auth_method(&mut self, method: AuthMethod, cx: &mut Context<Self>) {
self.ssh_auth_method = method;
cx.notify();
@@ -3194,7 +3223,7 @@ impl Render for Ashell {
.with_state(&self.workspace_panels)
.child(
resizable_panel()
.size(px(SIDEBAR_WIDTH))
.size(px(self.config.workspace_panels().and_then(|s| s.first().copied()).unwrap_or(SIDEBAR_WIDTH)))
.size_range(px(240.)..px(520.))
.flex_none()
.child(self.sidebar(cx)),
@@ -3349,7 +3378,7 @@ impl Render for Ashell {
)
.child(
resizable_panel()
.size(px(248.))
.size(px(self.config.body_panels().and_then(|s| s.get(1).copied()).unwrap_or(248.0)))
.size_range(px(180.)..px(420.))
.child(self.render_sftp_panel(window, cx)),
),
@@ -3576,7 +3605,38 @@ fn open_main_window(cx: &mut App) {
window_options.icon = Some(std::sync::Arc::new(img.into_rgba8()));
}
if let Some(display) = cx.displays().first().cloned() {
let config = ConfigStore::load().unwrap_or_else(|_| ConfigStore::in_memory());
if let Some(bounds) = config.window_bounds() {
window_options.window_bounds = Some(match bounds {
crate::config::SavedWindowBounds::Fullscreen {
x,
y,
width,
height,
} => gpui::WindowBounds::Fullscreen(Bounds::new(
point(px(*x), px(*y)),
size(px(*width), px(*height)),
)),
crate::config::SavedWindowBounds::Maximized {
x,
y,
width,
height,
} => gpui::WindowBounds::Maximized(Bounds::new(
point(px(*x), px(*y)),
size(px(*width), px(*height)),
)),
crate::config::SavedWindowBounds::Windowed {
x,
y,
width,
height,
} => gpui::WindowBounds::Windowed(Bounds::new(
point(px(*x), px(*y)),
size(px(*width), px(*height)),
)),
});
} else if let Some(display) = cx.displays().first().cloned() {
let display_bounds = display.bounds();
let width = display_bounds.size.width * 0.8;
let height = display_bounds.size.height * 0.9;
@@ -3599,6 +3659,49 @@ fn open_main_window(cx: &mut App) {
window.set_window_title("ashell");
Theme::sync_system_appearance(Some(window), cx);
let view = cx.new(|cx| Ashell::new(window, cx));
let workspace_panels_clone = view.read(cx).workspace_panels.clone();
let body_panels_clone = view.read(cx).body_panels.clone();
window.on_window_should_close(cx, move |window: &mut gpui::Window, cx: &mut gpui::App| {
let mut config = ConfigStore::load().unwrap_or_else(|_| ConfigStore::in_memory());
let current_bounds = window.window_bounds();
let saved_bounds = match current_bounds {
gpui::WindowBounds::Fullscreen(b) => crate::config::SavedWindowBounds::Fullscreen {
x: b.origin.x.into(),
y: b.origin.y.into(),
width: b.size.width.into(),
height: b.size.height.into(),
},
gpui::WindowBounds::Maximized(b) => crate::config::SavedWindowBounds::Maximized {
x: b.origin.x.into(),
y: b.origin.y.into(),
width: b.size.width.into(),
height: b.size.height.into(),
},
gpui::WindowBounds::Windowed(b) => crate::config::SavedWindowBounds::Windowed {
x: b.origin.x.into(),
y: b.origin.y.into(),
width: b.size.width.into(),
height: b.size.height.into(),
},
};
let workspace_sizes: Vec<f32> = workspace_panels_clone
.read(cx)
.sizes()
.iter()
.map(|s| s.into())
.collect();
let body_sizes: Vec<f32> = body_panels_clone
.read(cx)
.sizes()
.iter()
.map(|s| s.into())
.collect();
config.set_layout_state(Some(saved_bounds), Some(workspace_sizes), Some(body_sizes));
let _ = config.save();
true
});
cx.new(|cx| Root::new(view, window, cx))
})
.expect("failed to open window");
+21 -14
View File
@@ -24,6 +24,8 @@ use uuid::Uuid;
use walkdir::WalkDir;
use zip::read::ZipArchive;
use rust_i18n::t;
use crate::{
config::{AuthMethod, Session},
terminal::BackendEvent,
@@ -185,7 +187,7 @@ async fn run_sftp(
Err(err) => {
let _ = events.send(BackendEvent::SftpStatus {
tab_id: tab_id.clone(),
text: format!("preview failed: {err:#}"),
text: t!("preview_failed", err = format!("{err:#}")).into(),
});
}
},
@@ -193,19 +195,19 @@ async fn run_sftp(
let base = base_name(&remote);
let _ = events.send(BackendEvent::SftpStatus {
tab_id: tab_id.clone(),
text: format!("downloading {base}..."),
text: t!("downloading_file", base = base).into(),
});
match download_path_impl(&handle, &sftp, &remote, Path::new(&local_dir)).await {
Ok(summary) => {
let _ = events.send(BackendEvent::SftpStatus {
tab_id: tab_id.clone(),
text: summary,
text: summary.into(),
});
}
Err(err) => {
let _ = events.send(BackendEvent::SftpStatus {
tab_id: tab_id.clone(),
text: format!("download failed: {err:#}"),
text: t!("download_failed", err = format!("{err:#}")).into(),
});
}
}
@@ -213,20 +215,20 @@ async fn run_sftp(
SftpCommand::UploadPaths { locals, remote_dir } => {
let _ = events.send(BackendEvent::SftpStatus {
tab_id: tab_id.clone(),
text: "uploading...".into(),
text: t!("uploading").into(),
});
match upload_paths_impl(&sftp, &locals, &remote_dir).await {
Ok(summary) => {
let _ = events.send(BackendEvent::SftpStatus {
tab_id: tab_id.clone(),
text: summary,
text: summary.into(),
});
let _ = emit_entries(&events, &tab_id, &sftp, &remote_dir).await;
}
Err(err) => {
let _ = events.send(BackendEvent::SftpStatus {
tab_id: tab_id.clone(),
text: format!("upload failed: {err:#}"),
text: t!("upload_failed", err = format!("{err:#}")).into(),
});
}
}
@@ -543,12 +545,12 @@ async fn download_path_impl(
));
let extracted_to =
download_remote_directory_archive(handle, sftp, remote, &local_archive).await?;
return Ok(format!("downloaded folder to {}", extracted_to.display()));
return Ok(t!("downloaded_folder", path = extracted_to.display()).to_string());
}
let local_path = local_dir.join(base_name(remote));
download_file_impl(sftp, remote, &local_path).await?;
Ok(format!("downloaded file to {}", local_path.display()))
Ok(t!("downloaded_file", path = local_path.display()).to_string())
}
#[allow(dead_code)]
@@ -660,11 +662,16 @@ async fn upload_paths_impl(
}
}
let summary = match (file_count, folder_count) {
(1, 0) => "uploaded file".to_string(),
(0, 1) => "uploaded folder".to_string(),
(files, 0) => format!("uploaded {files} files"),
(0, folders) => format!("uploaded {folders} folders"),
(files, folders) => format!("uploaded {files} files and {folders} folders"),
(1, 0) => t!("uploaded_file").to_string(),
(0, 1) => t!("uploaded_folder").to_string(),
(files, 0) => t!("uploaded_n_files", files = files).to_string(),
(0, folders) => t!("uploaded_n_folders", folders = folders).to_string(),
(files, folders) => t!(
"uploaded_files_and_folders",
files = files,
folders = folders
)
.to_string(),
};
Ok(summary)
}