mirror of
https://github.com/rust-kotlin/ashell.git
synced 2026-10-03 16:00:18 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
108a9cf95b | ||
|
|
9885b2573a | ||
|
|
cdf4cc3d53 | ||
|
|
5b381045f9 | ||
|
|
f246c952a2 | ||
|
|
2276eef191 | ||
|
|
bb39626173 | ||
|
|
2cb8ab50aa | ||
|
|
785670c428 | ||
|
|
d33babe552 | ||
|
|
985f88fb1a |
@@ -34,7 +34,7 @@ jobs:
|
||||
target: aarch64-apple-darwin
|
||||
name: macos-aarch64
|
||||
bin: ashell
|
||||
- os: macos-13 # Intel
|
||||
- os: macos-14 # Intel (cross-compiled from Apple Silicon to avoid queue delays)
|
||||
target: x86_64-apple-darwin
|
||||
name: macos-x86_64
|
||||
bin: ashell
|
||||
@@ -86,6 +86,7 @@ jobs:
|
||||
mkdir -p "$CONTENTS/MacOS" "$CONTENTS/Resources"
|
||||
|
||||
cp "target/${{ matrix.target }}/release/ashell" "$CONTENTS/MacOS/"
|
||||
cp "assets/icons/ashell.icns" "$CONTENTS/Resources/ashell.icns"
|
||||
|
||||
VERSION_NUM="${VERSION#v}"
|
||||
cat > "$CONTENTS/Info.plist" <<EOF
|
||||
@@ -98,6 +99,8 @@ jobs:
|
||||
<string>en</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>ashell</string>
|
||||
<key>CFBundleIconFile</key>
|
||||
<string>ashell.icns</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>dev.ashell.app</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
|
||||
Generated
+95
-19
@@ -1,6 +1,6 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 3
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "accesskit"
|
||||
@@ -296,15 +296,18 @@ dependencies = [
|
||||
"gpui-component",
|
||||
"gpui-component-assets",
|
||||
"gpui_platform",
|
||||
"image",
|
||||
"menu",
|
||||
"portable-pty",
|
||||
"rfd",
|
||||
"russh",
|
||||
"russh-keys",
|
||||
"russh-sftp",
|
||||
"rust-i18n 3.1.5",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"ssh-key",
|
||||
"sys-locale",
|
||||
"sysinfo 0.33.1",
|
||||
"tar",
|
||||
"thiserror 1.0.69",
|
||||
@@ -313,6 +316,7 @@ dependencies = [
|
||||
"tracing-subscriber",
|
||||
"uuid",
|
||||
"walkdir",
|
||||
"winres",
|
||||
"zip",
|
||||
]
|
||||
|
||||
@@ -753,7 +757,7 @@ dependencies = [
|
||||
"bitflags 2.12.1",
|
||||
"cexpr",
|
||||
"clang-sys",
|
||||
"itertools 0.11.0",
|
||||
"itertools 0.13.0",
|
||||
"log",
|
||||
"prettyplease",
|
||||
"proc-macro2",
|
||||
@@ -1682,7 +1686,7 @@ dependencies = [
|
||||
"libc",
|
||||
"option-ext",
|
||||
"redox_users 0.5.2",
|
||||
"windows-sys 0.60.2",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1942,7 +1946,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2563,7 +2567,7 @@ dependencies = [
|
||||
"log",
|
||||
"presser",
|
||||
"thiserror 2.0.18",
|
||||
"windows 0.58.0",
|
||||
"windows 0.62.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2697,7 +2701,7 @@ dependencies = [
|
||||
"paste",
|
||||
"regex",
|
||||
"ropey",
|
||||
"rust-i18n",
|
||||
"rust-i18n 4.1.0",
|
||||
"schemars",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -3258,7 +3262,7 @@ dependencies = [
|
||||
"js-sys",
|
||||
"log",
|
||||
"wasm-bindgen",
|
||||
"windows-core 0.58.0",
|
||||
"windows-core 0.62.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4122,7 +4126,7 @@ version = "0.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "536bfad37a309d62069485248eeaba1e8d9853aaf951caaeaed0585a95346f08"
|
||||
dependencies = [
|
||||
"windows-sys 0.60.2",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4308,7 +4312,7 @@ version = "0.50.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
|
||||
dependencies = [
|
||||
"windows-sys 0.60.2",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5984,6 +5988,20 @@ dependencies = [
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rust-i18n"
|
||||
version = "3.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fda2551fdfaf6cc5ee283adc15e157047b92ae6535cf80f6d4962d05717dc332"
|
||||
dependencies = [
|
||||
"globwalk",
|
||||
"once_cell",
|
||||
"regex",
|
||||
"rust-i18n-macro 3.1.5",
|
||||
"rust-i18n-support 3.1.5",
|
||||
"smallvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rust-i18n"
|
||||
version = "4.1.0"
|
||||
@@ -5992,11 +6010,28 @@ checksum = "55691a65892c33ee2de49c15ea5600c6f4a70e8eeb8e6c3cd96d2a231d230c40"
|
||||
dependencies = [
|
||||
"globwalk",
|
||||
"regex",
|
||||
"rust-i18n-macro",
|
||||
"rust-i18n-support",
|
||||
"rust-i18n-macro 4.1.0",
|
||||
"rust-i18n-support 4.1.0",
|
||||
"smallvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rust-i18n-macro"
|
||||
version = "3.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "22baf7d7f56656d23ebe24f6bb57a5d40d2bce2a5f1c503e692b5b2fa450f965"
|
||||
dependencies = [
|
||||
"glob",
|
||||
"once_cell",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"rust-i18n-support 3.1.5",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_yaml",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rust-i18n-macro"
|
||||
version = "4.1.0"
|
||||
@@ -6006,13 +6041,36 @@ dependencies = [
|
||||
"glob",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"rust-i18n-support",
|
||||
"rust-i18n-support 4.1.0",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_yaml",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rust-i18n-support"
|
||||
version = "3.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "940ed4f52bba4c0152056d771e563b7133ad9607d4384af016a134b58d758f19"
|
||||
dependencies = [
|
||||
"arc-swap",
|
||||
"base62",
|
||||
"globwalk",
|
||||
"itertools 0.11.0",
|
||||
"lazy_static",
|
||||
"normpath",
|
||||
"once_cell",
|
||||
"proc-macro2",
|
||||
"regex",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_yaml",
|
||||
"siphasher",
|
||||
"toml 0.8.23",
|
||||
"triomphe",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rust-i18n-support"
|
||||
version = "4.1.0"
|
||||
@@ -6069,7 +6127,7 @@ dependencies = [
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys 0.4.15",
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6082,7 +6140,7 @@ dependencies = [
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys 0.12.1",
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6737,7 +6795,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.60.2",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6845,7 +6903,7 @@ dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"psm",
|
||||
"windows-sys 0.60.2",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -7205,7 +7263,7 @@ dependencies = [
|
||||
"getrandom 0.4.2",
|
||||
"once_cell",
|
||||
"rustix 1.1.4",
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -7425,6 +7483,15 @@ dependencies = [
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml"
|
||||
version = "0.5.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml"
|
||||
version = "0.8.23"
|
||||
@@ -7682,7 +7749,7 @@ checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e"
|
||||
dependencies = [
|
||||
"memoffset 0.9.1",
|
||||
"tempfile",
|
||||
"windows-sys 0.60.2",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -8442,7 +8509,7 @@ version = "0.1.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
|
||||
dependencies = [
|
||||
"windows-sys 0.48.0",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -9079,6 +9146,15 @@ dependencies = [
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winres"
|
||||
version = "0.1.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b68db261ef59e9e52806f688020631e987592bd83619edccda9c47d42cde4f6c"
|
||||
dependencies = [
|
||||
"toml 0.5.11",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winsafe"
|
||||
version = "0.0.19"
|
||||
|
||||
@@ -24,6 +24,8 @@ rfd = "0.15"
|
||||
russh = "0.49"
|
||||
russh-keys = "0.49"
|
||||
russh-sftp = "2"
|
||||
rust-i18n = "3"
|
||||
sys-locale = "0.3"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
ssh-key = "0.6"
|
||||
@@ -36,9 +38,13 @@ tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||
uuid = { version = "1", features = ["v4", "serde"] }
|
||||
walkdir = "2"
|
||||
zip = { version = "2", default-features = false, features = ["deflate"] }
|
||||
image = { version = "0.25.9", default-features = false, features = ["png"] }
|
||||
|
||||
[profile.release]
|
||||
opt-level = 3
|
||||
lto = "thin"
|
||||
codegen-units = 1
|
||||
strip = true
|
||||
|
||||
[build-dependencies]
|
||||
winres = "0.1.12"
|
||||
|
||||
+10
-1
@@ -10,7 +10,16 @@ This project focuses on providing a high-performance and visually appealing shel
|
||||
|
||||
## Download
|
||||
|
||||
You can download the latest pre-compiled releases for macOS and Windows from the [GitHub Releases page](https://github.com/rust-kotlin/ashell/releases/latest).
|
||||
You can download the latest pre-compiled releases for macOS, Windows, and Linux from the [GitHub Releases page](https://github.com/rust-kotlin/ashell/releases/latest).
|
||||
|
||||
## Mac Installation Guide
|
||||
|
||||
After downloading, please unzip the file and move `ashell.app` to your **Applications** folder.
|
||||
Since the app uses ad-hoc signing, macOS may warn that the app is "damaged" upon first launch. If this happens, open Terminal and run the following command:
|
||||
|
||||
```bash
|
||||
sudo xattr -cr /Applications/ashell.app
|
||||
```
|
||||
|
||||
## Features
|
||||
|
||||
|
||||
@@ -10,7 +10,16 @@
|
||||
|
||||
## 下载
|
||||
|
||||
您可以从 [GitHub Releases 页面](https://github.com/rust-kotlin/ashell/releases/latest) 下载 macOS 和 Windows 版本的最新预编译程序。
|
||||
您可以从 [GitHub Releases 页面](https://github.com/rust-kotlin/ashell/releases/latest) 下载 macOS、Windows 和 Linux 版本的最新预编译程序。
|
||||
|
||||
## Mac 安装指南
|
||||
|
||||
下载并解压后,请先将 `ashell.app` 拖入或移动到 **应用程序 (Applications)** 目录。
|
||||
由于应用采用本地签名,初次启动时如果系统提示“App 已损坏,无法打开”,请打开终端(Terminal)并执行以下命令:
|
||||
|
||||
```bash
|
||||
sudo xattr -cr /Applications/ashell.app
|
||||
```
|
||||
|
||||
## 功能特性
|
||||
|
||||
|
||||
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 110 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 437 KiB |
@@ -0,0 +1,8 @@
|
||||
fn main() {
|
||||
#[cfg(windows)]
|
||||
{
|
||||
let mut res = winres::WindowsResource::new();
|
||||
res.set_icon("assets/icons/ashell.ico");
|
||||
res.compile().unwrap();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
remote_files: "REMOTE FILES"
|
||||
open_ssh_tab_sftp: "Open an SSH tab to browse SFTP files."
|
||||
sftp_connecting: "sftp connecting..."
|
||||
refresh: "Refresh"
|
||||
upload_file: "Upload File"
|
||||
upload_folder: "Upload Folder"
|
||||
hidden: "Hidden"
|
||||
download: "Download"
|
||||
download_count: "Download (%{count})"
|
||||
name: "NAME"
|
||||
size: "SIZE"
|
||||
modified: "MODIFIED"
|
||||
new_ssh_connection: "New SSH Connection"
|
||||
create_or_edit_ssh_session: "Create or edit an SSH session"
|
||||
local_terminal: "Local Terminal"
|
||||
open_local_shell_tab: "Open a new local shell tab"
|
||||
connect: "Connect"
|
||||
edit: "Edit"
|
||||
delete: "Delete"
|
||||
host: "Host"
|
||||
port: "Port"
|
||||
user: "User"
|
||||
password: "Password"
|
||||
key: "Key"
|
||||
private_key_path: "Private Key Path"
|
||||
private_key_data: "Private Key Data"
|
||||
save_and_connect: "Save & Connect"
|
||||
save: "Save"
|
||||
cancel: "Cancel"
|
||||
host_and_user_required: "Host and user are required"
|
||||
language: "Language"
|
||||
follow_system: "Follow System"
|
||||
light_theme: "Light Theme"
|
||||
dark_theme: "Dark Theme"
|
||||
use_light_mode: "Use Light Mode"
|
||||
use_dark_mode: "Use Dark Mode"
|
||||
english: "English"
|
||||
chinese: "简体中文"
|
||||
idle_no_session: "idle / No session"
|
||||
system: "SYSTEM"
|
||||
cpu: "CPU"
|
||||
mem: "MEM"
|
||||
swap: "SWAP"
|
||||
net: "NET"
|
||||
disk: "DISK"
|
||||
global_load: "global load"
|
||||
live: "Live"
|
||||
idle: "idle"
|
||||
saved: "SAVED"
|
||||
add_ssh: "+ ssh"
|
||||
open_local_or_ssh: "Open a local terminal or pick an SSH session."
|
||||
open_session: "Open Session"
|
||||
settings: "Settings"
|
||||
terminal_font_size: "Terminal Font Size"
|
||||
theme_management_hint: "Theme mode and light/dark theme families are managed from the top-bar theme button."
|
||||
local_host: "local host"
|
||||
local_terminal_opened: "local terminal opened"
|
||||
connecting: "Connecting"
|
||||
starting_connection: "starting connection..."
|
||||
connection_failed: "Connection Failed"
|
||||
@@ -0,0 +1,60 @@
|
||||
remote_files: "远程文件"
|
||||
open_ssh_tab_sftp: "打开一个 SSH 会话面板以浏览 SFTP 文件。"
|
||||
sftp_connecting: "正在连接 SFTP..."
|
||||
refresh: "刷新"
|
||||
upload_file: "上传文件"
|
||||
upload_folder: "上传文件夹"
|
||||
hidden: "显示隐藏文件"
|
||||
download: "下载"
|
||||
download_count: "下载 (%{count})"
|
||||
name: "名称"
|
||||
size: "大小"
|
||||
modified: "修改时间"
|
||||
new_ssh_connection: "新建 SSH 连接"
|
||||
create_or_edit_ssh_session: "创建或编辑一个 SSH 会话"
|
||||
local_terminal: "本地终端"
|
||||
open_local_shell_tab: "打开一个新的本地终端页面"
|
||||
connect: "连接"
|
||||
edit: "编辑"
|
||||
delete: "删除"
|
||||
host: "主机 (Host)"
|
||||
port: "端口 (Port)"
|
||||
user: "用户 (User)"
|
||||
password: "密码 (Password)"
|
||||
key: "密钥 (Key)"
|
||||
private_key_path: "私钥路径"
|
||||
private_key_data: "私钥内容"
|
||||
save_and_connect: "保存并连接"
|
||||
save: "保存"
|
||||
cancel: "取消"
|
||||
host_and_user_required: "主机名和用户名不能为空"
|
||||
language: "显示语言"
|
||||
follow_system: "跟随系统"
|
||||
light_theme: "浅色主题"
|
||||
dark_theme: "深色主题"
|
||||
use_light_mode: "使用浅色模式"
|
||||
use_dark_mode: "使用深色模式"
|
||||
english: "English"
|
||||
chinese: "简体中文"
|
||||
idle_no_session: "空闲 / 未连接会话"
|
||||
system: "系统"
|
||||
cpu: "CPU"
|
||||
mem: "内存"
|
||||
swap: "虚拟内存"
|
||||
net: "网络"
|
||||
disk: "磁盘"
|
||||
global_load: "全局负载"
|
||||
live: "实时"
|
||||
idle: "空闲"
|
||||
saved: "已保存会话"
|
||||
add_ssh: "+ SSH"
|
||||
open_local_or_ssh: "打开本地终端或选择一个 SSH 会话。"
|
||||
open_session: "打开会话"
|
||||
settings: "设置"
|
||||
terminal_font_size: "终端字体大小"
|
||||
theme_management_hint: "主题模式和亮/暗主题系列可通过顶栏的主题按钮管理。"
|
||||
local_host: "本地主机"
|
||||
local_terminal_opened: "已打开本地终端"
|
||||
connecting: "正在连接"
|
||||
starting_connection: "开始连接..."
|
||||
connection_failed: "连接失败"
|
||||
@@ -17,6 +17,8 @@ rm -rf "$APP_DIR"
|
||||
mkdir -p "$MACOS_DIR" "$RESOURCES_DIR"
|
||||
cp "$ROOT_DIR/target/release/$APP_NAME" "$MACOS_DIR/$APP_NAME"
|
||||
|
||||
cp "$ROOT_DIR/assets/icons/ashell.icns" "$RESOURCES_DIR/ashell.icns"
|
||||
|
||||
cat > "$CONTENTS_DIR/Info.plist" <<EOF
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
|
||||
@@ -27,6 +29,8 @@ cat > "$CONTENTS_DIR/Info.plist" <<EOF
|
||||
<string>en</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$APP_NAME</string>
|
||||
<key>CFBundleIconFile</key>
|
||||
<string>ashell.icns</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$BUNDLE_ID</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
|
||||
+28
-1
@@ -75,15 +75,23 @@ pub struct ConfigFile {
|
||||
#[serde(default)]
|
||||
pub follow_system_theme: bool,
|
||||
#[serde(default)]
|
||||
pub theme_mode: String,
|
||||
#[serde(default)]
|
||||
pub light_theme_name: String,
|
||||
#[serde(default)]
|
||||
pub dark_theme_name: String,
|
||||
#[serde(default = "default_locale")]
|
||||
pub locale: String,
|
||||
#[serde(default = "default_terminal_font_size")]
|
||||
pub terminal_font_size: f32,
|
||||
#[serde(default)]
|
||||
pub sessions: Vec<Session>,
|
||||
}
|
||||
|
||||
fn default_locale() -> String {
|
||||
"system".to_string()
|
||||
}
|
||||
|
||||
fn default_terminal_font_size() -> f32 {
|
||||
13.0
|
||||
}
|
||||
@@ -136,6 +144,10 @@ impl ConfigStore {
|
||||
self.cache.follow_system_theme
|
||||
}
|
||||
|
||||
pub fn theme_mode(&self) -> &str {
|
||||
&self.cache.theme_mode
|
||||
}
|
||||
|
||||
pub fn light_theme_name(&self) -> &str {
|
||||
&self.cache.light_theme_name
|
||||
}
|
||||
@@ -144,6 +156,18 @@ impl ConfigStore {
|
||||
&self.cache.dark_theme_name
|
||||
}
|
||||
|
||||
pub fn locale(&self) -> &str {
|
||||
if self.cache.locale.is_empty() {
|
||||
"system"
|
||||
} else {
|
||||
&self.cache.locale
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_locale(&mut self, locale: &str) {
|
||||
self.cache.locale = locale.to_string();
|
||||
}
|
||||
|
||||
pub fn terminal_font_size(&self) -> f32 {
|
||||
if self.cache.terminal_font_size <= 0.0 {
|
||||
default_terminal_font_size()
|
||||
@@ -155,10 +179,12 @@ impl ConfigStore {
|
||||
pub fn set_theme_preferences(
|
||||
&mut self,
|
||||
follow_system_theme: bool,
|
||||
theme_mode: impl Into<String>,
|
||||
light_theme_name: impl Into<String>,
|
||||
dark_theme_name: impl Into<String>,
|
||||
) {
|
||||
self.cache.follow_system_theme = follow_system_theme;
|
||||
self.cache.theme_mode = theme_mode.into();
|
||||
self.cache.light_theme_name = light_theme_name.into();
|
||||
self.cache.dark_theme_name = dark_theme_name.into();
|
||||
}
|
||||
@@ -188,6 +214,7 @@ impl ConfigStore {
|
||||
return Ok(());
|
||||
}
|
||||
let raw = serde_json::to_string_pretty(&self.cache)?;
|
||||
fs::write(&self.path, raw).with_context(|| format!("failed to write {}", self.path.display()))
|
||||
fs::write(&self.path, raw)
|
||||
.with_context(|| format!("failed to write {}", self.path.display()))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ use std::{
|
||||
};
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use portable_pty::{native_pty_system, CommandBuilder, PtySize};
|
||||
use portable_pty::{CommandBuilder, PtySize, native_pty_system};
|
||||
|
||||
use crate::terminal::{BackendCommand, BackendEvent, BackendTx};
|
||||
|
||||
@@ -34,7 +34,10 @@ pub fn spawn_local_terminal(
|
||||
});
|
||||
|
||||
let mut cmd = CommandBuilder::new(&shell);
|
||||
cmd.env("TERM", std::env::var("TERM").unwrap_or_else(|_| "xterm-256color".into()));
|
||||
cmd.env(
|
||||
"TERM",
|
||||
std::env::var("TERM").unwrap_or_else(|_| "xterm-256color".into()),
|
||||
);
|
||||
cmd.env(
|
||||
"COLORTERM",
|
||||
std::env::var("COLORTERM").unwrap_or_else(|_| "truecolor".into()),
|
||||
|
||||
+427
-80
@@ -1,7 +1,6 @@
|
||||
#![windows_subsystem = "windows"]
|
||||
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
cell::{Cell, RefCell},
|
||||
collections::HashMap,
|
||||
ops::Range,
|
||||
@@ -15,7 +14,7 @@ use alacritty_terminal::index::Side;
|
||||
use alacritty_terminal::selection::SelectionType;
|
||||
use anyhow::{Context as _, Result};
|
||||
use gpui::{
|
||||
Anchor, App, AppContext as _, Bounds, ClipboardItem, Context, Entity, FocusHandle,
|
||||
Anchor, App, AppContext as _, Bounds, ClipboardItem, Context, ElementId, Entity, FocusHandle,
|
||||
Focusable as _, FontWeight, Hsla, InteractiveElement as _, IntoElement, KeyBinding,
|
||||
KeyDownEvent, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, ParentElement as _,
|
||||
PathPromptOptions, Pixels, Point, QuitMode, Render, ScrollDelta, ScrollWheelEvent,
|
||||
@@ -23,8 +22,8 @@ use gpui::{
|
||||
prelude::FluentBuilder as _, px, size, uniform_list,
|
||||
};
|
||||
use gpui_component::{
|
||||
ActiveTheme as _, ElementExt, IconName, Root, Sizable as _, Theme, ThemeMode, ThemeRegistry,
|
||||
WindowExt as _,
|
||||
ActiveTheme as _, Disableable, ElementExt, IconName, Root, Sizable as _, Theme, ThemeMode,
|
||||
ThemeRegistry, WindowExt as _,
|
||||
button::{Button, ButtonVariants as _},
|
||||
checkbox::Checkbox,
|
||||
dialog::Dialog,
|
||||
@@ -38,6 +37,7 @@ use gpui_component::{
|
||||
v_flex,
|
||||
};
|
||||
use gpui_component_assets::Assets;
|
||||
use rust_i18n::t;
|
||||
use tokio::runtime::Runtime;
|
||||
use uuid::Uuid;
|
||||
|
||||
@@ -55,6 +55,8 @@ use system::{DiskSample, SystemSampler, SystemSnapshot, format_bytes};
|
||||
use terminal::{BackendCommand, BackendEvent, TabKind, TerminalTab, encode_key};
|
||||
use terminal_element::TerminalElement;
|
||||
|
||||
rust_i18n::i18n!("locales", fallback = "en");
|
||||
|
||||
const DEFAULT_COLS: u16 = 100;
|
||||
const DEFAULT_ROWS: u16 = 30;
|
||||
const APP_FONT_FAMILY: &str = "Maple Mono NF CN";
|
||||
@@ -145,6 +147,7 @@ struct Ashell {
|
||||
ssh_auth_method: AuthMethod,
|
||||
editing_session_id: Option<String>,
|
||||
follow_system_theme: bool,
|
||||
theme_mode: ThemeMode,
|
||||
light_theme_name: SharedString,
|
||||
dark_theme_name: SharedString,
|
||||
terminal_font_size: f32,
|
||||
@@ -201,14 +204,14 @@ struct SftpContextMenuState {
|
||||
|
||||
impl Ashell {
|
||||
fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||
let host_input = cx.new(|cx| InputState::new(window, cx).placeholder("host"));
|
||||
let host_input = cx.new(|cx| InputState::new(window, cx).placeholder(t!("host")));
|
||||
let session_name_input =
|
||||
cx.new(|cx| InputState::new(window, cx).placeholder("name (optional)"));
|
||||
let port_input = cx.new(|cx| InputState::new(window, cx).default_value("22"));
|
||||
let user_input = cx.new(|cx| InputState::new(window, cx).default_value("root"));
|
||||
let password_input = cx.new(|cx| {
|
||||
InputState::new(window, cx)
|
||||
.placeholder("password")
|
||||
.placeholder(t!("password"))
|
||||
.masked(true)
|
||||
});
|
||||
let key_path_input =
|
||||
@@ -249,6 +252,12 @@ impl Ashell {
|
||||
} else {
|
||||
config.follow_system_theme()
|
||||
};
|
||||
|
||||
let theme_mode = match config.theme_mode() {
|
||||
"light" => ThemeMode::Light,
|
||||
"dark" => ThemeMode::Dark,
|
||||
_ => ThemeMode::Light,
|
||||
};
|
||||
let light_theme_name = if config.light_theme_name().is_empty() {
|
||||
default_light_theme_name
|
||||
} else {
|
||||
@@ -259,6 +268,19 @@ impl Ashell {
|
||||
} else {
|
||||
config.dark_theme_name().into()
|
||||
};
|
||||
|
||||
let configured_locale = config.locale();
|
||||
let mut active_locale = configured_locale.to_string();
|
||||
if active_locale == "system" {
|
||||
active_locale = sys_locale::get_locale().unwrap_or_else(|| "en".to_string());
|
||||
if active_locale.starts_with("zh") {
|
||||
active_locale = "zh-CN".to_string();
|
||||
} else {
|
||||
active_locale = "en".to_string();
|
||||
}
|
||||
}
|
||||
rust_i18n::set_locale(&active_locale);
|
||||
gpui_component::set_locale(&active_locale);
|
||||
let mut this = Self {
|
||||
focus_handle: cx.focus_handle(),
|
||||
selector_focus_handle: cx.focus_handle(),
|
||||
@@ -273,6 +295,7 @@ impl Ashell {
|
||||
ssh_auth_method: AuthMethod::Password,
|
||||
editing_session_id: None,
|
||||
follow_system_theme,
|
||||
theme_mode,
|
||||
light_theme_name,
|
||||
dark_theme_name,
|
||||
terminal_font_size: config.terminal_font_size(),
|
||||
@@ -454,12 +477,32 @@ impl Ashell {
|
||||
progress.lines.push(reason.clone().into());
|
||||
let _ = session_label;
|
||||
let _ = tab_title;
|
||||
progress.title = "Connection Failed".into();
|
||||
progress.title = t!("connection_failed").into();
|
||||
progress.failed = true;
|
||||
}
|
||||
}
|
||||
self.status = reason.into();
|
||||
}
|
||||
BackendEvent::TerminalTitleChanged { tab_id, title } => {
|
||||
if let Some(tab) = self.tabs.iter_mut().find(|t| t.id == tab_id) {
|
||||
if let Some(sftp) = &tab.sftp {
|
||||
let potential_path = if let Some((_, p)) = title.rsplit_once(':') {
|
||||
p.trim()
|
||||
} else {
|
||||
title.trim()
|
||||
};
|
||||
|
||||
if potential_path.starts_with('/') || potential_path.starts_with('~') {
|
||||
let path_to_sync = potential_path.to_string();
|
||||
if path_to_sync != sftp.current_path {
|
||||
if let Some(handle) = self.sftp_handles.get(&tab_id) {
|
||||
handle.list_dir(path_to_sync);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -515,7 +558,8 @@ impl Ashell {
|
||||
) {
|
||||
Ok(backend) => {
|
||||
let title = if cfg!(windows) { "PowerShell" } else { "Local" }.to_string();
|
||||
let mut tab = TerminalTab::new_local(id.clone(), title, backend);
|
||||
let mut tab =
|
||||
TerminalTab::new_local(id.clone(), title, backend, self.events_tx.clone());
|
||||
tab.resize(DEFAULT_COLS, DEFAULT_ROWS);
|
||||
self.tabs.push(tab);
|
||||
self.active_tab = Some(id);
|
||||
@@ -544,7 +588,7 @@ impl Ashell {
|
||||
let key_inline = self.key_inline_input.read(cx).value().to_string();
|
||||
|
||||
if host.is_empty() || user.is_empty() {
|
||||
self.status = "host and user are required".into();
|
||||
self.status = t!("host_and_user_required").into();
|
||||
cx.notify();
|
||||
return;
|
||||
}
|
||||
@@ -685,7 +729,7 @@ impl Ashell {
|
||||
|
||||
window.open_dialog(cx, move |dialog: Dialog, _window, _cx| {
|
||||
dialog
|
||||
.title("SSH Session")
|
||||
.title(t!("new_ssh_connection"))
|
||||
.w(px(520.))
|
||||
.overlay_closable(true)
|
||||
.content({
|
||||
@@ -716,7 +760,7 @@ impl Ashell {
|
||||
.gap_2()
|
||||
.child(
|
||||
Button::new("ssh-auth-password")
|
||||
.label("password")
|
||||
.label(t!("password").to_string())
|
||||
.when(is_password, |button| button.primary())
|
||||
.on_click(window.listener_for(
|
||||
&view,
|
||||
@@ -730,7 +774,7 @@ impl Ashell {
|
||||
)
|
||||
.child(
|
||||
Button::new("ssh-auth-key")
|
||||
.label("key")
|
||||
.label(t!("key").to_string())
|
||||
.when(!is_password, |button| button.primary())
|
||||
.on_click(window.listener_for(
|
||||
&view,
|
||||
@@ -759,13 +803,17 @@ impl Ashell {
|
||||
.gap_2()
|
||||
.child(
|
||||
Button::new("connect-ssh-cancel")
|
||||
.label("cancel")
|
||||
.label(t!("cancel").to_string())
|
||||
.on_click(|_, window, cx| window.close_dialog(cx)),
|
||||
)
|
||||
.child(
|
||||
Button::new("connect-ssh-confirm")
|
||||
.primary()
|
||||
.label(if is_editing { "save" } else { "connect" })
|
||||
.label(if is_editing {
|
||||
t!("save")
|
||||
} else {
|
||||
t!("connect")
|
||||
})
|
||||
.on_click(window.listener_for(
|
||||
&view,
|
||||
|this, _, window, cx| {
|
||||
@@ -852,13 +900,13 @@ impl Ashell {
|
||||
div()
|
||||
.text_size(px(12.))
|
||||
.font_weight(FontWeight::SEMIBOLD)
|
||||
.child("Local Terminal"),
|
||||
.child(t!("local_terminal")),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.text_size(px(11.))
|
||||
.text_color(_cx.theme().muted_foreground)
|
||||
.child("Open a new local shell tab"),
|
||||
.child(t!("open_local_shell_tab")),
|
||||
),
|
||||
),
|
||||
)
|
||||
@@ -894,13 +942,13 @@ impl Ashell {
|
||||
div()
|
||||
.text_size(px(12.))
|
||||
.font_weight(FontWeight::SEMIBOLD)
|
||||
.child("New SSH Connection"),
|
||||
.child(t!("new_ssh_connection")),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.text_size(px(11.))
|
||||
.text_color(_cx.theme().muted_foreground)
|
||||
.child("Create or edit an SSH session"),
|
||||
.child(t!("create_or_edit_ssh_session")),
|
||||
),
|
||||
),
|
||||
)
|
||||
@@ -986,7 +1034,7 @@ impl Ashell {
|
||||
let view = cx.entity();
|
||||
window.open_dialog(cx, move |dialog: Dialog, _window, _| {
|
||||
dialog
|
||||
.title("Settings")
|
||||
.title(t!("settings").to_string())
|
||||
.w(px(480.))
|
||||
.content({
|
||||
let view = view.clone();
|
||||
@@ -998,33 +1046,109 @@ impl Ashell {
|
||||
h_flex()
|
||||
.items_center()
|
||||
.gap_3()
|
||||
.child(div().w(px(180.)).child("Terminal Font Size"))
|
||||
.child(
|
||||
Button::new("font-size-down")
|
||||
.label("-")
|
||||
.on_click(window.listener_for(&view, |this, _, _, cx| {
|
||||
this.change_terminal_font_size(-1.0, cx)
|
||||
})),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.min_w(px(64.))
|
||||
.text_center()
|
||||
.child(format!("{:.0}px", view.read(cx).terminal_font_size)),
|
||||
.w(px(180.))
|
||||
.child(t!("terminal_font_size").to_string()),
|
||||
)
|
||||
.child(Button::new("font-size-down").label("-").on_click(
|
||||
window.listener_for(&view, |this, _, _, cx| {
|
||||
this.change_terminal_font_size(-1.0, cx)
|
||||
}),
|
||||
))
|
||||
.child(div().min_w(px(64.)).text_center().child(format!(
|
||||
"{:.0}px",
|
||||
view.read(cx).terminal_font_size
|
||||
)))
|
||||
.child(Button::new("font-size-up").label("+").on_click(
|
||||
window.listener_for(&view, |this, _, _, cx| {
|
||||
this.change_terminal_font_size(1.0, cx)
|
||||
}),
|
||||
)),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.items_center()
|
||||
.gap_3()
|
||||
.child(div().w(px(180.)).child(t!("language").to_string()))
|
||||
.child(
|
||||
Button::new("font-size-up")
|
||||
.label("+")
|
||||
.on_click(window.listener_for(&view, |this, _, _, cx| {
|
||||
this.change_terminal_font_size(1.0, cx)
|
||||
})),
|
||||
Button::new("language-dropdown")
|
||||
.small()
|
||||
.icon(IconName::Globe)
|
||||
.label({
|
||||
let current_locale =
|
||||
view.read(cx).config.locale().to_string();
|
||||
if current_locale == "en" {
|
||||
t!("english").to_string()
|
||||
} else if current_locale == "zh-CN" {
|
||||
t!("chinese").to_string()
|
||||
} else {
|
||||
t!("follow_system").to_string()
|
||||
}
|
||||
})
|
||||
.dropdown_menu_with_anchor(Anchor::BottomRight, {
|
||||
let view = view.clone();
|
||||
move |mut menu, window, cx| {
|
||||
let current_locale = view
|
||||
.read(cx)
|
||||
.config
|
||||
.locale()
|
||||
.to_string();
|
||||
menu = menu
|
||||
.min_w(160.)
|
||||
.item(
|
||||
PopupMenuItem::new(
|
||||
t!("follow_system").to_string(),
|
||||
)
|
||||
.checked(current_locale == "system")
|
||||
.on_click(window.listener_for(
|
||||
&view,
|
||||
|this, _, window, cx| {
|
||||
this.set_display_language(
|
||||
"system", window, cx,
|
||||
)
|
||||
},
|
||||
)),
|
||||
)
|
||||
.separator()
|
||||
.item(
|
||||
PopupMenuItem::new(
|
||||
t!("english").to_string(),
|
||||
)
|
||||
.checked(current_locale == "en")
|
||||
.on_click(window.listener_for(
|
||||
&view,
|
||||
|this, _, window, cx| {
|
||||
this.set_display_language(
|
||||
"en", window, cx,
|
||||
)
|
||||
},
|
||||
)),
|
||||
)
|
||||
.item(
|
||||
PopupMenuItem::new(
|
||||
t!("chinese").to_string(),
|
||||
)
|
||||
.checked(current_locale == "zh-CN")
|
||||
.on_click(window.listener_for(
|
||||
&view,
|
||||
|this, _, window, cx| {
|
||||
this.set_display_language(
|
||||
"zh-CN", window, cx,
|
||||
)
|
||||
},
|
||||
)),
|
||||
);
|
||||
menu
|
||||
}
|
||||
}),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.text_size(px(12.))
|
||||
.text_color(cx.theme().muted_foreground)
|
||||
.child("Theme mode and light/dark theme families are managed from the top-bar theme button."),
|
||||
.child(t!("theme_management_hint")),
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -1137,8 +1261,12 @@ impl Ashell {
|
||||
DEFAULT_ROWS,
|
||||
self.events_tx.clone(),
|
||||
);
|
||||
self.tabs
|
||||
.push(TerminalTab::new_ssh(id.clone(), &session, backend));
|
||||
self.tabs.push(TerminalTab::new_ssh(
|
||||
id.clone(),
|
||||
&session,
|
||||
backend,
|
||||
self.events_tx.clone(),
|
||||
));
|
||||
let sftp_handle = sftp::spawn_sftp(
|
||||
self.runtime.handle(),
|
||||
id.clone(),
|
||||
@@ -1150,8 +1278,8 @@ impl Ashell {
|
||||
self.pending_sftp_path_sync = Some("/".into());
|
||||
self.connection_progress = Some(ConnectionProgress {
|
||||
tab_id: id,
|
||||
title: "Connecting".into(),
|
||||
lines: vec!["starting connection...".into()],
|
||||
title: t!("connecting").into(),
|
||||
lines: vec![t!("starting_connection").into()],
|
||||
failed: false,
|
||||
});
|
||||
self.status = "ssh tab opened".into();
|
||||
@@ -1461,8 +1589,8 @@ impl Ashell {
|
||||
|
||||
fn switch_theme_mode(&mut self, mode: ThemeMode, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.follow_system_theme = false;
|
||||
self.theme_mode = mode;
|
||||
self.apply_theme_preferences(window, cx);
|
||||
Theme::change(mode, Some(window), cx);
|
||||
self.status = format!("theme mode: {}", cx.theme().mode.name()).into();
|
||||
self.persist_theme_preferences();
|
||||
cx.notify();
|
||||
@@ -1504,6 +1632,26 @@ impl Ashell {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn set_display_language(&mut self, locale: &str, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.config.set_locale(locale);
|
||||
let mut active_locale = locale.to_string();
|
||||
if active_locale == "system" {
|
||||
active_locale = sys_locale::get_locale().unwrap_or_else(|| "en".to_string());
|
||||
if active_locale.starts_with("zh") {
|
||||
active_locale = "zh-CN".to_string();
|
||||
} else {
|
||||
active_locale = "en".to_string();
|
||||
}
|
||||
}
|
||||
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:#}");
|
||||
}
|
||||
window.refresh();
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn apply_theme_preferences(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let light_theme = ThemeRegistry::global(cx)
|
||||
.themes()
|
||||
@@ -1523,13 +1671,18 @@ impl Ashell {
|
||||
if self.follow_system_theme {
|
||||
Theme::sync_system_appearance(Some(window), cx);
|
||||
} else {
|
||||
Theme::change(cx.theme().mode, Some(window), cx);
|
||||
Theme::change(self.theme_mode, Some(window), cx);
|
||||
}
|
||||
}
|
||||
|
||||
fn persist_theme_preferences(&mut self) {
|
||||
let theme_mode_str = match self.theme_mode {
|
||||
ThemeMode::Light => "light",
|
||||
ThemeMode::Dark => "dark",
|
||||
};
|
||||
self.config.set_theme_preferences(
|
||||
self.follow_system_theme,
|
||||
theme_mode_str,
|
||||
self.light_theme_name.to_string(),
|
||||
self.dark_theme_name.to_string(),
|
||||
);
|
||||
@@ -1889,7 +2042,7 @@ impl Ashell {
|
||||
div()
|
||||
.text_size(px(13.))
|
||||
.text_color(cx.theme().muted_foreground)
|
||||
.child("Open a local terminal or pick an SSH session."),
|
||||
.child(t!("open_local_or_ssh")),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
@@ -1897,13 +2050,13 @@ impl Ashell {
|
||||
.child(
|
||||
Button::new("home-open-local")
|
||||
.primary()
|
||||
.label("Local Terminal")
|
||||
.label(t!("local_terminal").to_string())
|
||||
.on_click(cx.listener(|this, _, _, cx| this.open_local(cx))),
|
||||
)
|
||||
.child(
|
||||
Button::new("home-open-session")
|
||||
.ghost()
|
||||
.label("Open Session")
|
||||
.label(t!("open_session").to_string())
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
this.show_selector_dialog(window, cx)
|
||||
})),
|
||||
@@ -1947,7 +2100,7 @@ impl Ashell {
|
||||
.as_ref()
|
||||
.and_then(|id| self.tabs.iter().find(|t| &t.id == id))
|
||||
.map(|t| t.title.clone())
|
||||
.unwrap_or_else(|| "No session".into())
|
||||
.unwrap_or_else(|| t!("idle_no_session").into())
|
||||
}
|
||||
|
||||
fn active_ssh_session(&self) -> Option<(String, Session)> {
|
||||
@@ -2008,7 +2161,7 @@ impl Ashell {
|
||||
|
||||
fn system_card(
|
||||
&self,
|
||||
label: &'static str,
|
||||
label: String,
|
||||
percent: f32,
|
||||
detail: String,
|
||||
fill: Hsla,
|
||||
@@ -2031,7 +2184,7 @@ impl Ashell {
|
||||
.text_size(px(11.))
|
||||
.font_weight(FontWeight::SEMIBOLD)
|
||||
.text_color(fill)
|
||||
.child(label),
|
||||
.child(label.clone()),
|
||||
)
|
||||
.child(div().flex_1())
|
||||
.child(
|
||||
@@ -2077,14 +2230,14 @@ impl Ashell {
|
||||
.text_size(px(12.))
|
||||
.font_weight(FontWeight::SEMIBOLD)
|
||||
.text_color(cx.theme().chart_4)
|
||||
.child("NET"),
|
||||
.child(t!("net")),
|
||||
)
|
||||
.child(div().flex_1())
|
||||
.child(
|
||||
div()
|
||||
.text_size(px(11.))
|
||||
.text_color(cx.theme().muted_foreground)
|
||||
.child("live"),
|
||||
.child(t!("live")),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
@@ -2175,6 +2328,89 @@ impl Ashell {
|
||||
)
|
||||
}
|
||||
|
||||
fn toggle_sftp_entry(&mut self, path: String, checked: bool, cx: &mut Context<Self>) {
|
||||
if let Some(sftp) = self.active_sftp_mut() {
|
||||
if checked {
|
||||
sftp.selected_entries.insert(path);
|
||||
} else {
|
||||
sftp.selected_entries.remove(&path);
|
||||
}
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
fn toggle_all_sftp_entries(&mut self, checked: bool, cx: &mut Context<Self>) {
|
||||
if let Some(sftp) = self.active_sftp_mut() {
|
||||
if checked {
|
||||
let paths: Vec<String> = sftp.entries.iter().map(|e| e.full_path.clone()).collect();
|
||||
for path in paths {
|
||||
sftp.selected_entries.insert(path);
|
||||
}
|
||||
} else {
|
||||
sftp.selected_entries.clear();
|
||||
}
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
fn download_selected_sftp_entries(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let Some(sftp) = self.active_sftp() else {
|
||||
return;
|
||||
};
|
||||
let selected: Vec<String> = sftp.selected_entries.iter().cloned().collect();
|
||||
if selected.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let Some(handle) = self.active_sftp_handle().cloned() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let path_prompt = cx.prompt_for_paths(PathPromptOptions {
|
||||
files: false,
|
||||
directories: true,
|
||||
multiple: false,
|
||||
prompt: Some("Select Download Folder".into()),
|
||||
});
|
||||
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
if let Ok(Ok(Some(mut paths))) = path_prompt.await {
|
||||
if let Some(folder) = paths.pop() {
|
||||
let local_dir = folder.to_string_lossy().to_string();
|
||||
for remote in selected {
|
||||
let _ = handle.commands.send(crate::sftp::SftpCommand::Download {
|
||||
remote,
|
||||
local_dir: local_dir.clone(),
|
||||
});
|
||||
}
|
||||
|
||||
let _ = this.update(cx, |this, cx| {
|
||||
if let Some(sftp_mut) = this.active_sftp_mut() {
|
||||
sftp_mut.selected_entries.clear();
|
||||
}
|
||||
cx.notify();
|
||||
});
|
||||
}
|
||||
}
|
||||
Ok::<(), anyhow::Error>(())
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
fn upload_sftp_files_batch(&mut self, paths: Vec<String>, _cx: &mut Context<Self>) {
|
||||
if paths.is_empty() {
|
||||
return;
|
||||
}
|
||||
if let Some(sftp) = self.active_sftp() {
|
||||
if let Some(handle) = self.active_sftp_handle() {
|
||||
let _ = handle.commands.send(crate::sftp::SftpCommand::UploadPaths {
|
||||
locals: paths,
|
||||
remote_dir: sftp.current_path.clone(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn theme_dropdown(&self, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let view = cx.entity();
|
||||
let themes = ThemeRegistry::global(cx)
|
||||
@@ -2212,28 +2448,28 @@ impl Ashell {
|
||||
menu = menu
|
||||
.min_w(220.)
|
||||
.item(
|
||||
PopupMenuItem::new("Follow System")
|
||||
PopupMenuItem::new(t!("follow_system"))
|
||||
.checked(follow_system)
|
||||
.on_click(window.listener_for(&view, |this, _, window, cx| {
|
||||
this.set_follow_system_theme(true, window, cx)
|
||||
})),
|
||||
)
|
||||
.item(
|
||||
PopupMenuItem::new("Use Light Mode")
|
||||
PopupMenuItem::new(t!("use_light_mode"))
|
||||
.checked(!follow_system && !is_dark_mode)
|
||||
.on_click(window.listener_for(&view, |this, _, window, cx| {
|
||||
this.switch_theme_mode(ThemeMode::Light, window, cx)
|
||||
})),
|
||||
)
|
||||
.item(
|
||||
PopupMenuItem::new("Use Dark Mode")
|
||||
PopupMenuItem::new(t!("use_dark_mode"))
|
||||
.checked(!follow_system && is_dark_mode)
|
||||
.on_click(window.listener_for(&view, |this, _, window, cx| {
|
||||
this.switch_theme_mode(ThemeMode::Dark, window, cx)
|
||||
})),
|
||||
)
|
||||
.separator()
|
||||
.label("Light Theme");
|
||||
.label(t!("light_theme").to_string());
|
||||
|
||||
for theme_name in light_themes.clone() {
|
||||
let checked = theme_name == light_theme_name;
|
||||
@@ -2247,7 +2483,7 @@ impl Ashell {
|
||||
}
|
||||
|
||||
menu = menu.separator();
|
||||
menu = menu.label("Dark Theme");
|
||||
menu = menu.label(t!("dark_theme").to_string());
|
||||
|
||||
for theme_name in dark_themes.clone() {
|
||||
let checked = theme_name == dark_theme_name;
|
||||
@@ -2281,13 +2517,13 @@ impl Ashell {
|
||||
.text_size(px(12.))
|
||||
.font_weight(FontWeight::SEMIBOLD)
|
||||
.text_color(cx.theme().primary)
|
||||
.child("REMOTE FILES"),
|
||||
.child(t!("remote_files")),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.text_size(px(12.))
|
||||
.text_color(cx.theme().muted_foreground)
|
||||
.child("Open an SSH tab to browse SFTP files."),
|
||||
.child(t!("open_ssh_tab_sftp")),
|
||||
);
|
||||
};
|
||||
|
||||
@@ -2299,6 +2535,11 @@ impl Ashell {
|
||||
.filter(|entry| self.show_hidden_files || !entry.name.starts_with('.'))
|
||||
.collect::<Vec<_>>();
|
||||
let status = sftp.status.clone();
|
||||
let selected_entries = sftp.selected_entries.clone();
|
||||
let all_selected = !entries.is_empty()
|
||||
&& entries
|
||||
.iter()
|
||||
.all(|e| selected_entries.contains(&e.full_path));
|
||||
let parent_path = Self::sftp_parent_path(&sftp.current_path);
|
||||
let view = cx.entity();
|
||||
let icon_col_width = px(14.);
|
||||
@@ -2310,6 +2551,16 @@ impl Ashell {
|
||||
.gap_0()
|
||||
.border_color(cx.theme().border)
|
||||
.bg(cx.theme().background)
|
||||
.on_drop(
|
||||
cx.listener(|this, paths: &gpui::ExternalPaths, _window, cx| {
|
||||
let paths_to_upload: Vec<String> = paths
|
||||
.0
|
||||
.iter()
|
||||
.map(|p| p.to_string_lossy().to_string())
|
||||
.collect();
|
||||
this.upload_sftp_files_batch(paths_to_upload, cx);
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.h(px(34.))
|
||||
@@ -2324,7 +2575,7 @@ impl Ashell {
|
||||
.text_size(px(12.))
|
||||
.font_weight(FontWeight::SEMIBOLD)
|
||||
.text_color(cx.theme().primary)
|
||||
.child("REMOTE FILES"),
|
||||
.child(t!("remote_files")),
|
||||
)
|
||||
.child(div().flex_1())
|
||||
.child(
|
||||
@@ -2332,7 +2583,7 @@ impl Ashell {
|
||||
.ghost()
|
||||
.small()
|
||||
.icon(IconName::ArrowRight)
|
||||
.label("refresh")
|
||||
.label(t!("refresh").to_string())
|
||||
.on_click(cx.listener(|this, _, _, cx| this.refresh_sftp(cx))),
|
||||
)
|
||||
.child(
|
||||
@@ -2340,7 +2591,7 @@ impl Ashell {
|
||||
.ghost()
|
||||
.small()
|
||||
.icon(IconName::Plus)
|
||||
.label("upload file")
|
||||
.label(t!("upload_file").to_string())
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
this.upload_sftp_files(window, cx)
|
||||
})),
|
||||
@@ -2350,14 +2601,30 @@ impl Ashell {
|
||||
.ghost()
|
||||
.small()
|
||||
.icon(IconName::Folder)
|
||||
.label("upload folder")
|
||||
.label(t!("upload_folder").to_string())
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
this.upload_sftp_folder(window, cx)
|
||||
})),
|
||||
)
|
||||
.child(
|
||||
Button::new("sftp-download-selected")
|
||||
.ghost()
|
||||
.small()
|
||||
.icon(IconName::ArrowDown)
|
||||
.label(if selected_entries.is_empty() {
|
||||
t!("download").to_string()
|
||||
} else {
|
||||
t!("download_count", count = selected_entries.len()).to_string()
|
||||
})
|
||||
.disabled(selected_entries.is_empty())
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
this.download_selected_sftp_entries(window, cx);
|
||||
})),
|
||||
)
|
||||
.child(
|
||||
Checkbox::new("sftp-show-hidden")
|
||||
.label("hidden")
|
||||
.small()
|
||||
.label(t!("hidden").to_string())
|
||||
.checked(self.show_hidden_files)
|
||||
.tab_stop(false)
|
||||
.on_click(cx.listener(|this, checked, _, cx| {
|
||||
@@ -2396,6 +2663,20 @@ impl Ashell {
|
||||
.border_b_1()
|
||||
.border_color(cx.theme().border)
|
||||
.bg(cx.theme().muted.opacity(0.8))
|
||||
.child(
|
||||
h_flex()
|
||||
.w(px(24.))
|
||||
.flex_none()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.child(
|
||||
Checkbox::new("sftp-select-all")
|
||||
.checked(all_selected)
|
||||
.on_click(cx.listener(move |this, checked, _, cx| {
|
||||
this.toggle_all_sftp_entries(*checked, cx);
|
||||
})),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.flex_1()
|
||||
@@ -2408,7 +2689,7 @@ impl Ashell {
|
||||
.flex_1()
|
||||
.text_size(px(11.))
|
||||
.text_color(cx.theme().muted_foreground)
|
||||
.child("NAME"),
|
||||
.child(t!("name")),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
@@ -2417,7 +2698,7 @@ impl Ashell {
|
||||
.flex_none()
|
||||
.text_size(px(11.))
|
||||
.text_color(cx.theme().muted_foreground)
|
||||
.child("SIZE"),
|
||||
.child(t!("size")),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
@@ -2425,7 +2706,7 @@ impl Ashell {
|
||||
.flex_none()
|
||||
.text_size(px(11.))
|
||||
.text_color(cx.theme().muted_foreground)
|
||||
.child("MODIFIED"),
|
||||
.child(t!("modified")),
|
||||
)
|
||||
.child(div().w(px(12.)).flex_none()),
|
||||
)
|
||||
@@ -2447,6 +2728,8 @@ impl Ashell {
|
||||
let left_row = entry.clone();
|
||||
let right_row = entry.clone();
|
||||
let remote_path = entry.full_path.clone();
|
||||
let is_checked =
|
||||
selected_entries.contains(&entry.full_path);
|
||||
let is_selected = selected_path
|
||||
.as_deref()
|
||||
.map(|path| path == entry.full_path)
|
||||
@@ -2504,6 +2787,37 @@ impl Ashell {
|
||||
}
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.w(px(24.))
|
||||
.flex_none()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.on_mouse_down(MouseButton::Left, |_, _, cx| {
|
||||
cx.stop_propagation()
|
||||
})
|
||||
.on_mouse_down(
|
||||
MouseButton::Right,
|
||||
|_, _, cx| cx.stop_propagation(),
|
||||
)
|
||||
.child(
|
||||
Checkbox::new(ElementId::Name(
|
||||
format!("check-{}", entry.full_path)
|
||||
.into(),
|
||||
))
|
||||
.checked(is_checked)
|
||||
.on_click(window.listener_for(&view, {
|
||||
let path = entry.full_path.clone();
|
||||
move |this, checked, _, cx| {
|
||||
this.toggle_sftp_entry(
|
||||
path.clone(),
|
||||
*checked,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
})),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.flex_1()
|
||||
@@ -2596,11 +2910,6 @@ impl Ashell {
|
||||
fn sidebar(&self, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let sessions = self.config.sessions().to_vec();
|
||||
let active_session_id = self.active_session_id().map(ToOwned::to_owned);
|
||||
let active_kind = match self.active_kind() {
|
||||
Some(TabKind::Local) => "local",
|
||||
Some(TabKind::Ssh) => "ssh",
|
||||
None => "idle",
|
||||
};
|
||||
let connection_color = self.active_connection_color(cx);
|
||||
|
||||
v_flex()
|
||||
@@ -2638,7 +2947,17 @@ impl Ashell {
|
||||
div()
|
||||
.text_size(px(11.))
|
||||
.text_color(cx.theme().muted_foreground)
|
||||
.child(format!("{} / {}", active_kind, self.active_title())),
|
||||
.child({
|
||||
if let Some(kind) = self.active_kind() {
|
||||
let kind_str = match kind {
|
||||
TabKind::Local => t!("local_terminal").to_string(),
|
||||
TabKind::Ssh => "ssh".to_string(),
|
||||
};
|
||||
format!("{} / {}", kind_str, self.active_title())
|
||||
} else {
|
||||
self.active_title()
|
||||
}
|
||||
}),
|
||||
),
|
||||
),
|
||||
)
|
||||
@@ -2653,7 +2972,7 @@ impl Ashell {
|
||||
.text_size(px(12.))
|
||||
.font_weight(FontWeight::SEMIBOLD)
|
||||
.text_color(cx.theme().primary)
|
||||
.child("SYSTEM"),
|
||||
.child(t!("system")),
|
||||
)
|
||||
.child(div().flex_1())
|
||||
.child(
|
||||
@@ -2672,21 +2991,21 @@ impl Ashell {
|
||||
)
|
||||
})
|
||||
.child(self.system_card(
|
||||
"CPU",
|
||||
t!("cpu").to_string(),
|
||||
self.system.cpu_percent,
|
||||
"global load".into(),
|
||||
t!("global_load").into(),
|
||||
cx.theme().chart_1,
|
||||
cx,
|
||||
))
|
||||
.child(self.system_card(
|
||||
"MEM",
|
||||
t!("mem").to_string(),
|
||||
self.system.mem_percent,
|
||||
self.system.mem_detail.clone(),
|
||||
cx.theme().chart_2,
|
||||
cx,
|
||||
))
|
||||
.child(self.system_card(
|
||||
"SWAP",
|
||||
t!("swap").to_string(),
|
||||
self.system.swap_percent,
|
||||
self.system.swap_detail.clone(),
|
||||
cx.theme().chart_3,
|
||||
@@ -2705,7 +3024,7 @@ impl Ashell {
|
||||
div()
|
||||
.text_size(px(11.))
|
||||
.text_color(cx.theme().chart_5)
|
||||
.child("DISK"),
|
||||
.child(t!("disk")),
|
||||
)
|
||||
.children({
|
||||
let mut disk_elements = Vec::new();
|
||||
@@ -2719,7 +3038,7 @@ impl Ashell {
|
||||
.child(
|
||||
Button::new("open-ssh-panel")
|
||||
.primary()
|
||||
.label("+ ssh")
|
||||
.label(t!("add_ssh").to_string())
|
||||
.on_click(
|
||||
cx.listener(|this, _, window, cx| this.open_new_ssh_dialog(window, cx)),
|
||||
),
|
||||
@@ -2732,7 +3051,7 @@ impl Ashell {
|
||||
.text_size(px(12.))
|
||||
.font_weight(FontWeight::SEMIBOLD)
|
||||
.text_color(cx.theme().primary)
|
||||
.child("SAVED"),
|
||||
.child(t!("saved")),
|
||||
)
|
||||
.child(
|
||||
v_flex()
|
||||
@@ -3250,7 +3569,31 @@ fn sync_macos_launch_environment() {
|
||||
fn sync_macos_launch_environment() {}
|
||||
|
||||
fn open_main_window(cx: &mut App) {
|
||||
let window_options = WindowOptions::default();
|
||||
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()));
|
||||
}
|
||||
|
||||
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");
|
||||
@@ -3270,9 +3613,13 @@ fn main() {
|
||||
)
|
||||
.init();
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
let app = gpui_platform::application()
|
||||
.with_assets(Assets)
|
||||
.with_quit_mode(QuitMode::Explicit);
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
let app = gpui_platform::application().with_assets(Assets);
|
||||
app.on_reopen(|cx| {
|
||||
if cx.windows().is_empty() {
|
||||
open_main_window(cx);
|
||||
|
||||
+116
-30
@@ -4,15 +4,15 @@ use std::{
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use anyhow::{Context, Result, anyhow};
|
||||
use async_trait::async_trait;
|
||||
use chrono::{DateTime, TimeZone, Utc};
|
||||
use directories::BaseDirs;
|
||||
use flate2::read::GzDecoder;
|
||||
use russh::{
|
||||
client::{self, Handler},
|
||||
keys::{decode_secret_key, key::PrivateKeyWithHashAlg, load_secret_key, HashAlg, PrivateKey},
|
||||
Disconnect,
|
||||
client::{self, Handler},
|
||||
keys::{HashAlg, PrivateKey, decode_secret_key, key::PrivateKeyWithHashAlg, load_secret_key},
|
||||
};
|
||||
use russh_sftp::client::SftpSession;
|
||||
use tokio::{
|
||||
@@ -38,6 +38,7 @@ pub struct RemoteEntry {
|
||||
pub modified: u32,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PreviewData {
|
||||
pub path: String,
|
||||
@@ -47,16 +48,23 @@ pub struct PreviewData {
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum SftpCommand {
|
||||
pub enum SftpCommand {
|
||||
ListDir(String),
|
||||
#[allow(dead_code)]
|
||||
Preview(String),
|
||||
Download { remote: String, local_dir: String },
|
||||
UploadPaths { locals: Vec<String>, remote_dir: String },
|
||||
Download {
|
||||
remote: String,
|
||||
local_dir: String,
|
||||
},
|
||||
UploadPaths {
|
||||
locals: Vec<String>,
|
||||
remote_dir: String,
|
||||
},
|
||||
Close,
|
||||
}
|
||||
|
||||
pub struct SftpHandle {
|
||||
commands: UnboundedSender<SftpCommand>,
|
||||
pub commands: UnboundedSender<SftpCommand>,
|
||||
#[allow(dead_code)]
|
||||
join: Option<JoinHandle<()>>,
|
||||
}
|
||||
@@ -75,16 +83,21 @@ impl SftpHandle {
|
||||
let _ = self.commands.send(SftpCommand::ListDir(path));
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn preview(&self, path: String) {
|
||||
let _ = self.commands.send(SftpCommand::Preview(path));
|
||||
}
|
||||
|
||||
pub fn download(&self, remote: String, local_dir: String) {
|
||||
let _ = self.commands.send(SftpCommand::Download { remote, local_dir });
|
||||
let _ = self
|
||||
.commands
|
||||
.send(SftpCommand::Download { remote, local_dir });
|
||||
}
|
||||
|
||||
pub fn upload_paths(&self, locals: Vec<String>, remote_dir: String) {
|
||||
let _ = self.commands.send(SftpCommand::UploadPaths { locals, remote_dir });
|
||||
let _ = self
|
||||
.commands
|
||||
.send(SftpCommand::UploadPaths { locals, remote_dir });
|
||||
}
|
||||
|
||||
pub fn close(&self) {
|
||||
@@ -137,14 +150,25 @@ async fn run_sftp(
|
||||
.await
|
||||
.context("sftp handshake")?;
|
||||
|
||||
let home = sftp.canonicalize(".").await.unwrap_or_else(|_| "/".to_string());
|
||||
let home = sftp
|
||||
.canonicalize(".")
|
||||
.await
|
||||
.unwrap_or_else(|_| "/".to_string());
|
||||
emit_entries(&events, &tab_id, &sftp, &home).await?;
|
||||
|
||||
while let Some(command) = commands.recv().await {
|
||||
match command {
|
||||
SftpCommand::Close => break,
|
||||
SftpCommand::ListDir(path) => {
|
||||
if let Err(err) = emit_entries(&events, &tab_id, &sftp, &path).await {
|
||||
let actual_path = if path == "~" {
|
||||
home.clone()
|
||||
} else if let Some(rest) = path.strip_prefix("~/") {
|
||||
crate::sftp::join_remote(&home, rest)
|
||||
} else {
|
||||
path
|
||||
};
|
||||
|
||||
if let Err(err) = emit_entries(&events, &tab_id, &sftp, &actual_path).await {
|
||||
let _ = events.send(BackendEvent::SftpStatus {
|
||||
tab_id: tab_id.clone(),
|
||||
text: format!("list failed: {err:#}"),
|
||||
@@ -210,7 +234,9 @@ async fn run_sftp(
|
||||
}
|
||||
}
|
||||
|
||||
let _ = handle.disconnect(Disconnect::ByApplication, "bye", "").await;
|
||||
let _ = handle
|
||||
.disconnect(Disconnect::ByApplication, "bye", "")
|
||||
.await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -233,7 +259,9 @@ async fn emit_entries(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn connect_and_authenticate(session: &Session) -> Result<russh::client::Handle<SftpClientHandler>> {
|
||||
async fn connect_and_authenticate(
|
||||
session: &Session,
|
||||
) -> Result<russh::client::Handle<SftpClientHandler>> {
|
||||
let config = Arc::new(client::Config {
|
||||
inactivity_timeout: Some(std::time::Duration::from_secs(600)),
|
||||
..Default::default()
|
||||
@@ -360,6 +388,7 @@ fn join_remote(parent: &str, child: &str) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn strip_archive_suffix(name: &str) -> &str {
|
||||
for suffix in [".tar.gz", ".tgz", ".zip", ".tar"] {
|
||||
if let Some(stripped) = name.strip_suffix(suffix) {
|
||||
@@ -429,7 +458,10 @@ async fn list_dir_impl(sftp: &SftpSession, path: &str) -> Result<Vec<RemoteEntry
|
||||
}
|
||||
|
||||
async fn preview_impl(sftp: &SftpSession, path: &str) -> Result<PreviewData> {
|
||||
let metadata = sftp.metadata(path).await.with_context(|| format!("metadata {path}"))?;
|
||||
let metadata = sftp
|
||||
.metadata(path)
|
||||
.await
|
||||
.with_context(|| format!("metadata {path}"))?;
|
||||
let is_dir = metadata
|
||||
.permissions
|
||||
.map(|mode| (mode & 0o170_000) == 0o040_000)
|
||||
@@ -450,9 +482,15 @@ async fn preview_impl(sftp: &SftpSession, path: &str) -> Result<PreviewData> {
|
||||
});
|
||||
}
|
||||
|
||||
let mut remote_file = sftp.open(path).await.with_context(|| format!("open remote {path}"))?;
|
||||
let mut remote_file = sftp
|
||||
.open(path)
|
||||
.await
|
||||
.with_context(|| format!("open remote {path}"))?;
|
||||
let mut buffer = vec![0u8; 128 * 1024];
|
||||
let read = remote_file.read(&mut buffer).await.context("read preview bytes")?;
|
||||
let read = remote_file
|
||||
.read(&mut buffer)
|
||||
.await
|
||||
.context("read preview bytes")?;
|
||||
buffer.truncate(read);
|
||||
|
||||
let nul_ratio = if buffer.is_empty() {
|
||||
@@ -488,7 +526,10 @@ async fn download_path_impl(
|
||||
.await
|
||||
.with_context(|| format!("create {}", local_dir.display()))?;
|
||||
|
||||
let metadata = sftp.metadata(remote).await.with_context(|| format!("metadata {remote}"))?;
|
||||
let metadata = sftp
|
||||
.metadata(remote)
|
||||
.await
|
||||
.with_context(|| format!("metadata {remote}"))?;
|
||||
let is_dir = metadata
|
||||
.permissions
|
||||
.map(|mode| (mode & 0o170_000) == 0o040_000)
|
||||
@@ -500,7 +541,8 @@ async fn download_path_impl(
|
||||
base_name(remote),
|
||||
Uuid::new_v4()
|
||||
));
|
||||
let extracted_to = download_remote_directory_archive(handle, sftp, remote, &local_archive).await?;
|
||||
let extracted_to =
|
||||
download_remote_directory_archive(handle, sftp, remote, &local_archive).await?;
|
||||
return Ok(format!("downloaded folder to {}", extracted_to.display()));
|
||||
}
|
||||
|
||||
@@ -509,7 +551,12 @@ async fn download_path_impl(
|
||||
Ok(format!("downloaded file to {}", local_path.display()))
|
||||
}
|
||||
|
||||
async fn download_dir_recursive(sftp: &SftpSession, remote_dir: &str, local_dir: &Path) -> Result<()> {
|
||||
#[allow(dead_code)]
|
||||
async fn download_dir_recursive(
|
||||
sftp: &SftpSession,
|
||||
remote_dir: &str,
|
||||
local_dir: &Path,
|
||||
) -> Result<()> {
|
||||
tokio::fs::create_dir_all(local_dir)
|
||||
.await
|
||||
.with_context(|| format!("create {}", local_dir.display()))?;
|
||||
@@ -532,7 +579,11 @@ async fn download_remote_directory_archive(
|
||||
remote_dir: &str,
|
||||
local_archive: &Path,
|
||||
) -> Result<PathBuf> {
|
||||
let remote_archive = format!("/tmp/ashell-{}-{}.tar.gz", base_name(remote_dir), Uuid::new_v4());
|
||||
let remote_archive = format!(
|
||||
"/tmp/ashell-{}-{}.tar.gz",
|
||||
base_name(remote_dir),
|
||||
Uuid::new_v4()
|
||||
);
|
||||
create_remote_archive(handle, remote_dir, &remote_archive).await?;
|
||||
let local_extract_root = local_archive
|
||||
.parent()
|
||||
@@ -541,7 +592,11 @@ async fn download_remote_directory_archive(
|
||||
|
||||
let archive_download = async {
|
||||
download_file_impl(sftp, &remote_archive, local_archive).await?;
|
||||
extract_archive_to(local_archive, local_archive.parent().unwrap_or_else(|| Path::new("."))).await?;
|
||||
extract_archive_to(
|
||||
local_archive,
|
||||
local_archive.parent().unwrap_or_else(|| Path::new(".")),
|
||||
)
|
||||
.await?;
|
||||
tokio::fs::remove_file(local_archive)
|
||||
.await
|
||||
.with_context(|| format!("remove {}", local_archive.display()))?;
|
||||
@@ -560,14 +615,20 @@ async fn download_remote_directory_archive(
|
||||
}
|
||||
|
||||
async fn download_file_impl(sftp: &SftpSession, remote: &str, local: &Path) -> Result<()> {
|
||||
let mut remote_file = sftp.open(remote).await.with_context(|| format!("open remote {remote}"))?;
|
||||
let mut remote_file = sftp
|
||||
.open(remote)
|
||||
.await
|
||||
.with_context(|| format!("open remote {remote}"))?;
|
||||
let mut local_file = tokio::fs::File::create(local)
|
||||
.await
|
||||
.with_context(|| format!("create local {}", local.display()))?;
|
||||
|
||||
let mut buffer = vec![0u8; 64 * 1024];
|
||||
loop {
|
||||
let read = remote_file.read(&mut buffer).await.context("read remote file")?;
|
||||
let read = remote_file
|
||||
.read(&mut buffer)
|
||||
.await
|
||||
.context("read remote file")?;
|
||||
if read == 0 {
|
||||
break;
|
||||
}
|
||||
@@ -580,7 +641,11 @@ async fn download_file_impl(sftp: &SftpSession, remote: &str, local: &Path) -> R
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn upload_paths_impl(sftp: &SftpSession, locals: &[String], remote_dir: &str) -> Result<String> {
|
||||
async fn upload_paths_impl(
|
||||
sftp: &SftpSession,
|
||||
locals: &[String],
|
||||
remote_dir: &str,
|
||||
) -> Result<String> {
|
||||
create_remote_dir_all(sftp, remote_dir).await?;
|
||||
let mut file_count = 0usize;
|
||||
let mut folder_count = 0usize;
|
||||
@@ -604,7 +669,11 @@ async fn upload_paths_impl(sftp: &SftpSession, locals: &[String], remote_dir: &s
|
||||
Ok(summary)
|
||||
}
|
||||
|
||||
async fn upload_directory_impl(sftp: &SftpSession, local_dir: &Path, remote_parent: &str) -> Result<()> {
|
||||
async fn upload_directory_impl(
|
||||
sftp: &SftpSession,
|
||||
local_dir: &Path,
|
||||
remote_parent: &str,
|
||||
) -> Result<()> {
|
||||
let root_name = local_dir
|
||||
.file_name()
|
||||
.and_then(|name| name.to_str())
|
||||
@@ -643,7 +712,11 @@ async fn upload_directory_impl(sftp: &SftpSession, local_dir: &Path, remote_pare
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn upload_file_to_dir_impl(sftp: &SftpSession, local_file: &Path, remote_dir: &str) -> Result<()> {
|
||||
async fn upload_file_to_dir_impl(
|
||||
sftp: &SftpSession,
|
||||
local_file: &Path,
|
||||
remote_dir: &str,
|
||||
) -> Result<()> {
|
||||
let file_name = local_file
|
||||
.file_name()
|
||||
.and_then(|name| name.to_str())
|
||||
@@ -779,8 +852,13 @@ fn shell_quote(value: &str) -> String {
|
||||
format!("'{}'", value.replace('\'', "'\"'\"'"))
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
async fn maybe_extract_archive(path: &Path) -> Result<Option<PathBuf>> {
|
||||
let Some(file_name) = path.file_name().and_then(|name| name.to_str()).map(|name| name.to_string()) else {
|
||||
let Some(file_name) = path
|
||||
.file_name()
|
||||
.and_then(|name| name.to_str())
|
||||
.map(|name| name.to_string())
|
||||
else {
|
||||
return Ok(None);
|
||||
};
|
||||
let is_archive = [".zip", ".tar", ".tar.gz", ".tgz"]
|
||||
@@ -826,7 +904,9 @@ async fn maybe_extract_archive(path: &Path) -> Result<Option<PathBuf>> {
|
||||
.with_context(|| format!("open {}", archive_path.display()))?;
|
||||
let decoder = GzDecoder::new(file);
|
||||
let mut archive = tar::Archive::new(decoder);
|
||||
archive.unpack(&target_dir).context("unpack tar.gz archive")?;
|
||||
archive
|
||||
.unpack(&target_dir)
|
||||
.context("unpack tar.gz archive")?;
|
||||
} else if file_name.ends_with(".tar") {
|
||||
let file = fs::File::open(&archive_path)
|
||||
.with_context(|| format!("open {}", archive_path.display()))?;
|
||||
@@ -843,7 +923,11 @@ async fn maybe_extract_archive(path: &Path) -> Result<Option<PathBuf>> {
|
||||
}
|
||||
|
||||
async fn extract_archive_to(path: &Path, target_dir: &Path) -> Result<()> {
|
||||
let Some(file_name) = path.file_name().and_then(|name| name.to_str()).map(|name| name.to_string()) else {
|
||||
let Some(file_name) = path
|
||||
.file_name()
|
||||
.and_then(|name| name.to_str())
|
||||
.map(|name| name.to_string())
|
||||
else {
|
||||
return Ok(());
|
||||
};
|
||||
let archive_path = path.to_path_buf();
|
||||
@@ -878,7 +962,9 @@ async fn extract_archive_to(path: &Path, target_dir: &Path) -> Result<()> {
|
||||
.with_context(|| format!("open {}", archive_path.display()))?;
|
||||
let decoder = GzDecoder::new(file);
|
||||
let mut archive = tar::Archive::new(decoder);
|
||||
archive.unpack(&target_dir).context("unpack tar.gz archive")?;
|
||||
archive
|
||||
.unpack(&target_dir)
|
||||
.context("unpack tar.gz archive")?;
|
||||
} else if file_name.ends_with(".tar") {
|
||||
let file = fs::File::open(&archive_path)
|
||||
.with_context(|| format!("open {}", archive_path.display()))?;
|
||||
|
||||
+4
-4
@@ -3,19 +3,19 @@ use std::{
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use anyhow::{Context, Result, anyhow};
|
||||
use async_trait::async_trait;
|
||||
use directories::BaseDirs;
|
||||
use russh::{
|
||||
client::{self, Handler},
|
||||
keys::{decode_secret_key, key::PrivateKeyWithHashAlg, load_secret_key, HashAlg, PrivateKey},
|
||||
ChannelMsg, Disconnect,
|
||||
client::{self, Handler},
|
||||
keys::{HashAlg, PrivateKey, decode_secret_key, key::PrivateKeyWithHashAlg, load_secret_key},
|
||||
};
|
||||
use tokio::sync::mpsc;
|
||||
|
||||
use crate::{
|
||||
config::{AuthMethod, Session},
|
||||
system::{remote_snapshot_from_kv, SystemSnapshot},
|
||||
system::{SystemSnapshot, remote_snapshot_from_kv},
|
||||
terminal::{BackendCommand, BackendEvent, BackendTx},
|
||||
};
|
||||
|
||||
|
||||
+11
-13
@@ -1,6 +1,9 @@
|
||||
use std::{collections::BTreeMap, time::{Duration, Instant}};
|
||||
use std::{
|
||||
collections::BTreeMap,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use anyhow::{Result, anyhow};
|
||||
use sysinfo::{Disks, Networks, System};
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
@@ -69,7 +72,10 @@ impl SystemSampler {
|
||||
let rx_total: u64 = self.nets.iter().map(|(_, d)| d.total_received()).sum();
|
||||
let tx_total: u64 = self.nets.iter().map(|(_, d)| d.total_transmitted()).sum();
|
||||
let now = Instant::now();
|
||||
let elapsed = now.duration_since(self.last_instant).as_secs_f64().max(0.001);
|
||||
let elapsed = now
|
||||
.duration_since(self.last_instant)
|
||||
.as_secs_f64()
|
||||
.max(0.001);
|
||||
let rx_rate = (rx_total.saturating_sub(self.last_rx_total) as f64 / elapsed) as u64;
|
||||
let tx_rate = (tx_total.saturating_sub(self.last_tx_total) as f64 / elapsed) as u64;
|
||||
self.last_rx_total = rx_total;
|
||||
@@ -92,16 +98,8 @@ impl SystemSampler {
|
||||
cpu_percent,
|
||||
mem_percent: ratio(mem_used, mem_total),
|
||||
swap_percent: ratio(swap_used, swap_total),
|
||||
mem_detail: format!(
|
||||
"{}/{}",
|
||||
format_bytes(mem_used),
|
||||
format_bytes(mem_total)
|
||||
),
|
||||
swap_detail: format!(
|
||||
"{}/{}",
|
||||
format_bytes(swap_used),
|
||||
format_bytes(swap_total)
|
||||
),
|
||||
mem_detail: format!("{}/{}", format_bytes(mem_used), format_bytes(mem_total)),
|
||||
swap_detail: format!("{}/{}", format_bytes(swap_used), format_bytes(swap_total)),
|
||||
net_rx: format!("{}/s", format_bytes(rx_rate)),
|
||||
net_tx: format!("{}/s", format_bytes(tx_rate)),
|
||||
disks,
|
||||
|
||||
+122
-29
@@ -5,10 +5,7 @@ use alacritty_terminal::{
|
||||
grid::{Dimensions, Scroll},
|
||||
index::{Column, Point, Side},
|
||||
selection::{Selection, SelectionRange, SelectionType},
|
||||
term::{
|
||||
cell::Cell,
|
||||
point_to_viewport, viewport_to_point, Config, Term, TermMode,
|
||||
},
|
||||
term::{Config, Term, TermMode, cell::Cell, point_to_viewport, viewport_to_point},
|
||||
vte::ansi::{CursorShape, Processor},
|
||||
};
|
||||
use gpui::Keystroke;
|
||||
@@ -32,15 +29,46 @@ pub enum BackendCommand {
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum BackendEvent {
|
||||
Output { tab_id: String, bytes: Vec<u8> },
|
||||
Status { tab_id: String, text: String },
|
||||
Connected { tab_id: String },
|
||||
SftpEntries { tab_id: String, path: String, entries: Vec<RemoteEntry> },
|
||||
SftpPreview { tab_id: String, preview: PreviewData },
|
||||
SftpStatus { tab_id: String, text: String },
|
||||
RemoteSystem { tab_id: String, snapshot: SystemSnapshot },
|
||||
RemoteSystemUnavailable { tab_id: String, reason: String },
|
||||
Closed { tab_id: String, reason: String },
|
||||
Output {
|
||||
tab_id: String,
|
||||
bytes: Vec<u8>,
|
||||
},
|
||||
Status {
|
||||
tab_id: String,
|
||||
text: String,
|
||||
},
|
||||
Connected {
|
||||
tab_id: String,
|
||||
},
|
||||
SftpEntries {
|
||||
tab_id: String,
|
||||
path: String,
|
||||
entries: Vec<RemoteEntry>,
|
||||
},
|
||||
SftpPreview {
|
||||
tab_id: String,
|
||||
preview: PreviewData,
|
||||
},
|
||||
SftpStatus {
|
||||
tab_id: String,
|
||||
text: String,
|
||||
},
|
||||
RemoteSystem {
|
||||
tab_id: String,
|
||||
snapshot: SystemSnapshot,
|
||||
},
|
||||
RemoteSystemUnavailable {
|
||||
tab_id: String,
|
||||
reason: String,
|
||||
},
|
||||
Closed {
|
||||
tab_id: String,
|
||||
reason: String,
|
||||
},
|
||||
TerminalTitleChanged {
|
||||
tab_id: String,
|
||||
title: String,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
@@ -118,20 +146,42 @@ pub struct SftpUiState {
|
||||
pub entries: Vec<RemoteEntry>,
|
||||
pub selected_path: Option<String>,
|
||||
pub preview: Option<PreviewData>,
|
||||
pub selected_entries: std::collections::HashSet<String>,
|
||||
}
|
||||
|
||||
impl TerminalTab {
|
||||
pub fn new_local(id: String, title: String, backend: BackendTx) -> Self {
|
||||
Self::new(id, title, TabKind::Local, "local shell".into(), backend)
|
||||
pub fn new_local(
|
||||
id: String,
|
||||
title: String,
|
||||
backend: BackendTx,
|
||||
events: std::sync::mpsc::Sender<BackendEvent>,
|
||||
) -> Self {
|
||||
Self::new(
|
||||
id,
|
||||
title,
|
||||
TabKind::Local,
|
||||
"local shell".into(),
|
||||
backend,
|
||||
events,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn new_ssh(id: String, session: &Session, backend: BackendTx) -> Self {
|
||||
pub fn new_ssh(
|
||||
id: String,
|
||||
session: &Session,
|
||||
backend: BackendTx,
|
||||
events: std::sync::mpsc::Sender<BackendEvent>,
|
||||
) -> Self {
|
||||
let mut tab = Self::new(
|
||||
id,
|
||||
session.name.clone(),
|
||||
TabKind::Ssh,
|
||||
format!("connecting {}@{}:{}", session.user, session.host, session.port),
|
||||
format!(
|
||||
"connecting {}@{}:{}",
|
||||
session.user, session.host, session.port
|
||||
),
|
||||
backend,
|
||||
events,
|
||||
);
|
||||
tab.session = Some(session.clone());
|
||||
tab.connected = false;
|
||||
@@ -141,13 +191,21 @@ impl TerminalTab {
|
||||
entries: Vec::new(),
|
||||
selected_path: None,
|
||||
preview: None,
|
||||
selected_entries: std::collections::HashSet::new(),
|
||||
});
|
||||
tab
|
||||
}
|
||||
|
||||
fn new(id: String, title: String, kind: TabKind, status: String, backend: BackendTx) -> Self {
|
||||
fn new(
|
||||
id: String,
|
||||
title: String,
|
||||
kind: TabKind,
|
||||
status: String,
|
||||
backend: BackendTx,
|
||||
events: std::sync::mpsc::Sender<BackendEvent>,
|
||||
) -> Self {
|
||||
Self {
|
||||
id,
|
||||
id: id.clone(),
|
||||
title,
|
||||
kind,
|
||||
status,
|
||||
@@ -155,7 +213,7 @@ impl TerminalTab {
|
||||
session: None,
|
||||
sftp: None,
|
||||
processor: Processor::new(),
|
||||
term: new_term(100, 30, backend.clone()),
|
||||
term: new_term(100, 30, backend.clone(), id, events),
|
||||
cols: 100,
|
||||
rows: 30,
|
||||
backend,
|
||||
@@ -260,8 +318,11 @@ impl TerminalTab {
|
||||
self.term.scroll_display(Scroll::Bottom);
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn has_selection(&self) -> bool {
|
||||
self.term.selection_to_string().is_some_and(|text| !text.is_empty())
|
||||
self.term
|
||||
.selection_to_string()
|
||||
.is_some_and(|text| !text.is_empty())
|
||||
}
|
||||
|
||||
pub fn clear_selection(&mut self) {
|
||||
@@ -269,7 +330,9 @@ impl TerminalTab {
|
||||
}
|
||||
|
||||
pub fn selection_text(&self) -> Option<String> {
|
||||
self.term.selection_to_string().filter(|text| !text.is_empty())
|
||||
self.term
|
||||
.selection_to_string()
|
||||
.filter(|text| !text.is_empty())
|
||||
}
|
||||
|
||||
pub fn begin_selection(
|
||||
@@ -297,19 +360,25 @@ impl TerminalTab {
|
||||
}
|
||||
|
||||
pub fn paste_text(&mut self, text: &str) {
|
||||
let paste_text = text.replace('\x1b', "").replace("\r\n", "\r").replace('\n', "\r");
|
||||
let paste_text = text
|
||||
.replace('\x1b', "")
|
||||
.replace("\r\n", "\r")
|
||||
.replace('\n', "\r");
|
||||
|
||||
self.backend
|
||||
.send(BackendCommand::Input(paste_text.into_bytes()));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
fn viewport_selection_from_range(
|
||||
display_offset: usize,
|
||||
selection: &Option<SelectionRange>,
|
||||
) -> Option<ViewportSelection> {
|
||||
let SelectionRange { start, end, is_block } = selection.as_ref().copied()?;
|
||||
let SelectionRange {
|
||||
start,
|
||||
end,
|
||||
is_block,
|
||||
} = selection.as_ref().copied()?;
|
||||
let start = point_to_viewport(display_offset, start)?;
|
||||
let end = point_to_viewport(display_offset, end)?;
|
||||
|
||||
@@ -324,13 +393,17 @@ fn viewport_selection_from_range(
|
||||
|
||||
#[derive(Clone)]
|
||||
struct TerminalListener {
|
||||
tab_id: String,
|
||||
backend: BackendTx,
|
||||
events: std::sync::mpsc::Sender<BackendEvent>,
|
||||
}
|
||||
|
||||
impl EventListener for TerminalListener {
|
||||
fn send_event(&self, event: Event) {
|
||||
match event {
|
||||
Event::PtyWrite(output) => self.backend.send(BackendCommand::Input(output.into_bytes())),
|
||||
Event::PtyWrite(output) => self
|
||||
.backend
|
||||
.send(BackendCommand::Input(output.into_bytes())),
|
||||
Event::TextAreaSizeRequest(format) => {
|
||||
let size = alacritty_terminal::event::WindowSize {
|
||||
num_lines: 30,
|
||||
@@ -341,19 +414,35 @@ impl EventListener for TerminalListener {
|
||||
self.backend
|
||||
.send(BackendCommand::Input(format(size).into_bytes()));
|
||||
}
|
||||
Event::Title(title) => {
|
||||
let _ = self.events.send(BackendEvent::TerminalTitleChanged {
|
||||
tab_id: self.tab_id.clone(),
|
||||
title,
|
||||
});
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn new_term(cols: u16, rows: u16, backend: BackendTx) -> Term<TerminalListener> {
|
||||
fn new_term(
|
||||
cols: u16,
|
||||
rows: u16,
|
||||
backend: BackendTx,
|
||||
tab_id: String,
|
||||
events: std::sync::mpsc::Sender<BackendEvent>,
|
||||
) -> Term<TerminalListener> {
|
||||
Term::new(
|
||||
Config {
|
||||
scrolling_history: 2000,
|
||||
..Config::default()
|
||||
},
|
||||
&TerminalSize::new(cols, rows),
|
||||
TerminalListener { backend },
|
||||
TerminalListener {
|
||||
tab_id,
|
||||
backend,
|
||||
events,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -385,7 +474,11 @@ impl Dimensions for TerminalSize {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn encode_key(keystroke: &Keystroke, app_cursor_mode: bool, option_as_meta: bool) -> Option<Vec<u8>> {
|
||||
pub fn encode_key(
|
||||
keystroke: &Keystroke,
|
||||
app_cursor_mode: bool,
|
||||
option_as_meta: bool,
|
||||
) -> Option<Vec<u8>> {
|
||||
zed_like_to_esc_str(keystroke, app_cursor_mode, option_as_meta)
|
||||
.map(|text| text.into_owned().into_bytes())
|
||||
}
|
||||
|
||||
+56
-27
@@ -3,9 +3,10 @@ use alacritty_terminal::{
|
||||
vte::ansi::{Color as AnsiColor, CursorShape, NamedColor},
|
||||
};
|
||||
use gpui::{
|
||||
App, Bounds, Element, ElementId, Entity, FocusHandle, Font, FontStyle, FontWeight, GlobalElementId, Hsla, InputHandler, IntoElement,
|
||||
LayoutId, Pixels, Point, Rgba, StrikethroughStyle, TextRun, TextStyle, UTF16Selection, UnderlineStyle, Window, fill, point, px,
|
||||
relative, rgb,
|
||||
App, Bounds, Element, ElementId, Entity, FocusHandle, Font, FontStyle, FontWeight,
|
||||
GlobalElementId, Hsla, InputHandler, IntoElement, LayoutId, Pixels, Point, Rgba,
|
||||
StrikethroughStyle, TextRun, TextStyle, UTF16Selection, UnderlineStyle, Window, fill, point,
|
||||
px, relative, rgb,
|
||||
};
|
||||
use gpui_component::ActiveTheme as _;
|
||||
|
||||
@@ -81,7 +82,13 @@ impl BatchedTextRun {
|
||||
}
|
||||
}
|
||||
|
||||
fn paint(&self, origin: Point<Pixels>, metrics: TerminalMetrics, window: &mut Window, cx: &mut App) {
|
||||
fn paint(
|
||||
&self,
|
||||
origin: Point<Pixels>,
|
||||
metrics: TerminalMetrics,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) {
|
||||
let pos = point(
|
||||
origin.x + metrics.cell_width * self.col as f32,
|
||||
origin.y + metrics.line_height * self.row as f32,
|
||||
@@ -146,10 +153,13 @@ impl InputHandler for TerminalInputHandler {
|
||||
_window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Option<UTF16Selection> {
|
||||
self.view.read(cx).terminal_accepts_text_input().then_some(UTF16Selection {
|
||||
range: 0..0,
|
||||
reversed: false,
|
||||
})
|
||||
self.view
|
||||
.read(cx)
|
||||
.terminal_accepts_text_input()
|
||||
.then_some(UTF16Selection {
|
||||
range: 0..0,
|
||||
reversed: false,
|
||||
})
|
||||
}
|
||||
|
||||
fn marked_text_range(
|
||||
@@ -277,15 +287,21 @@ impl TerminalElement {
|
||||
fg.a *= 0.7;
|
||||
}
|
||||
|
||||
let underline = cell.flags.intersects(Flags::ALL_UNDERLINES).then(|| UnderlineStyle {
|
||||
color: Some(fg),
|
||||
thickness: px(1.0),
|
||||
wavy: cell.flags.contains(Flags::UNDERCURL),
|
||||
});
|
||||
let strikethrough = cell.flags.contains(Flags::STRIKEOUT).then(|| StrikethroughStyle {
|
||||
color: Some(fg),
|
||||
thickness: px(1.0),
|
||||
});
|
||||
let underline = cell
|
||||
.flags
|
||||
.intersects(Flags::ALL_UNDERLINES)
|
||||
.then(|| UnderlineStyle {
|
||||
color: Some(fg),
|
||||
thickness: px(1.0),
|
||||
wavy: cell.flags.contains(Flags::UNDERCURL),
|
||||
});
|
||||
let strikethrough = cell
|
||||
.flags
|
||||
.contains(Flags::STRIKEOUT)
|
||||
.then(|| StrikethroughStyle {
|
||||
color: Some(fg),
|
||||
thickness: px(1.0),
|
||||
});
|
||||
|
||||
let weight = if cell.flags.intersects(Flags::BOLD | Flags::DIM_BOLD) {
|
||||
FontWeight::BOLD
|
||||
@@ -326,10 +342,9 @@ impl TerminalElement {
|
||||
continue;
|
||||
}
|
||||
|
||||
let selected = self
|
||||
.snapshot
|
||||
.selection
|
||||
.is_some_and(|selection| selection_contains(selection, render_cell.row, render_cell.col));
|
||||
let selected = self.snapshot.selection.is_some_and(|selection| {
|
||||
selection_contains(selection, render_cell.row, render_cell.col)
|
||||
});
|
||||
let bg = color_to_hsla(cell.bg, false, cx);
|
||||
if selected || !is_default_bg(cell.bg) {
|
||||
rects.push(LayoutRect {
|
||||
@@ -525,7 +540,11 @@ impl Element for TerminalElement {
|
||||
}
|
||||
|
||||
if let Some(cursor) = prepaint.cursor {
|
||||
if self.marked_text.as_ref().is_some_and(|text| !text.is_empty()) {
|
||||
if self
|
||||
.marked_text
|
||||
.as_ref()
|
||||
.is_some_and(|text| !text.is_empty())
|
||||
{
|
||||
return;
|
||||
}
|
||||
let x = prepaint.bounds.origin.x + prepaint.metrics.cell_width * cursor.col as f32;
|
||||
@@ -534,7 +553,10 @@ impl Element for TerminalElement {
|
||||
CursorShape::Hidden => {}
|
||||
CursorShape::Beam => {
|
||||
window.paint_quad(fill(
|
||||
Bounds::new(point(x, y), gpui::size(px(2.), prepaint.metrics.line_height)),
|
||||
Bounds::new(
|
||||
point(x, y),
|
||||
gpui::size(px(2.), prepaint.metrics.line_height),
|
||||
),
|
||||
cursor.color,
|
||||
));
|
||||
}
|
||||
@@ -572,7 +594,10 @@ fn merge_rects(mut rects: Vec<LayoutRect>) -> Vec<LayoutRect> {
|
||||
|
||||
for rect in rects {
|
||||
if let Some(last) = merged.last_mut() {
|
||||
if last.row == rect.row && last.color == rect.color && last.col + last.cells as i32 == rect.col {
|
||||
if last.row == rect.row
|
||||
&& last.color == rect.color
|
||||
&& last.col + last.cells as i32 == rect.col
|
||||
{
|
||||
last.cells += rect.cells;
|
||||
continue;
|
||||
}
|
||||
@@ -601,7 +626,11 @@ fn selection_contains(selection: ViewportSelection, row: i32, col: i32) -> bool
|
||||
}
|
||||
|
||||
fn is_blank(cell: &alacritty_terminal::term::cell::Cell) -> bool {
|
||||
cell.c == ' ' && cell.zerowidth().is_none() && !cell.flags.intersects(Flags::ALL_UNDERLINES | Flags::STRIKEOUT)
|
||||
cell.c == ' '
|
||||
&& cell.zerowidth().is_none()
|
||||
&& !cell
|
||||
.flags
|
||||
.intersects(Flags::ALL_UNDERLINES | Flags::STRIKEOUT)
|
||||
}
|
||||
|
||||
fn is_default_bg(color: AnsiColor) -> bool {
|
||||
@@ -623,8 +652,8 @@ fn color_to_hsla(color: AnsiColor, foreground: bool, cx: &App) -> Hsla {
|
||||
|
||||
fn ansi_index_color(index: u8, _cx: &App) -> Hsla {
|
||||
const ANSI_16: [u32; 16] = [
|
||||
0x1f2430, 0xff5c57, 0x5af78e, 0xf3f99d, 0x57c7ff, 0xff6ac1, 0x9aedfe, 0xf1f1f0,
|
||||
0x686868, 0xff5c57, 0x5af78e, 0xf3f99d, 0x57c7ff, 0xff6ac1, 0x9aedfe, 0xffffff,
|
||||
0x1f2430, 0xff5c57, 0x5af78e, 0xf3f99d, 0x57c7ff, 0xff6ac1, 0x9aedfe, 0xf1f1f0, 0x686868,
|
||||
0xff5c57, 0x5af78e, 0xf3f99d, 0x57c7ff, 0xff6ac1, 0x9aedfe, 0xffffff,
|
||||
];
|
||||
|
||||
if (index as usize) < ANSI_16.len() {
|
||||
|
||||
Reference in New Issue
Block a user