mirror of
https://github.com/rust-kotlin/ashell.git
synced 2026-10-03 16:00:18 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7504bf6448 | ||
|
|
09ecd67ca6 | ||
|
|
4c5148fccf |
@@ -156,4 +156,5 @@ jobs:
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
files: ${{ env.ASSET }}
|
||||
generate_release_notes: true
|
||||
fail_on_unmatched_files: true
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -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: "布局占比已恢复默认。窗口大小将在下次启动时生效。"
|
||||
|
||||
@@ -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
@@ -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
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user