refactor: restructure project

This commit is contained in:
ancion
2026-06-13 19:21:33 +08:00
parent 77206bd126
commit 81b160285d
18 changed files with 254 additions and 246 deletions
+7
View File
@@ -0,0 +1,7 @@
pub(crate) const DEFAULT_COLS: u16 = 100;
pub(crate) const DEFAULT_ROWS: u16 = 30;
pub(crate) const SIDEBAR_WIDTH: f32 = 306.0;
pub(crate) const TAB_BAR_HEIGHT: f32 = 52.0;
pub(crate) const TERMINAL_PADDING_X: f32 = 32.0;
pub(crate) const TERMINAL_PADDING_Y: f32 = 32.0;
pub(crate) const TERMINAL_KEY_CONTEXT: &str = "AshellTerminal";
+1 -1
View File
@@ -19,7 +19,7 @@ use rust_i18n::t;
use crate::{
Ashell,
config::AuthMethod,
session::config::AuthMethod,
system::format_bytes,
};
+11 -3
View File
@@ -1,3 +1,9 @@
pub mod constants;
pub mod dialogs;
pub mod startup;
pub mod theme;
pub mod ui;
use std::{
cell::{Cell, RefCell},
collections::HashMap,
@@ -22,11 +28,11 @@ use rust_i18n::t;
use tokio::runtime::Runtime;
use crate::{
config::{AuthMethod, ConfigStore},
sftp::SftpHandle,
session::config::{AuthMethod, ConfigStore},
system::{SystemSampler, SystemSnapshot},
terminal::{self, BackendCommand, BackendEvent, TabKind, TerminalTab},
ssh_terminal,
backend::ssh,
};
#[derive(Clone, Debug)]
@@ -638,6 +644,8 @@ impl Ashell {
self.focused_pane_path = vec![];
self.active_tab = None;
self.active_group = None;
self.tab_groups.clear();
self.tabs.clear();
self.system_tab_id = None;
self.cpu_history.clear();
self.net_rx_history.clear();
@@ -799,7 +807,7 @@ impl Ashell {
let events = self.events_tx.clone();
let tab_id = tab_id.clone();
self.runtime.spawn(async move {
match ssh_terminal::sample_remote_system(session).await {
match ssh::sample_remote_system(session).await {
Ok(snapshot) => {
let _ = events.send(BackendEvent::RemoteSystem { tab_id, snapshot });
}
+166
View File
@@ -0,0 +1,166 @@
use gpui::{App, AppContext as _, Bounds, WindowOptions, point, px, size};
use gpui_component::Root;
use crate::session::config::ConfigStore;
use crate::Ashell;
#[cfg(target_os = "macos")]
pub(crate) fn sync_macos_launch_environment() {
let shell = std::env::var("SHELL").unwrap_or_else(|_| "/bin/zsh".to_string());
let Ok(output) = std::process::Command::new(&shell).args(["-l", "-c", "env -0"]).output() else {
return;
};
if !output.status.success() {
return;
}
for entry in output.stdout.split(|b| *b == 0) {
if entry.is_empty() {
continue;
}
let Some(eq) = entry.iter().position(|b| *b == b'=') else {
continue;
};
let Ok(key) = std::str::from_utf8(&entry[..eq]) else {
continue;
};
let Ok(value) = std::str::from_utf8(&entry[eq + 1..]) else {
continue;
};
let should_import = matches!(
key,
"PATH"
| "MANPATH"
| "INFOPATH"
| "LANG"
| "LC_ALL"
| "LC_CTYPE"
| "SHELL"
| "HOME"
| "HOMEBREW_PREFIX"
| "HOMEBREW_CELLAR"
| "HOMEBREW_REPOSITORY"
) || key.starts_with("LC_");
if should_import {
unsafe {
std::env::set_var(key, value);
}
}
}
}
#[cfg(not(target_os = "macos"))]
pub(crate) fn sync_macos_launch_environment() {}
pub(crate) fn open_main_window(cx: &mut App) {
let mut window_options = WindowOptions::default();
#[cfg(not(target_os = "macos"))]
if let Ok(img) = image::load_from_memory(include_bytes!("../../assets/icons/ashell.png")) {
window_options.icon = Some(std::sync::Arc::new(img.into_rgba8()));
}
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::session::config::SavedWindowBounds::Fullscreen {
x,
y,
width,
height,
} => gpui::WindowBounds::Fullscreen(Bounds::new(
point(px(*x), px(*y)),
size(px(*width), px(*height)),
)),
crate::session::config::SavedWindowBounds::Maximized {
x,
y,
width,
height,
} => gpui::WindowBounds::Maximized(Bounds::new(
point(px(*x), px(*y)),
size(px(*width), px(*height)),
)),
crate::session::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;
let x = display_bounds.origin.x + (display_bounds.size.width - width) / 2.0;
#[cfg(target_os = "macos")]
let y = display_bounds.origin.y;
#[cfg(not(target_os = "macos"))]
let y = display_bounds.origin.y + (display_bounds.size.height - height) / 2.0;
window_options.window_bounds = Some(gpui::WindowBounds::Windowed(Bounds::new(
point(x, y),
size(width, height),
)));
}
cx.open_window(window_options, |window, cx| {
window.activate_window();
window.set_window_title("ashell");
gpui_component::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::session::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::session::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::session::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");
}
+29 -1
View File
@@ -1,4 +1,5 @@
use gpui::{Anchor, Context, IntoElement, SharedString, Window, px};
use anyhow::{Context as _, Result};
use gpui::{Anchor, App, Context, IntoElement, SharedString, Window, px};
use gpui_component::{
ActiveTheme as _, IconName, Sizable as _, Theme, ThemeMode, ThemeRegistry,
button::{Button, ButtonVariants as _},
@@ -8,6 +9,33 @@ use rust_i18n::t;
use crate::Ashell;
pub(crate) const EMBEDDED_THEME_JSONS: &[&str] = &[
include_str!("../../assets/themes/matrix.json"),
include_str!("../../assets/themes/tokyonight.json"),
include_str!("../../assets/themes/gruvbox.json"),
include_str!("../../assets/themes/solarized.json"),
];
pub(crate) fn load_fonts(cx: &mut App) -> Result<()> {
let regular =
std::borrow::Cow::Borrowed(include_bytes!("../../assets/fonts/MapleMono-NF-CN-Regular.ttf").as_slice());
let bold = std::borrow::Cow::Borrowed(include_bytes!("../../assets/fonts/MapleMono-NF-CN-Bold.ttf").as_slice());
cx.text_system()
.add_fonts(vec![regular, bold])
.context("load Maple Mono NF CN fonts")?;
set_theme_font_names(cx.global_mut::<Theme>(), ".SystemUIFont");
Ok(())
}
pub(crate) fn load_embedded_themes(cx: &mut App) {
let registry = ThemeRegistry::global_mut(cx);
for theme_json in EMBEDDED_THEME_JSONS {
if let Err(err) = registry.load_themes_from_str(theme_json) {
tracing::warn!("failed to load embedded theme: {err:#}");
}
}
}
pub(crate) fn set_theme_font_names(theme: &mut Theme, ui_font_family: &str) {
theme.font_family = ui_font_family.into();
theme.mono_font_family = ui_font_family.into();
+5 -5
View File
@@ -22,12 +22,12 @@ use gpui_component::{
use rust_i18n::t;
use crate::{
Ashell, PaneLayout, SIDEBAR_WIDTH, TERMINAL_KEY_CONTEXT,
sftp_ops::is_editable_text_file,
Ashell, PaneLayout,
app::constants::{SIDEBAR_WIDTH, TERMINAL_KEY_CONTEXT},
sftp::ops::is_editable_text_file,
sftp::format_mtime,
system::format_bytes,
terminal::{TabKind, TerminalTab},
terminal_element,
terminal::{self, TabKind, TerminalTab},
};
impl Ashell {
@@ -1574,7 +1574,7 @@ impl Ashell {
this.focus_pane_with_id(tab_id_clone2.clone());
cx.notify();
}))
.child(terminal_element::TerminalElement::new(
.child(terminal::element::TerminalElement::new(
cx.entity(),
focus_handle,
snapshot,
+2
View File
@@ -0,0 +1,2 @@
pub mod local;
pub mod ssh;
+1 -1
View File
@@ -14,7 +14,7 @@ use russh::{
use tokio::sync::mpsc;
use crate::{
config::{AuthMethod, Session},
session::config::{AuthMethod, Session},
system::{SystemSnapshot, remote_snapshot_from_kv},
terminal::{BackendCommand, BackendEvent, BackendTx},
};
+11 -222
View File
@@ -1,235 +1,25 @@
#![windows_subsystem = "windows"]
use anyhow::{Context as _, Result};
use gpui::{App, AppContext as _, Bounds, KeyBinding, WindowOptions, point, px, size};
use gpui_component::{Root, Theme, ThemeRegistry};
use gpui::{ KeyBinding };
use gpui_component_assets::Assets;
mod config;
mod local_terminal;
mod app;
mod backend;
mod session;
mod sftp;
mod ssh_terminal;
mod system;
mod terminal;
mod terminal_element;
mod terminal_input;
mod app;
mod session;
mod sftp_ops;
mod theme;
mod dialogs;
mod ui;
use config::ConfigStore;
rust_i18n::i18n!("locales", fallback = "en");
const DEFAULT_COLS: u16 = 100;
const DEFAULT_ROWS: u16 = 30;
const SIDEBAR_WIDTH: f32 = 306.0;
const TAB_BAR_HEIGHT: f32 = 52.0;
const TERMINAL_PADDING_X: f32 = 32.0;
const TERMINAL_PADDING_Y: f32 = 32.0;
const TERMINAL_KEY_CONTEXT: &str = "AshellTerminal";
const EMBEDDED_THEME_JSONS: &[&str] = &[
include_str!("../assets/themes/matrix.json"),
include_str!("../assets/themes/tokyonight.json"),
include_str!("../assets/themes/gruvbox.json"),
include_str!("../assets/themes/solarized.json"),
];
gpui::actions!(ashell_terminal, [TerminalTabKey, TerminalBacktabKey]);
pub(crate) use app::{
Ashell, ConnectionProgress, PaneLayout, SelectorEntry, SftpContextMenuState, TabGroup,
};
fn load_fonts(cx: &mut App) -> Result<()> {
let regular =
std::borrow::Cow::Borrowed(include_bytes!("../assets/fonts/MapleMono-NF-CN-Regular.ttf").as_slice());
let bold = std::borrow::Cow::Borrowed(include_bytes!("../assets/fonts/MapleMono-NF-CN-Bold.ttf").as_slice());
cx.text_system()
.add_fonts(vec![regular, bold])
.context("load Maple Mono NF CN fonts")?;
// At startup we don't have a config yet, so pass the default UI font family.
// It will be reapplied in Ashell::new() -> apply_theme_preferences.
theme::set_theme_font_names(cx.global_mut::<Theme>(), ".SystemUIFont");
Ok(())
}
fn load_embedded_themes(cx: &mut App) {
let registry = ThemeRegistry::global_mut(cx);
for theme_json in EMBEDDED_THEME_JSONS {
if let Err(err) = registry.load_themes_from_str(theme_json) {
tracing::warn!("failed to load embedded theme: {err:#}");
}
}
}
#[cfg(target_os = "macos")]
fn sync_macos_launch_environment() {
let shell = std::env::var("SHELL").unwrap_or_else(|_| "/bin/zsh".to_string());
let Ok(output) = std::process::Command::new(&shell).args(["-l", "-c", "env -0"]).output() else {
return;
};
if !output.status.success() {
return;
}
for entry in output.stdout.split(|b| *b == 0) {
if entry.is_empty() {
continue;
}
let Some(eq) = entry.iter().position(|b| *b == b'=') else {
continue;
};
let Ok(key) = std::str::from_utf8(&entry[..eq]) else {
continue;
};
let Ok(value) = std::str::from_utf8(&entry[eq + 1..]) else {
continue;
};
let should_import = matches!(
key,
"PATH"
| "MANPATH"
| "INFOPATH"
| "LANG"
| "LC_ALL"
| "LC_CTYPE"
| "SHELL"
| "HOME"
| "HOMEBREW_PREFIX"
| "HOMEBREW_CELLAR"
| "HOMEBREW_REPOSITORY"
) || key.starts_with("LC_");
if should_import {
unsafe {
std::env::set_var(key, value);
}
}
}
}
#[cfg(not(target_os = "macos"))]
fn sync_macos_launch_environment() {}
fn open_main_window(cx: &mut App) {
let mut window_options = WindowOptions::default();
#[cfg(not(target_os = "macos"))]
if let Ok(img) = image::load_from_memory(include_bytes!("../assets/icons/ashell.png")) {
window_options.icon = Some(std::sync::Arc::new(img.into_rgba8()));
}
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;
let x = display_bounds.origin.x + (display_bounds.size.width - width) / 2.0;
#[cfg(target_os = "macos")]
let y = display_bounds.origin.y;
#[cfg(not(target_os = "macos"))]
let y = display_bounds.origin.y + (display_bounds.size.height - height) / 2.0;
window_options.window_bounds = Some(gpui::WindowBounds::Windowed(Bounds::new(
point(x, y),
size(width, height),
)));
}
cx.open_window(window_options, |window, cx| {
window.activate_window();
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");
}
fn main() {
sync_macos_launch_environment();
app::startup::sync_macos_launch_environment();
tracing_subscriber::fmt()
.with_env_filter(
tracing_subscriber::EnvFilter::try_from_default_env()
@@ -246,20 +36,19 @@ fn main() {
let app = gpui_platform::application().with_assets(Assets);
app.on_reopen(|cx| {
if cx.windows().is_empty() {
open_main_window(cx);
app::startup::open_main_window(cx);
}
});
app.run(move |cx| {
gpui_component::init(cx);
cx.bind_keys([
KeyBinding::new("tab", TerminalTabKey, Some(TERMINAL_KEY_CONTEXT)),
KeyBinding::new("shift-tab", TerminalBacktabKey, Some(TERMINAL_KEY_CONTEXT)),
KeyBinding::new("tab", TerminalTabKey, Some(app::constants::TERMINAL_KEY_CONTEXT)),
KeyBinding::new("shift-tab", TerminalBacktabKey, Some(app::constants::TERMINAL_KEY_CONTEXT)),
]);
load_embedded_themes(cx);
if let Err(err) = load_fonts(cx) {
app::theme::load_embedded_themes(cx);
if let Err(err) = app::theme::load_fonts(cx) {
tracing::warn!("failed to load embedded fonts: {err:#}");
}
open_main_window(cx);
app::startup::open_main_window(cx);
});
}
+13 -10
View File
@@ -1,3 +1,5 @@
pub mod config;
use gpui::{
App, AppContext as _, Context, Entity, KeyDownEvent, MouseButton,
MouseDownEvent, MouseMoveEvent, SharedString, Window, px,
@@ -9,21 +11,20 @@ use gpui_component::{
use rust_i18n::t;
use uuid::Uuid;
use self::config::{AuthMethod, Session};
use crate::{
Ashell, ConnectionProgress, PaneLayout, SelectorEntry, TabGroup,
config::{AuthMethod, Session},
local_terminal,
backend::{local, ssh},
sftp,
ssh_terminal,
terminal::{BackendCommand, RenderSnapshot, TabKind, TerminalTab},
DEFAULT_COLS, DEFAULT_ROWS, SIDEBAR_WIDTH, TAB_BAR_HEIGHT, TERMINAL_PADDING_X,
TERMINAL_PADDING_Y,
app::constants::{DEFAULT_COLS, DEFAULT_ROWS, SIDEBAR_WIDTH, TAB_BAR_HEIGHT, TERMINAL_PADDING_X, TERMINAL_PADDING_Y},
};
impl Ashell {
pub(crate) fn open_local(&mut self, cx: &mut Context<Self>) {
let id = Uuid::new_v4().to_string();
match local_terminal::spawn_local_terminal(
match local::spawn_local_terminal(
id.clone(),
DEFAULT_COLS,
DEFAULT_ROWS,
@@ -221,7 +222,7 @@ impl Ashell {
if let Err(err) = self.config.save() {
tracing::warn!("failed to save UI font family: {err:#}");
}
crate::theme::set_theme_font_names(Theme::global_mut(cx), &self.ui_font_family);
crate::app::theme::set_theme_font_names(Theme::global_mut(cx), &self.ui_font_family);
cx.notify();
window.refresh();
}
@@ -346,7 +347,7 @@ impl Ashell {
pub(crate) fn open_ssh_session(&mut self, session: Session, cx: &mut Context<Self>) {
let id = Uuid::new_v4().to_string();
let backend = ssh_terminal::spawn_ssh_terminal(
let backend = ssh::spawn_ssh_terminal(
self.runtime.handle(),
id.clone(),
session.clone(),
@@ -528,6 +529,8 @@ impl Ashell {
self.focused_pane_path = vec![];
self.active_tab = None;
self.active_group = None;
self.tab_groups.clear();
self.tabs.clear();
self.system_tab_id = None;
self.cpu_history.clear();
self.net_rx_history.clear();
@@ -718,7 +721,7 @@ impl Ashell {
let new_id = Uuid::new_v4().to_string();
let mut tab = match current_tab.kind {
TabKind::Local => {
match local_terminal::spawn_local_terminal(
match local::spawn_local_terminal(
new_id.clone(),
DEFAULT_COLS,
DEFAULT_ROWS,
@@ -743,7 +746,7 @@ impl Ashell {
cx.notify();
return;
};
let backend = ssh_terminal::spawn_ssh_terminal(
let backend = ssh::spawn_ssh_terminal(
self.runtime.handle(),
new_id.clone(),
session.clone(),
+4 -2
View File
@@ -1,3 +1,5 @@
pub mod ops;
use std::{
fs,
path::{Path, PathBuf},
@@ -27,7 +29,7 @@ use zip::read::ZipArchive;
use rust_i18n::t;
use crate::{
config::{AuthMethod, Session},
session::config::{AuthMethod, Session},
terminal::BackendEvent,
};
@@ -496,7 +498,7 @@ async fn run_sftp(
}
SftpCommand::EditFile { remote_path } => {
let id = uuid::Uuid::new_v4().to_string();
let config = crate::config::ConfigStore::load().unwrap();
let config = crate::session::config::ConfigStore::load().unwrap();
let tmp_dir = config.tmp_dir().unwrap_or_else(|| PathBuf::from("/tmp"));
let base = base_name(&remote_path);
let local_path = tmp_dir.join(format!("{}-{}", id, base));
View File
View File
+4 -1
View File
@@ -1,3 +1,6 @@
pub mod element;
pub mod input;
use std::sync::mpsc::Sender;
use alacritty_terminal::{
@@ -10,7 +13,7 @@ use alacritty_terminal::{
};
use gpui::Keystroke;
use crate::config::Session;
use crate::session::config::Session;
use crate::sftp::{PreviewData, RemoteEntry};
use crate::system::SystemSnapshot;