mirror of
https://github.com/rust-kotlin/ashell.git
synced 2026-10-05 08:00:20 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4ba3000538 | ||
|
|
e7e2c67393 | ||
|
|
47b03510b2 | ||
|
|
54c50c871a | ||
|
|
6f887712e6 | ||
|
|
99071997d5 | ||
|
|
e35a2458bb | ||
|
|
690d7418f4 | ||
|
|
aca03c924f | ||
|
|
1847736d66 | ||
|
|
e163d05683 | ||
|
|
c3b1707014 | ||
|
|
8a82288c5e | ||
|
|
8c24eb885c | ||
|
|
2afcd1ea90 | ||
|
|
68439bc599 | ||
|
|
f1186e4c84 | ||
|
|
2595cc4853 | ||
|
|
2ae28df8f8 | ||
|
|
58fd03386a | ||
|
|
890ce85cfc | ||
|
|
d5fb956161 | ||
|
|
4bad75e522 | ||
|
|
9e28e78319 | ||
|
|
d561232d19 | ||
|
|
107fe8942c | ||
|
|
06796031e6 | ||
|
|
0cfd3a2fc2 | ||
|
|
7504bf6448 | ||
|
|
09ecd67ca6 | ||
|
|
6dfa199dcb | ||
|
|
4c5148fccf | ||
|
|
108a9cf95b | ||
|
|
9885b2573a | ||
|
|
cdf4cc3d53 | ||
|
|
5b381045f9 | ||
|
|
f246c952a2 | ||
|
|
2276eef191 | ||
|
|
bb39626173 | ||
|
|
2cb8ab50aa | ||
|
|
785670c428 | ||
|
|
d33babe552 | ||
|
|
985f88fb1a |
@@ -34,7 +34,7 @@ jobs:
|
|||||||
target: aarch64-apple-darwin
|
target: aarch64-apple-darwin
|
||||||
name: macos-aarch64
|
name: macos-aarch64
|
||||||
bin: ashell
|
bin: ashell
|
||||||
- os: macos-13 # Intel
|
- os: macos-14 # Intel (cross-compiled from Apple Silicon to avoid queue delays)
|
||||||
target: x86_64-apple-darwin
|
target: x86_64-apple-darwin
|
||||||
name: macos-x86_64
|
name: macos-x86_64
|
||||||
bin: ashell
|
bin: ashell
|
||||||
@@ -86,6 +86,7 @@ jobs:
|
|||||||
mkdir -p "$CONTENTS/MacOS" "$CONTENTS/Resources"
|
mkdir -p "$CONTENTS/MacOS" "$CONTENTS/Resources"
|
||||||
|
|
||||||
cp "target/${{ matrix.target }}/release/ashell" "$CONTENTS/MacOS/"
|
cp "target/${{ matrix.target }}/release/ashell" "$CONTENTS/MacOS/"
|
||||||
|
cp "assets/icons/ashell.icns" "$CONTENTS/Resources/ashell.icns"
|
||||||
|
|
||||||
VERSION_NUM="${VERSION#v}"
|
VERSION_NUM="${VERSION#v}"
|
||||||
cat > "$CONTENTS/Info.plist" <<EOF
|
cat > "$CONTENTS/Info.plist" <<EOF
|
||||||
@@ -98,6 +99,8 @@ jobs:
|
|||||||
<string>en</string>
|
<string>en</string>
|
||||||
<key>CFBundleExecutable</key>
|
<key>CFBundleExecutable</key>
|
||||||
<string>ashell</string>
|
<string>ashell</string>
|
||||||
|
<key>CFBundleIconFile</key>
|
||||||
|
<string>ashell.icns</string>
|
||||||
<key>CFBundleIdentifier</key>
|
<key>CFBundleIdentifier</key>
|
||||||
<string>dev.ashell.app</string>
|
<string>dev.ashell.app</string>
|
||||||
<key>CFBundleInfoDictionaryVersion</key>
|
<key>CFBundleInfoDictionaryVersion</key>
|
||||||
@@ -148,9 +151,20 @@ jobs:
|
|||||||
name: ${{ matrix.name }}
|
name: ${{ matrix.name }}
|
||||||
path: ${{ env.ASSET }}
|
path: ${{ env.ASSET }}
|
||||||
|
|
||||||
|
publish:
|
||||||
|
name: Publish Release
|
||||||
|
needs: build
|
||||||
|
if: startsWith(github.ref, 'refs/tags/')
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Download all workflow artifacts
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
path: dist
|
||||||
|
|
||||||
- name: Attach to GitHub Release
|
- name: Attach to GitHub Release
|
||||||
if: startsWith(github.ref, 'refs/tags/')
|
|
||||||
uses: softprops/action-gh-release@v2
|
uses: softprops/action-gh-release@v2
|
||||||
with:
|
with:
|
||||||
files: ${{ env.ASSET }}
|
files: dist/**/*
|
||||||
|
generate_release_notes: true
|
||||||
fail_on_unmatched_files: true
|
fail_on_unmatched_files: true
|
||||||
|
|||||||
Generated
+189
-23
@@ -1,6 +1,6 @@
|
|||||||
# This file is automatically @generated by Cargo.
|
# This file is automatically @generated by Cargo.
|
||||||
# It is not intended for manual editing.
|
# It is not intended for manual editing.
|
||||||
version = 3
|
version = 4
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "accesskit"
|
name = "accesskit"
|
||||||
@@ -292,19 +292,25 @@ dependencies = [
|
|||||||
"chrono",
|
"chrono",
|
||||||
"directories",
|
"directories",
|
||||||
"flate2",
|
"flate2",
|
||||||
|
"futures",
|
||||||
"gpui",
|
"gpui",
|
||||||
"gpui-component",
|
"gpui-component",
|
||||||
"gpui-component-assets",
|
"gpui-component-assets",
|
||||||
"gpui_platform",
|
"gpui_platform",
|
||||||
|
"image",
|
||||||
"menu",
|
"menu",
|
||||||
|
"notify 6.1.1",
|
||||||
|
"open",
|
||||||
"portable-pty",
|
"portable-pty",
|
||||||
"rfd",
|
"rfd",
|
||||||
"russh",
|
"russh",
|
||||||
"russh-keys",
|
"russh-keys",
|
||||||
"russh-sftp",
|
"russh-sftp",
|
||||||
|
"rust-i18n 3.1.5",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"ssh-key",
|
"ssh-key",
|
||||||
|
"sys-locale",
|
||||||
"sysinfo 0.33.1",
|
"sysinfo 0.33.1",
|
||||||
"tar",
|
"tar",
|
||||||
"thiserror 1.0.69",
|
"thiserror 1.0.69",
|
||||||
@@ -313,6 +319,7 @@ dependencies = [
|
|||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
"uuid",
|
"uuid",
|
||||||
"walkdir",
|
"walkdir",
|
||||||
|
"winres",
|
||||||
"zip",
|
"zip",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -753,7 +760,7 @@ dependencies = [
|
|||||||
"bitflags 2.12.1",
|
"bitflags 2.12.1",
|
||||||
"cexpr",
|
"cexpr",
|
||||||
"clang-sys",
|
"clang-sys",
|
||||||
"itertools 0.11.0",
|
"itertools 0.13.0",
|
||||||
"log",
|
"log",
|
||||||
"prettyplease",
|
"prettyplease",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
@@ -1409,6 +1416,15 @@ dependencies = [
|
|||||||
"cfg-if",
|
"cfg-if",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crossbeam-channel"
|
||||||
|
version = "0.5.15"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2"
|
||||||
|
dependencies = [
|
||||||
|
"crossbeam-utils",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "crossbeam-deque"
|
name = "crossbeam-deque"
|
||||||
version = "0.8.6"
|
version = "0.8.6"
|
||||||
@@ -1682,7 +1698,7 @@ dependencies = [
|
|||||||
"libc",
|
"libc",
|
||||||
"option-ext",
|
"option-ext",
|
||||||
"redox_users 0.5.2",
|
"redox_users 0.5.2",
|
||||||
"windows-sys 0.60.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1942,7 +1958,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"windows-sys 0.52.0",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2563,7 +2579,7 @@ dependencies = [
|
|||||||
"log",
|
"log",
|
||||||
"presser",
|
"presser",
|
||||||
"thiserror 2.0.18",
|
"thiserror 2.0.18",
|
||||||
"windows 0.58.0",
|
"windows 0.62.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2691,13 +2707,13 @@ dependencies = [
|
|||||||
"lsp-types",
|
"lsp-types",
|
||||||
"markdown",
|
"markdown",
|
||||||
"markup5ever_rcdom",
|
"markup5ever_rcdom",
|
||||||
"notify",
|
"notify 7.0.0",
|
||||||
"num-traits",
|
"num-traits",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"paste",
|
"paste",
|
||||||
"regex",
|
"regex",
|
||||||
"ropey",
|
"ropey",
|
||||||
"rust-i18n",
|
"rust-i18n 4.1.0",
|
||||||
"schemars",
|
"schemars",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
@@ -3258,7 +3274,7 @@ dependencies = [
|
|||||||
"js-sys",
|
"js-sys",
|
||||||
"log",
|
"log",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
"windows-core 0.58.0",
|
"windows-core 0.62.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -3453,6 +3469,17 @@ dependencies = [
|
|||||||
"serde_core",
|
"serde_core",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "inotify"
|
||||||
|
version = "0.9.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f8069d3ec154eb856955c1c0fbffefbf5f3c40a104ec912d4797314c1801abff"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 1.3.2",
|
||||||
|
"inotify-sys",
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "inotify"
|
name = "inotify"
|
||||||
version = "0.10.2"
|
version = "0.10.2"
|
||||||
@@ -3542,6 +3569,25 @@ version = "2.12.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2"
|
checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "is-docker"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "928bae27f42bc99b60d9ac7334e3a21d10ad8f1835a4e12ec3ec0464765ed1b3"
|
||||||
|
dependencies = [
|
||||||
|
"once_cell",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "is-wsl"
|
||||||
|
version = "0.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "173609498df190136aa7dea1a91db051746d339e18476eed5ca40521f02d7aa5"
|
||||||
|
dependencies = [
|
||||||
|
"is-docker",
|
||||||
|
"once_cell",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "itertools"
|
name = "itertools"
|
||||||
version = "0.11.0"
|
version = "0.11.0"
|
||||||
@@ -4104,6 +4150,18 @@ dependencies = [
|
|||||||
"simd-adler32",
|
"simd-adler32",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mio"
|
||||||
|
version = "0.8.11"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"log",
|
||||||
|
"wasi",
|
||||||
|
"windows-sys 0.48.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mio"
|
name = "mio"
|
||||||
version = "1.2.1"
|
version = "1.2.1"
|
||||||
@@ -4122,7 +4180,7 @@ version = "0.6.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "536bfad37a309d62069485248eeaba1e8d9853aaf951caaeaed0585a95346f08"
|
checksum = "536bfad37a309d62069485248eeaba1e8d9853aaf951caaeaed0585a95346f08"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"windows-sys 0.60.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -4265,6 +4323,25 @@ dependencies = [
|
|||||||
"windows-sys 0.61.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "notify"
|
||||||
|
version = "6.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6205bd8bb1e454ad2e27422015fb5e4f2bcc7e08fa8f27058670d208324a4d2d"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 2.12.1",
|
||||||
|
"crossbeam-channel",
|
||||||
|
"filetime",
|
||||||
|
"fsevent-sys",
|
||||||
|
"inotify 0.9.6",
|
||||||
|
"kqueue",
|
||||||
|
"libc",
|
||||||
|
"log",
|
||||||
|
"mio 0.8.11",
|
||||||
|
"walkdir",
|
||||||
|
"windows-sys 0.48.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "notify"
|
name = "notify"
|
||||||
version = "7.0.0"
|
version = "7.0.0"
|
||||||
@@ -4274,11 +4351,11 @@ dependencies = [
|
|||||||
"bitflags 2.12.1",
|
"bitflags 2.12.1",
|
||||||
"filetime",
|
"filetime",
|
||||||
"fsevent-sys",
|
"fsevent-sys",
|
||||||
"inotify",
|
"inotify 0.10.2",
|
||||||
"kqueue",
|
"kqueue",
|
||||||
"libc",
|
"libc",
|
||||||
"log",
|
"log",
|
||||||
"mio",
|
"mio 1.2.1",
|
||||||
"notify-types",
|
"notify-types",
|
||||||
"walkdir",
|
"walkdir",
|
||||||
"windows-sys 0.52.0",
|
"windows-sys 0.52.0",
|
||||||
@@ -4308,7 +4385,7 @@ version = "0.50.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
|
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"windows-sys 0.60.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -4702,6 +4779,17 @@ version = "0.3.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
|
checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "open"
|
||||||
|
version = "5.3.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2fbaa89d2ddc8473c78a3adf69eea8cffa28c483b8e02a971ef31527cd0fc92c"
|
||||||
|
dependencies = [
|
||||||
|
"is-wsl",
|
||||||
|
"libc",
|
||||||
|
"pathdiff",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "openssl-probe"
|
name = "openssl-probe"
|
||||||
version = "0.2.1"
|
version = "0.2.1"
|
||||||
@@ -4827,6 +4915,12 @@ version = "0.1.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "35fb2e5f958ec131621fdd531e9fc186ed768cbe395337403ae56c17a74c68ec"
|
checksum = "35fb2e5f958ec131621fdd531e9fc186ed768cbe395337403ae56c17a74c68ec"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pathdiff"
|
||||||
|
version = "0.2.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pathfinder_geometry"
|
name = "pathfinder_geometry"
|
||||||
version = "0.5.1"
|
version = "0.5.1"
|
||||||
@@ -5984,6 +6078,20 @@ dependencies = [
|
|||||||
"walkdir",
|
"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]]
|
[[package]]
|
||||||
name = "rust-i18n"
|
name = "rust-i18n"
|
||||||
version = "4.1.0"
|
version = "4.1.0"
|
||||||
@@ -5992,11 +6100,28 @@ checksum = "55691a65892c33ee2de49c15ea5600c6f4a70e8eeb8e6c3cd96d2a231d230c40"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"globwalk",
|
"globwalk",
|
||||||
"regex",
|
"regex",
|
||||||
"rust-i18n-macro",
|
"rust-i18n-macro 4.1.0",
|
||||||
"rust-i18n-support",
|
"rust-i18n-support 4.1.0",
|
||||||
"smallvec",
|
"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]]
|
[[package]]
|
||||||
name = "rust-i18n-macro"
|
name = "rust-i18n-macro"
|
||||||
version = "4.1.0"
|
version = "4.1.0"
|
||||||
@@ -6006,13 +6131,36 @@ dependencies = [
|
|||||||
"glob",
|
"glob",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"rust-i18n-support",
|
"rust-i18n-support 4.1.0",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"serde_yaml",
|
"serde_yaml",
|
||||||
"syn",
|
"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]]
|
[[package]]
|
||||||
name = "rust-i18n-support"
|
name = "rust-i18n-support"
|
||||||
version = "4.1.0"
|
version = "4.1.0"
|
||||||
@@ -6069,7 +6217,7 @@ dependencies = [
|
|||||||
"errno",
|
"errno",
|
||||||
"libc",
|
"libc",
|
||||||
"linux-raw-sys 0.4.15",
|
"linux-raw-sys 0.4.15",
|
||||||
"windows-sys 0.52.0",
|
"windows-sys 0.59.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -6082,7 +6230,7 @@ dependencies = [
|
|||||||
"errno",
|
"errno",
|
||||||
"libc",
|
"libc",
|
||||||
"linux-raw-sys 0.12.1",
|
"linux-raw-sys 0.12.1",
|
||||||
"windows-sys 0.52.0",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -6737,7 +6885,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51"
|
checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"windows-sys 0.60.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -6845,7 +6993,7 @@ dependencies = [
|
|||||||
"cfg-if",
|
"cfg-if",
|
||||||
"libc",
|
"libc",
|
||||||
"psm",
|
"psm",
|
||||||
"windows-sys 0.60.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -7205,7 +7353,7 @@ dependencies = [
|
|||||||
"getrandom 0.4.2",
|
"getrandom 0.4.2",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"rustix 1.1.4",
|
"rustix 1.1.4",
|
||||||
"windows-sys 0.52.0",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -7359,7 +7507,7 @@ checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"libc",
|
"libc",
|
||||||
"mio",
|
"mio 1.2.1",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"socket2",
|
"socket2",
|
||||||
"tokio-macros",
|
"tokio-macros",
|
||||||
@@ -7425,6 +7573,15 @@ dependencies = [
|
|||||||
"tokio",
|
"tokio",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "toml"
|
||||||
|
version = "0.5.11"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "toml"
|
name = "toml"
|
||||||
version = "0.8.23"
|
version = "0.8.23"
|
||||||
@@ -7682,7 +7839,7 @@ checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"memoffset 0.9.1",
|
"memoffset 0.9.1",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
"windows-sys 0.60.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -8442,7 +8599,7 @@ version = "0.1.11"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
|
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"windows-sys 0.48.0",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -9079,6 +9236,15 @@ dependencies = [
|
|||||||
"windows-sys 0.59.0",
|
"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]]
|
[[package]]
|
||||||
name = "winsafe"
|
name = "winsafe"
|
||||||
version = "0.0.19"
|
version = "0.0.19"
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ rfd = "0.15"
|
|||||||
russh = "0.49"
|
russh = "0.49"
|
||||||
russh-keys = "0.49"
|
russh-keys = "0.49"
|
||||||
russh-sftp = "2"
|
russh-sftp = "2"
|
||||||
|
rust-i18n = "3"
|
||||||
|
sys-locale = "0.3"
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
ssh-key = "0.6"
|
ssh-key = "0.6"
|
||||||
@@ -36,9 +38,16 @@ tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
|||||||
uuid = { version = "1", features = ["v4", "serde"] }
|
uuid = { version = "1", features = ["v4", "serde"] }
|
||||||
walkdir = "2"
|
walkdir = "2"
|
||||||
zip = { version = "2", default-features = false, features = ["deflate"] }
|
zip = { version = "2", default-features = false, features = ["deflate"] }
|
||||||
|
image = { version = "0.25.9", default-features = false, features = ["png"] }
|
||||||
|
futures = "0.3.32"
|
||||||
|
notify = "6.1.1"
|
||||||
|
open = "5.1"
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
opt-level = 3
|
opt-level = 3
|
||||||
lto = "thin"
|
lto = "thin"
|
||||||
codegen-units = 1
|
codegen-units = 1
|
||||||
strip = true
|
strip = true
|
||||||
|
|
||||||
|
[build-dependencies]
|
||||||
|
winres = "0.1.12"
|
||||||
|
|||||||
+20
-1
@@ -8,9 +8,28 @@
|
|||||||
|
|
||||||
This project focuses on providing a high-performance and visually appealing shell workspace by combining local and remote environments with a rich set of built-in features.
|
This project focuses on providing a high-performance and visually appealing shell workspace by combining local and remote environments with a rich set of built-in features.
|
||||||
|
|
||||||
|
## 🚀 v0.3 Massive Update!
|
||||||
|
|
||||||
|
The brand-new v0.3 introduces comprehensive upgrades to both architecture and user experience:
|
||||||
|
- ✨ **Global Real-Time Font Settings** (Thanks to [@ancion](https://github.com/ancion) for [PR #6](https://github.com/rust-kotlin/ashell/pull/6)): Customize your global UI fonts and terminal rendering fonts independently. Adjust text size in real-time with automatic persistence.
|
||||||
|
- ✨ **Concurrent SFTP Transfers**: A dedicated Transfers dialog now supports highly optimized, concurrent file uploads and downloads with precise progress display.
|
||||||
|
- ✨ **Persistent Workspace State**: Your sidebar width adjustments, terminal themes, font settings, and file visibility preferences are completely restored upon your next launch!
|
||||||
|
- ✨ **Intelligent SSH Disconnect/Reconnect**: Greatly enhanced SSH connection resilience. Provides a beautiful and localized overlay for one-click re-connection on unexpected disconnections.
|
||||||
|
- ✨ **Hot-Swappable i18n System**: Deeply optimized internationalization. Switch instantly between English and Simplified Chinese without restarting the application.
|
||||||
|
- ✨ **Smart Right-Click Actions**: Added intelligent right-click copy and paste for the terminal, featuring a dedicated settings toggle and OS-native keyboard shortcut hints.
|
||||||
|
|
||||||
## Download
|
## 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
|
## Features
|
||||||
|
|
||||||
|
|||||||
@@ -8,9 +8,28 @@
|
|||||||
|
|
||||||
该项目旨在通过结合本地和远程环境,并内置丰富的核心功能,提供一个高性能且美观的 Shell 工作区。
|
该项目旨在通过结合本地和远程环境,并内置丰富的核心功能,提供一个高性能且美观的 Shell 工作区。
|
||||||
|
|
||||||
|
## 🚀 v0.3 版本重磅更新!
|
||||||
|
|
||||||
|
全新发布的 v0.3 迎来了架构和体验的全面升级:
|
||||||
|
- ✨ **全局实时字体设置与字号调节**(感谢 [@ancion](https://github.com/ancion) 提供的 [PR #6](https://github.com/rust-kotlin/ashell/pull/6)):支持自定义全局 UI 字体和终端渲染字体,支持实时字号调节并自动记忆。
|
||||||
|
- ✨ **SFTP 多任务并发传输队列**:引入全新的独立传输对话框,支持多文件并发上传下载,提供精准的进度展示。
|
||||||
|
- ✨ **持久化工作区记忆**:您对侧边栏的尺寸拖拽、终端主题、字体、隐藏文件开关等一切设置都会在下次启动时原样恢复!
|
||||||
|
- ✨ **智能断线重连与状态感知**:全面增强的 SSH 网络异常处理机制,断线后直接提供美观的覆盖层用于一键重连或关闭。
|
||||||
|
- ✨ **多语言热切换增强**:深度优化的 i18n 系统,包含中文与英文在内的所有界面文案现在均支持热切换。
|
||||||
|
- ✨ **智能右键快捷操作**:新增终端右键智能复制与粘贴功能,提供专属设置项自由控制开关,并支持根据不同操作系统自适应显示原生快捷键提示。
|
||||||
|
|
||||||
## 下载
|
## 下载
|
||||||
|
|
||||||
您可以从 [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();
|
||||||
|
}
|
||||||
|
}
|
||||||
+115
@@ -0,0 +1,115 @@
|
|||||||
|
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_folder: "Download Folder"
|
||||||
|
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"
|
||||||
|
new_folder: "New Folder"
|
||||||
|
create_folder_success: "Created folder: %{name}"
|
||||||
|
create_folder_failed: "Failed to create folder: %{err}"
|
||||||
|
delete_selected: "Delete"
|
||||||
|
confirm_delete: "Confirm Delete"
|
||||||
|
confirm_delete_desc: "Are you sure you want to delete the selected %{count} items? This action cannot be undone."
|
||||||
|
system_path_warning: "WARNING: You are attempting to delete critical system paths! This may cause system corruption!"
|
||||||
|
delete_success: "Successfully deleted %{count} items"
|
||||||
|
delete_failed: "Failed to delete: %{err}"
|
||||||
|
deleting_paths: "Deleting %{count} items..."
|
||||||
|
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"
|
||||||
|
ui_font_size: "UI Font Size"
|
||||||
|
terminal_font_size: "Terminal Font Size"
|
||||||
|
system_default: "System Default"
|
||||||
|
ui_font_family: "UI Font"
|
||||||
|
terminal_font_family: "Terminal Font"
|
||||||
|
theme_management_hint: "Theme mode and light/dark theme families are managed from the top-bar theme button."
|
||||||
|
monitoring_position: "Monitoring Position"
|
||||||
|
position_bottom: "Bottom"
|
||||||
|
position_sidebar: "Sidebar"
|
||||||
|
position_hidden: "Hidden"
|
||||||
|
local_host: "local host"
|
||||||
|
local_terminal_opened: "local terminal opened"
|
||||||
|
connecting: "Connecting"
|
||||||
|
starting_connection: "starting connection..."
|
||||||
|
connection_failed: "Connection Failed"
|
||||||
|
|
||||||
|
preview_failed: "Preview failed: %{err}"
|
||||||
|
downloading_file: "Downloading %{base}..."
|
||||||
|
download_failed: "Download failed: %{err}"
|
||||||
|
uploading: "Uploading..."
|
||||||
|
upload_failed: "Upload failed: %{err}"
|
||||||
|
downloaded_folder: "Downloaded folder to %{path}"
|
||||||
|
downloaded_file: "Downloaded file to %{path}"
|
||||||
|
uploaded_file: "Uploaded file"
|
||||||
|
uploaded_folder: "Uploaded folder"
|
||||||
|
uploaded_n_files: "Uploaded %{files} files"
|
||||||
|
uploaded_n_folders: "Uploaded %{folders} folders"
|
||||||
|
uploaded_files_and_folders: "Uploaded %{files} files and %{folders} folders"
|
||||||
|
n_files: "%{files} files"
|
||||||
|
n_folders: "%{folders} folders"
|
||||||
|
n_files_and_folders: "%{files} files and %{folders} folders"
|
||||||
|
reset: "Reset"
|
||||||
|
reset_layout: "Reset Default Layout"
|
||||||
|
reset_layout_success: "Layout reset. Window size takes effect on restart."
|
||||||
|
|
||||||
|
transfers: "Transfers"
|
||||||
|
no_transfers_yet: "No transfers yet"
|
||||||
|
downloading: "Downloading"
|
||||||
|
paused: "Paused"
|
||||||
|
completed: "Completed"
|
||||||
|
failed: "Failed"
|
||||||
|
cancelled: "Cancelled"
|
||||||
|
session: "Session"
|
||||||
|
|
||||||
|
software_builtin: "Built-in"
|
||||||
|
retry: "Retry"
|
||||||
|
right_click_copy_paste: "Right-click Copy/Paste"
|
||||||
|
copy_paste_hint: "Tip: Regardless of this switch, you can always use %{key} + C/V to copy/paste."
|
||||||
|
edit_file: "Edit File"
|
||||||
|
edit_file_tooltip: "Download to temporary directory, open in default editor, and auto upload on save."
|
||||||
|
auto_saved_and_uploaded: "Auto-saved & uploaded: %{base}"
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
remote_files: "远程文件"
|
||||||
|
open_ssh_tab_sftp: "打开一个 SSH 会话面板以浏览 SFTP 文件。"
|
||||||
|
sftp_connecting: "正在连接 SFTP..."
|
||||||
|
refresh: "刷新"
|
||||||
|
upload_file: "上传文件"
|
||||||
|
upload_folder: "上传文件夹"
|
||||||
|
hidden: "显示隐藏文件"
|
||||||
|
download: "下载"
|
||||||
|
download_folder: "下载文件夹"
|
||||||
|
download_count: "下载 (%{count})"
|
||||||
|
name: "名称"
|
||||||
|
size: "大小"
|
||||||
|
modified: "修改时间"
|
||||||
|
new_ssh_connection: "新建 SSH 连接"
|
||||||
|
create_or_edit_ssh_session: "创建或编辑一个 SSH 会话"
|
||||||
|
new_folder: "新建文件夹"
|
||||||
|
create_folder_success: "成功创建文件夹: %{name}"
|
||||||
|
create_folder_failed: "创建文件夹失败: %{err}"
|
||||||
|
delete_selected: "删除"
|
||||||
|
confirm_delete: "确认删除"
|
||||||
|
confirm_delete_desc: "确定要删除以下选中的 %{count} 个项吗?此操作无法撤销。"
|
||||||
|
system_path_warning: "警告:您即将删除系统关键路径!这可能会导致系统损坏!"
|
||||||
|
delete_success: "成功删除 %{count} 个项"
|
||||||
|
delete_failed: "删除失败: %{err}"
|
||||||
|
deleting_paths: "正在删除 %{count} 个项..."
|
||||||
|
cancel: "取消"
|
||||||
|
confirm: "确认"
|
||||||
|
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: "设置"
|
||||||
|
ui_font_size: "界面字体大小"
|
||||||
|
terminal_font_size: "终端字体大小"
|
||||||
|
system_default: "系统默认"
|
||||||
|
ui_font_family: "界面字体"
|
||||||
|
terminal_font_family: "终端字体"
|
||||||
|
theme_management_hint: "深色/浅色模式以及主题配色由顶部栏的样式按钮统一管理。"
|
||||||
|
monitoring_position: "系统监控显示位置"
|
||||||
|
position_bottom: "底部"
|
||||||
|
position_sidebar: "侧边栏"
|
||||||
|
position_hidden: "不显示"
|
||||||
|
local_host: "本地主机"
|
||||||
|
local_terminal_opened: "已打开本地终端"
|
||||||
|
connecting: "正在连接"
|
||||||
|
starting_connection: "开始连接..."
|
||||||
|
connection_failed: "连接失败"
|
||||||
|
|
||||||
|
preview_failed: "预览失败: %{err}"
|
||||||
|
downloading_file: "正在下载 %{base}..."
|
||||||
|
download_failed: "下载失败: %{err}"
|
||||||
|
uploading: "正在上传..."
|
||||||
|
upload_failed: "上传失败: %{err}"
|
||||||
|
downloaded_folder: "已下载文件夹至 %{path}"
|
||||||
|
downloaded_file: "已下载文件至 %{path}"
|
||||||
|
uploaded_file: "已上传文件"
|
||||||
|
uploaded_folder: "已上传文件夹"
|
||||||
|
uploaded_n_files: "已上传 %{files} 个文件"
|
||||||
|
uploaded_n_folders: "已上传 %{folders} 个文件夹"
|
||||||
|
uploaded_files_and_folders: "已上传 %{files} 个文件及 %{folders} 个文件夹"
|
||||||
|
n_files: "%{files} 个文件"
|
||||||
|
n_folders: "%{folders} 个文件夹"
|
||||||
|
n_files_and_folders: "%{files} 个文件及 %{folders} 个文件夹"
|
||||||
|
reset: "重置"
|
||||||
|
reset_layout: "恢复默认布局"
|
||||||
|
reset_layout_success: "布局占比已恢复默认。窗口大小将在下次启动时生效。"
|
||||||
|
|
||||||
|
transfers: "传输记录"
|
||||||
|
no_transfers_yet: "暂无传输记录"
|
||||||
|
downloading: "正在下载"
|
||||||
|
paused: "已暂停"
|
||||||
|
completed: "已完成"
|
||||||
|
failed: "失败"
|
||||||
|
cancelled: "已取消"
|
||||||
|
session: "会话"
|
||||||
|
|
||||||
|
software_builtin: "软件内置"
|
||||||
|
retry: "重试"
|
||||||
|
right_click_copy_paste: "右键复制/粘贴"
|
||||||
|
copy_paste_hint: "提示:不管是否开启此开关,都可以使用 %{key} + C/V 快捷键进行复制和粘贴。"
|
||||||
|
edit_file: "编辑文件"
|
||||||
|
edit_file_tooltip: "下载到临时目录,使用默认编辑器打开,并在保存后自动上传。"
|
||||||
|
auto_saved_and_uploaded: "已自动保存并上传: %{base}"
|
||||||
BIN
Binary file not shown.
|
Before Width: | Height: | Size: 292 KiB After Width: | Height: | Size: 279 KiB |
@@ -17,6 +17,8 @@ rm -rf "$APP_DIR"
|
|||||||
mkdir -p "$MACOS_DIR" "$RESOURCES_DIR"
|
mkdir -p "$MACOS_DIR" "$RESOURCES_DIR"
|
||||||
cp "$ROOT_DIR/target/release/$APP_NAME" "$MACOS_DIR/$APP_NAME"
|
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
|
cat > "$CONTENTS_DIR/Info.plist" <<EOF
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
|
||||||
@@ -27,6 +29,8 @@ cat > "$CONTENTS_DIR/Info.plist" <<EOF
|
|||||||
<string>en</string>
|
<string>en</string>
|
||||||
<key>CFBundleExecutable</key>
|
<key>CFBundleExecutable</key>
|
||||||
<string>$APP_NAME</string>
|
<string>$APP_NAME</string>
|
||||||
|
<key>CFBundleIconFile</key>
|
||||||
|
<string>ashell.icns</string>
|
||||||
<key>CFBundleIdentifier</key>
|
<key>CFBundleIdentifier</key>
|
||||||
<string>$BUNDLE_ID</string>
|
<string>$BUNDLE_ID</string>
|
||||||
<key>CFBundleInfoDictionaryVersion</key>
|
<key>CFBundleInfoDictionaryVersion</key>
|
||||||
|
|||||||
+667
@@ -0,0 +1,667 @@
|
|||||||
|
use std::{
|
||||||
|
cell::{Cell, RefCell},
|
||||||
|
collections::HashMap,
|
||||||
|
ops::Range,
|
||||||
|
rc::Rc,
|
||||||
|
sync::mpsc,
|
||||||
|
time::{Duration, Instant},
|
||||||
|
};
|
||||||
|
|
||||||
|
use gpui::{
|
||||||
|
AppContext as _, Bounds, Context, Entity, FocusHandle, Pixels, Point,
|
||||||
|
SharedString, Size, UniformListScrollHandle, Window, point,
|
||||||
|
px, size,
|
||||||
|
};
|
||||||
|
use gpui_component::{
|
||||||
|
Theme, ThemeMode, ThemeRegistry,
|
||||||
|
input::{InputEvent, InputState},
|
||||||
|
resizable::ResizableState,
|
||||||
|
scroll::ScrollbarHandle,
|
||||||
|
};
|
||||||
|
use rust_i18n::t;
|
||||||
|
use tokio::runtime::Runtime;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
config::{AuthMethod, ConfigStore},
|
||||||
|
sftp::SftpHandle,
|
||||||
|
system::{SystemSampler, SystemSnapshot},
|
||||||
|
terminal::{self, BackendEvent, TabKind, TerminalTab},
|
||||||
|
ssh_terminal,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub(crate) struct TerminalScrollbarState {
|
||||||
|
line_height: Pixels,
|
||||||
|
total_lines: usize,
|
||||||
|
viewport_lines: usize,
|
||||||
|
display_offset: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Default)]
|
||||||
|
pub(crate) struct TerminalScrollbarHandle {
|
||||||
|
state: Rc<RefCell<Option<TerminalScrollbarState>>>,
|
||||||
|
pub(crate) future_display_offset: Rc<Cell<Option<usize>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TerminalScrollbarHandle {
|
||||||
|
pub(crate) fn update(&self, snapshot: &terminal::RenderSnapshot, line_height: Pixels) {
|
||||||
|
self.state.replace(Some(TerminalScrollbarState {
|
||||||
|
line_height,
|
||||||
|
total_lines: snapshot.history_size + snapshot.rows,
|
||||||
|
viewport_lines: snapshot.rows,
|
||||||
|
display_offset: snapshot.display_offset,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ScrollbarHandle for TerminalScrollbarHandle {
|
||||||
|
fn offset(&self) -> Point<Pixels> {
|
||||||
|
let state_ref = self.state.borrow();
|
||||||
|
let Some(state) = state_ref.as_ref() else {
|
||||||
|
return point(px(0.), px(0.));
|
||||||
|
};
|
||||||
|
let scroll_offset = state
|
||||||
|
.total_lines
|
||||||
|
.saturating_sub(state.viewport_lines)
|
||||||
|
.saturating_sub(state.display_offset);
|
||||||
|
point(px(0.), -(scroll_offset as f32 * state.line_height))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_offset(&self, offset: Point<Pixels>) {
|
||||||
|
let state_ref = self.state.borrow();
|
||||||
|
let Some(state) = state_ref.as_ref() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let offset_delta = (offset.y / state.line_height).round() as i32;
|
||||||
|
let max_offset = state.total_lines.saturating_sub(state.viewport_lines);
|
||||||
|
let display_offset = (max_offset as i32 + offset_delta).clamp(0, max_offset as i32);
|
||||||
|
self.future_display_offset
|
||||||
|
.set(Some(display_offset as usize));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn content_size(&self) -> Size<Pixels> {
|
||||||
|
let state_ref = self.state.borrow();
|
||||||
|
let Some(state) = state_ref.as_ref() else {
|
||||||
|
return size(px(0.), px(0.));
|
||||||
|
};
|
||||||
|
size(
|
||||||
|
px(0.),
|
||||||
|
state.total_lines.max(state.viewport_lines) as f32 * state.line_height,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
pub(crate) struct Ashell {
|
||||||
|
pub(crate) focus_handle: FocusHandle,
|
||||||
|
pub(crate) selector_focus_handle: FocusHandle,
|
||||||
|
pub(crate) host_input: Entity<InputState>,
|
||||||
|
pub(crate) session_name_input: Entity<InputState>,
|
||||||
|
pub(crate) port_input: Entity<InputState>,
|
||||||
|
pub(crate) user_input: Entity<InputState>,
|
||||||
|
pub(crate) password_input: Entity<InputState>,
|
||||||
|
pub(crate) key_path_input: Entity<InputState>,
|
||||||
|
pub(crate) key_inline_input: Entity<InputState>,
|
||||||
|
pub(crate) sftp_path_input: Entity<InputState>,
|
||||||
|
pub(crate) ssh_auth_method: AuthMethod,
|
||||||
|
pub(crate) editing_session_id: Option<String>,
|
||||||
|
pub(crate) follow_system_theme: bool,
|
||||||
|
pub(crate) theme_mode: ThemeMode,
|
||||||
|
pub(crate) light_theme_name: SharedString,
|
||||||
|
pub(crate) dark_theme_name: SharedString,
|
||||||
|
pub(crate) ui_font_size: f32,
|
||||||
|
pub(crate) terminal_font_size: f32,
|
||||||
|
pub(crate) ui_font_family: SharedString,
|
||||||
|
pub(crate) terminal_font_family: SharedString,
|
||||||
|
pub(crate) tabs: Vec<TerminalTab>,
|
||||||
|
pub(crate) sftp_handles: HashMap<String, SftpHandle>,
|
||||||
|
pub(crate) active_tab: Option<String>,
|
||||||
|
pub(crate) selector_selection: usize,
|
||||||
|
pub(crate) workspace_panels: Entity<ResizableState>,
|
||||||
|
pub(crate) body_panels: Entity<ResizableState>,
|
||||||
|
pub(crate) terminal_scrollbar: TerminalScrollbarHandle,
|
||||||
|
pub(crate) remote_files_scroll_handle: UniformListScrollHandle,
|
||||||
|
pub(crate) disk_scroll_handle: gpui::ScrollHandle,
|
||||||
|
pub(crate) tabs_scroll_handle: gpui::ScrollHandle,
|
||||||
|
pub(crate) selector_scroll_handle: gpui::ScrollHandle,
|
||||||
|
pub(crate) saved_scroll_handle: gpui::ScrollHandle,
|
||||||
|
pub(crate) connection_progress: Option<ConnectionProgress>,
|
||||||
|
pub(crate) pending_sftp_path_sync: Option<String>,
|
||||||
|
pub(crate) sftp_context_menu: Option<SftpContextMenuState>,
|
||||||
|
pub(crate) sftp_creating_folder: bool,
|
||||||
|
pub(crate) sftp_new_folder_input: Entity<InputState>,
|
||||||
|
pub(crate) sftp_delete_scroll_handle: gpui::ScrollHandle,
|
||||||
|
pub(crate) show_hidden_files: bool,
|
||||||
|
pub(crate) transfers: Vec<crate::terminal::Transfer>,
|
||||||
|
pub(crate) show_transfers_dialog: bool,
|
||||||
|
pub(crate) system_status: Option<SharedString>,
|
||||||
|
pub(crate) terminal_bounds: Option<Bounds<Pixels>>,
|
||||||
|
pub(crate) terminal_selecting: bool,
|
||||||
|
pub(crate) terminal_marked_text: Option<String>,
|
||||||
|
pub(crate) sftp_panel_minimized: bool,
|
||||||
|
pub(crate) prev_monitoring_size: Option<Pixels>,
|
||||||
|
pub(crate) status: SharedString,
|
||||||
|
pub(crate) config: ConfigStore,
|
||||||
|
pub(crate) system_sampler: SystemSampler,
|
||||||
|
pub(crate) system: SystemSnapshot,
|
||||||
|
pub(crate) cpu_history: Vec<f32>,
|
||||||
|
pub(crate) net_rx_history: Vec<f32>,
|
||||||
|
pub(crate) net_tx_history: Vec<f32>,
|
||||||
|
pub(crate) last_system_sample: Instant,
|
||||||
|
pub(crate) last_theme_sync: Instant,
|
||||||
|
|
||||||
|
pub(crate) remote_sample_in_flight: bool,
|
||||||
|
pub(crate) runtime: Runtime,
|
||||||
|
pub(crate) events_rx: mpsc::Receiver<BackendEvent>,
|
||||||
|
pub(crate) events_tx: mpsc::Sender<BackendEvent>,
|
||||||
|
pub(crate) _subscriptions: Vec<gpui::Subscription>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub(crate) enum SelectorEntry {
|
||||||
|
Local,
|
||||||
|
NewSsh,
|
||||||
|
Saved(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub(crate) struct ConnectionProgress {
|
||||||
|
pub(crate) tab_id: String,
|
||||||
|
pub(crate) title: SharedString,
|
||||||
|
pub(crate) lines: Vec<SharedString>,
|
||||||
|
pub(crate) failed: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub(crate) struct SftpContextMenuState {
|
||||||
|
pub(crate) remote_path: String,
|
||||||
|
pub(crate) is_dir: bool,
|
||||||
|
pub(crate) position: Point<Pixels>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Ashell {
|
||||||
|
pub(crate) fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||||
|
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(t!("password"))
|
||||||
|
.masked(true)
|
||||||
|
});
|
||||||
|
let key_path_input =
|
||||||
|
cx.new(|cx| InputState::new(window, cx).placeholder("~/.ssh/id_ed25519"));
|
||||||
|
let key_inline_input = cx.new(|cx| {
|
||||||
|
InputState::new(window, cx)
|
||||||
|
.multi_line(true)
|
||||||
|
.rows(5)
|
||||||
|
.placeholder("-----BEGIN OPENSSH PRIVATE KEY-----")
|
||||||
|
});
|
||||||
|
let sftp_path_input = cx.new(|cx| InputState::new(window, cx).default_value("/"));
|
||||||
|
let sftp_new_folder_input = cx.new(|cx| InputState::new(window, cx).placeholder(t!("new_folder").to_string()));
|
||||||
|
|
||||||
|
let _subscriptions = vec![
|
||||||
|
cx.subscribe_in(&host_input, window, Self::on_input_event),
|
||||||
|
cx.subscribe_in(&session_name_input, window, Self::on_input_event),
|
||||||
|
cx.subscribe_in(&port_input, window, Self::on_input_event),
|
||||||
|
cx.subscribe_in(&user_input, window, Self::on_input_event),
|
||||||
|
cx.subscribe_in(&password_input, window, Self::on_input_event),
|
||||||
|
cx.subscribe_in(&key_path_input, window, Self::on_input_event),
|
||||||
|
cx.subscribe_in(&key_inline_input, window, Self::on_input_event),
|
||||||
|
cx.subscribe_in(&sftp_path_input, window, Self::on_input_event),
|
||||||
|
cx.subscribe_in(&sftp_new_folder_input, window, Self::on_input_event),
|
||||||
|
];
|
||||||
|
|
||||||
|
let (events_tx, events_rx) = mpsc::channel();
|
||||||
|
let workspace_panels = cx.new(|_| ResizableState::default());
|
||||||
|
let body_panels = cx.new(|_| ResizableState::default());
|
||||||
|
let mut system_sampler = SystemSampler::new();
|
||||||
|
let system = system_sampler.sample();
|
||||||
|
let default_light_theme_name = ThemeRegistry::global(cx).default_light_theme().name.clone();
|
||||||
|
let default_dark_theme_name = ThemeRegistry::global(cx).default_dark_theme().name.clone();
|
||||||
|
let config = ConfigStore::load().unwrap_or_else(|err| {
|
||||||
|
tracing::warn!("failed to load config: {err:#}");
|
||||||
|
ConfigStore::in_memory()
|
||||||
|
});
|
||||||
|
let follow_system_theme =
|
||||||
|
if config.light_theme_name().is_empty() && config.dark_theme_name().is_empty() {
|
||||||
|
true
|
||||||
|
} 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 {
|
||||||
|
config.light_theme_name().into()
|
||||||
|
};
|
||||||
|
let dark_theme_name = if config.dark_theme_name().is_empty() {
|
||||||
|
default_dark_theme_name
|
||||||
|
} 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 ui_font_family: SharedString = config.ui_font_family().into();
|
||||||
|
let terminal_font_family: SharedString = config.terminal_font_family().into();
|
||||||
|
let mut this = Self {
|
||||||
|
focus_handle: cx.focus_handle(),
|
||||||
|
selector_focus_handle: cx.focus_handle(),
|
||||||
|
host_input,
|
||||||
|
session_name_input,
|
||||||
|
port_input,
|
||||||
|
user_input,
|
||||||
|
password_input,
|
||||||
|
key_path_input,
|
||||||
|
key_inline_input,
|
||||||
|
sftp_path_input,
|
||||||
|
ssh_auth_method: AuthMethod::Password,
|
||||||
|
editing_session_id: None,
|
||||||
|
follow_system_theme,
|
||||||
|
theme_mode,
|
||||||
|
light_theme_name,
|
||||||
|
dark_theme_name,
|
||||||
|
ui_font_size: config.ui_font_size(),
|
||||||
|
terminal_font_size: config.terminal_font_size(),
|
||||||
|
ui_font_family,
|
||||||
|
terminal_font_family,
|
||||||
|
tabs: Vec::new(),
|
||||||
|
sftp_handles: HashMap::new(),
|
||||||
|
active_tab: None,
|
||||||
|
selector_selection: 0,
|
||||||
|
workspace_panels,
|
||||||
|
body_panels,
|
||||||
|
terminal_scrollbar: TerminalScrollbarHandle::default(),
|
||||||
|
remote_files_scroll_handle: UniformListScrollHandle::new(),
|
||||||
|
disk_scroll_handle: gpui::ScrollHandle::new(),
|
||||||
|
tabs_scroll_handle: gpui::ScrollHandle::new(),
|
||||||
|
selector_scroll_handle: gpui::ScrollHandle::new(),
|
||||||
|
saved_scroll_handle: gpui::ScrollHandle::new(),
|
||||||
|
connection_progress: None,
|
||||||
|
pending_sftp_path_sync: Some("/".into()),
|
||||||
|
sftp_context_menu: None,
|
||||||
|
sftp_creating_folder: false,
|
||||||
|
sftp_new_folder_input,
|
||||||
|
sftp_delete_scroll_handle: gpui::ScrollHandle::new(),
|
||||||
|
show_hidden_files: config.show_hidden_files(),
|
||||||
|
transfers: config.transfers(),
|
||||||
|
show_transfers_dialog: false,
|
||||||
|
system_status: None,
|
||||||
|
terminal_bounds: None,
|
||||||
|
terminal_selecting: false,
|
||||||
|
terminal_marked_text: None,
|
||||||
|
sftp_panel_minimized: false,
|
||||||
|
prev_monitoring_size: None,
|
||||||
|
status: "ready".into(),
|
||||||
|
config,
|
||||||
|
system_sampler,
|
||||||
|
system,
|
||||||
|
cpu_history: Vec::with_capacity(20),
|
||||||
|
net_rx_history: Vec::with_capacity(20),
|
||||||
|
net_tx_history: Vec::with_capacity(20),
|
||||||
|
last_system_sample: Instant::now(),
|
||||||
|
last_theme_sync: Instant::now(),
|
||||||
|
|
||||||
|
remote_sample_in_flight: false,
|
||||||
|
runtime: Runtime::new().expect("create tokio runtime"),
|
||||||
|
events_rx,
|
||||||
|
events_tx,
|
||||||
|
_subscriptions,
|
||||||
|
};
|
||||||
|
|
||||||
|
this.apply_theme_preferences(window, cx);
|
||||||
|
// this.open_local(cx);
|
||||||
|
this.start_event_pump(cx);
|
||||||
|
this
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn on_input_event(
|
||||||
|
&mut self,
|
||||||
|
input: &Entity<InputState>,
|
||||||
|
event: &InputEvent,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
if input == &self.sftp_path_input {
|
||||||
|
if let InputEvent::PressEnter { .. } = event {
|
||||||
|
let path = self
|
||||||
|
.sftp_path_input
|
||||||
|
.read(cx)
|
||||||
|
.text()
|
||||||
|
.to_string()
|
||||||
|
.trim()
|
||||||
|
.to_string();
|
||||||
|
self.navigate_sftp(if path.is_empty() { "/".into() } else { path }, cx);
|
||||||
|
window.prevent_default();
|
||||||
|
cx.stop_propagation();
|
||||||
|
}
|
||||||
|
} else if input == &self.sftp_new_folder_input {
|
||||||
|
match event {
|
||||||
|
InputEvent::PressEnter { .. } => {
|
||||||
|
let name = self.sftp_new_folder_input.read(cx).text().to_string();
|
||||||
|
if !name.is_empty() {
|
||||||
|
let base_path = self.sftp_path_input.read(cx).text().to_string();
|
||||||
|
let path = crate::sftp::join_remote(&base_path, &name);
|
||||||
|
if let Some(id) = self.active_tab.clone() {
|
||||||
|
if let Some(handle) = self.sftp_handles.get(&id) {
|
||||||
|
let _ = handle.commands.send(crate::sftp::SftpCommand::CreateDir(path));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.sftp_creating_folder = false;
|
||||||
|
window.prevent_default();
|
||||||
|
cx.stop_propagation();
|
||||||
|
}
|
||||||
|
InputEvent::Blur => {
|
||||||
|
self.sftp_creating_folder = false;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn start_event_pump(&self, cx: &mut Context<Self>) {
|
||||||
|
cx.spawn(async move |this, cx| {
|
||||||
|
loop {
|
||||||
|
cx.background_executor()
|
||||||
|
.timer(Duration::from_millis(16))
|
||||||
|
.await;
|
||||||
|
if this
|
||||||
|
.update(cx, |this, cx| {
|
||||||
|
this.drain_backend_events();
|
||||||
|
this.sample_system_if_due();
|
||||||
|
this.sync_theme_if_due(cx);
|
||||||
|
cx.notify();
|
||||||
|
})
|
||||||
|
.is_err()
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn drain_backend_events(&mut self) {
|
||||||
|
let mut transfers_changed = false;
|
||||||
|
while let Ok(event) = self.events_rx.try_recv() {
|
||||||
|
match event {
|
||||||
|
BackendEvent::Output { tab_id, bytes } => {
|
||||||
|
if let Some(tab) = self.tabs.iter_mut().find(|t| t.id == tab_id) {
|
||||||
|
tab.feed(&bytes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
BackendEvent::Status { tab_id, text } => {
|
||||||
|
if let Some(tab) = self.tabs.iter_mut().find(|t| t.id == tab_id) {
|
||||||
|
tab.status = text.clone();
|
||||||
|
}
|
||||||
|
if let Some(progress) = self.connection_progress.as_mut() {
|
||||||
|
if progress.tab_id == tab_id {
|
||||||
|
progress.lines.push(text.clone().into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.status = text.into();
|
||||||
|
}
|
||||||
|
BackendEvent::Connected { tab_id } => {
|
||||||
|
if let Some(tab) = self.tabs.iter_mut().find(|t| t.id == tab_id) {
|
||||||
|
tab.connected = true;
|
||||||
|
}
|
||||||
|
self.request_active_system_snapshot();
|
||||||
|
if self
|
||||||
|
.connection_progress
|
||||||
|
.as_ref()
|
||||||
|
.is_some_and(|progress| progress.tab_id == tab_id)
|
||||||
|
{
|
||||||
|
self.connection_progress = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
BackendEvent::SftpEntries {
|
||||||
|
tab_id,
|
||||||
|
path,
|
||||||
|
entries,
|
||||||
|
} => {
|
||||||
|
if let Some(tab) = self.tabs.iter_mut().find(|t| t.id == tab_id) {
|
||||||
|
if let Some(sftp) = tab.sftp.as_mut() {
|
||||||
|
sftp.current_path = path;
|
||||||
|
sftp.entries = entries;
|
||||||
|
if self.active_tab.as_deref() == Some(tab_id.as_str()) {
|
||||||
|
self.pending_sftp_path_sync = Some(sftp.current_path.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
BackendEvent::SftpPreview { tab_id, preview } => {
|
||||||
|
if let Some(tab) = self.tabs.iter_mut().find(|t| t.id == tab_id) {
|
||||||
|
if let Some(sftp) = tab.sftp.as_mut() {
|
||||||
|
sftp.selected_path = Some(preview.path.clone());
|
||||||
|
sftp.preview = Some(preview);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
BackendEvent::SftpStatus { tab_id, text } => {
|
||||||
|
if let Some(tab) = self.tabs.iter_mut().find(|t| t.id == tab_id) {
|
||||||
|
if let Some(sftp) = tab.sftp.as_mut() {
|
||||||
|
sftp.status = text.clone();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.status = text.into();
|
||||||
|
}
|
||||||
|
BackendEvent::RemoteSystem { tab_id, snapshot } => {
|
||||||
|
self.remote_sample_in_flight = false;
|
||||||
|
if self.active_tab.as_deref() == Some(tab_id.as_str()) {
|
||||||
|
self.system_status = None;
|
||||||
|
self.system = snapshot.clone();
|
||||||
|
self.cpu_history.push(snapshot.cpu_percent);
|
||||||
|
if self.cpu_history.len() > 20 {
|
||||||
|
self.cpu_history.remove(0);
|
||||||
|
}
|
||||||
|
self.net_rx_history.push(snapshot.net_rx_rate as f32);
|
||||||
|
if self.net_rx_history.len() > 20 {
|
||||||
|
self.net_rx_history.remove(0);
|
||||||
|
}
|
||||||
|
self.net_tx_history.push(snapshot.net_tx_rate as f32);
|
||||||
|
if self.net_tx_history.len() > 20 {
|
||||||
|
self.net_tx_history.remove(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
BackendEvent::RemoteSystemUnavailable { tab_id, reason } => {
|
||||||
|
self.remote_sample_in_flight = false;
|
||||||
|
if self.active_tab.as_deref() == Some(tab_id.as_str()) {
|
||||||
|
self.system_status = Some(reason.clone().into());
|
||||||
|
self.status = reason.into();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
BackendEvent::Closed { tab_id, reason } => {
|
||||||
|
self.remote_sample_in_flight = false;
|
||||||
|
let mut tab_title = None;
|
||||||
|
let mut session_label = None;
|
||||||
|
if let Some(tab) = self.tabs.iter_mut().find(|t| t.id == tab_id) {
|
||||||
|
tab.connected = false;
|
||||||
|
tab.status = reason.clone();
|
||||||
|
tab_title = Some(tab.title.clone());
|
||||||
|
session_label = tab.session.as_ref().map(|session| {
|
||||||
|
format!("{}@{}:{}", session.user, session.host, session.port)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if self.active_tab.as_deref() == Some(tab_id.as_str()) {
|
||||||
|
self.system_status = Some(reason.clone().into());
|
||||||
|
}
|
||||||
|
let is_graceful_exit = reason == "Terminal process exited"
|
||||||
|
|| reason == "SSH session exited";
|
||||||
|
if let Some(progress) = self.connection_progress.as_mut() {
|
||||||
|
if progress.tab_id == tab_id {
|
||||||
|
progress.lines.push(reason.clone().into());
|
||||||
|
let _ = session_label;
|
||||||
|
let _ = tab_title;
|
||||||
|
progress.title = t!("connection_failed").into();
|
||||||
|
progress.failed = true;
|
||||||
|
}
|
||||||
|
} else if let Some(_) = session_label {
|
||||||
|
if !is_graceful_exit {
|
||||||
|
self.connection_progress = Some(ConnectionProgress {
|
||||||
|
tab_id: tab_id.clone(),
|
||||||
|
title: t!("connection_failed").into(),
|
||||||
|
lines: vec![reason.clone().into()],
|
||||||
|
failed: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.status = reason.into();
|
||||||
|
}
|
||||||
|
BackendEvent::TransferProgress {
|
||||||
|
tab_id: _,
|
||||||
|
id,
|
||||||
|
transferred,
|
||||||
|
total,
|
||||||
|
state,
|
||||||
|
} => {
|
||||||
|
if let Some(t) = self.transfers.iter_mut().find(|t| t.info.id == id) {
|
||||||
|
t.transferred = transferred;
|
||||||
|
if let Some(total) = total {
|
||||||
|
t.total = Some(total);
|
||||||
|
}
|
||||||
|
t.state = state;
|
||||||
|
transfers_changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
BackendEvent::TransferStarted { tab_id, info } => {
|
||||||
|
let tab_title = self
|
||||||
|
.tabs
|
||||||
|
.iter()
|
||||||
|
.find(|t| t.id == tab_id)
|
||||||
|
.map(|t| t.title.clone())
|
||||||
|
.unwrap_or_else(|| "Unknown".to_string());
|
||||||
|
self.transfers.insert(
|
||||||
|
0,
|
||||||
|
crate::terminal::Transfer {
|
||||||
|
tab_id,
|
||||||
|
tab_title,
|
||||||
|
info,
|
||||||
|
transferred: 0,
|
||||||
|
total: None,
|
||||||
|
state: crate::terminal::TransferState::Running,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
transfers_changed = true;
|
||||||
|
}
|
||||||
|
BackendEvent::SftpHome { tab_id, home } => {
|
||||||
|
if let Some(tab) = self.tabs.iter_mut().find(|t| t.id == tab_id) {
|
||||||
|
if let Some(sftp) = tab.sftp.as_mut() {
|
||||||
|
sftp.home_dir = home;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
BackendEvent::TerminalTitleChanged { tab_id, title } => {
|
||||||
|
if let Some(tab) = self.tabs.iter_mut().find(|t| t.id == tab_id) {
|
||||||
|
tab.title = title.clone();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if transfers_changed {
|
||||||
|
self.config.set_transfers(self.transfers.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn sample_system_if_due(&mut self) {
|
||||||
|
if self.last_system_sample.elapsed() >= SystemSampler::interval() {
|
||||||
|
self.last_system_sample = Instant::now();
|
||||||
|
// When an SSH tab is active with remote data flowing, don't push
|
||||||
|
// local machine data into history. Instead trigger a remote fetch
|
||||||
|
// so both the current values and history reflect the remote server.
|
||||||
|
if matches!(self.active_kind(), Some(TabKind::Ssh)) && self.system_status.is_none() {
|
||||||
|
self.request_active_system_snapshot();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let snapshot = self.system_sampler.sample();
|
||||||
|
let cpu_usage = snapshot.cpu_percent;
|
||||||
|
self.cpu_history.push(cpu_usage);
|
||||||
|
if self.cpu_history.len() > 20 {
|
||||||
|
self.cpu_history.remove(0);
|
||||||
|
}
|
||||||
|
self.net_rx_history.push(snapshot.net_rx_rate as f32);
|
||||||
|
if self.net_rx_history.len() > 20 {
|
||||||
|
self.net_rx_history.remove(0);
|
||||||
|
}
|
||||||
|
self.net_tx_history.push(snapshot.net_tx_rate as f32);
|
||||||
|
if self.net_tx_history.len() > 20 {
|
||||||
|
self.net_tx_history.remove(0);
|
||||||
|
}
|
||||||
|
self.system = snapshot;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn sync_theme_if_due(&mut self, cx: &mut Context<Self>) {
|
||||||
|
if self.follow_system_theme && self.last_theme_sync.elapsed() >= Duration::from_secs(1) {
|
||||||
|
self.last_theme_sync = Instant::now();
|
||||||
|
Theme::sync_system_appearance(None, cx);
|
||||||
|
cx.refresh_windows();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn request_active_system_snapshot(&mut self) {
|
||||||
|
if let Some((tab_id, session)) = self.active_ssh_session() {
|
||||||
|
if self.remote_sample_in_flight {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self.remote_sample_in_flight = true;
|
||||||
|
let events = self.events_tx.clone();
|
||||||
|
self.runtime.spawn(async move {
|
||||||
|
match ssh_terminal::sample_remote_system(session).await {
|
||||||
|
Ok(snapshot) => {
|
||||||
|
let _ = events.send(BackendEvent::RemoteSystem { tab_id, snapshot });
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
let _ = events.send(BackendEvent::RemoteSystemUnavailable {
|
||||||
|
tab_id,
|
||||||
|
reason: format!("remote metrics unavailable: {err:#}"),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn terminal_ime_bounds_for_range(
|
||||||
|
&self,
|
||||||
|
range_utf16: Range<usize>,
|
||||||
|
element_bounds: Bounds<Pixels>,
|
||||||
|
cell_width: f32,
|
||||||
|
line_height: f32,
|
||||||
|
) -> Option<Bounds<Pixels>> {
|
||||||
|
let snapshot = self.active_snapshot()?;
|
||||||
|
let cursor = snapshot.cursor?;
|
||||||
|
let x = element_bounds.origin.x
|
||||||
|
+ px(cell_width) * cursor.col as f32
|
||||||
|
+ px(cell_width) * range_utf16.start as f32;
|
||||||
|
let y = element_bounds.origin.y
|
||||||
|
+ px(line_height) * cursor.row as f32;
|
||||||
|
Some(Bounds::new(
|
||||||
|
point(x, y),
|
||||||
|
size(
|
||||||
|
px(cell_width),
|
||||||
|
px(line_height),
|
||||||
|
),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
+191
-1
@@ -70,24 +70,93 @@ impl Session {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(tag = "type")]
|
||||||
|
pub enum SavedWindowBounds {
|
||||||
|
Fullscreen {
|
||||||
|
x: f32,
|
||||||
|
y: f32,
|
||||||
|
width: f32,
|
||||||
|
height: f32,
|
||||||
|
},
|
||||||
|
Maximized {
|
||||||
|
x: f32,
|
||||||
|
y: f32,
|
||||||
|
width: f32,
|
||||||
|
height: f32,
|
||||||
|
},
|
||||||
|
Windowed {
|
||||||
|
x: f32,
|
||||||
|
y: f32,
|
||||||
|
width: f32,
|
||||||
|
height: f32,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||||
pub struct ConfigFile {
|
pub struct ConfigFile {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub follow_system_theme: bool,
|
pub follow_system_theme: bool,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
|
pub theme_mode: String,
|
||||||
|
#[serde(default)]
|
||||||
pub light_theme_name: String,
|
pub light_theme_name: String,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub dark_theme_name: String,
|
pub dark_theme_name: String,
|
||||||
|
#[serde(default = "default_locale")]
|
||||||
|
pub locale: String,
|
||||||
#[serde(default = "default_terminal_font_size")]
|
#[serde(default = "default_terminal_font_size")]
|
||||||
pub terminal_font_size: f32,
|
pub terminal_font_size: f32,
|
||||||
|
#[serde(default = "default_ui_font_size")]
|
||||||
|
pub ui_font_size: f32,
|
||||||
|
#[serde(default)]
|
||||||
|
pub right_click_copy_paste: bool,
|
||||||
|
#[serde(default = "default_ui_font_family")]
|
||||||
|
pub ui_font_family: String,
|
||||||
|
#[serde(default = "default_terminal_font_family")]
|
||||||
|
pub terminal_font_family: String,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub sessions: Vec<Session>,
|
pub sessions: Vec<Session>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub window_bounds: Option<SavedWindowBounds>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub workspace_panels: Option<Vec<f32>>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub body_panels: Option<Vec<f32>>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub transfers: Vec<crate::terminal::Transfer>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub show_hidden_files: bool,
|
||||||
|
#[serde(default = "default_monitoring_position")]
|
||||||
|
pub monitoring_position: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_monitoring_position() -> String {
|
||||||
|
"Sidebar".to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_locale() -> String {
|
||||||
|
"system".to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn default_terminal_font_size() -> f32 {
|
fn default_terminal_font_size() -> f32 {
|
||||||
13.0
|
13.0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn default_ui_font_size() -> f32 {
|
||||||
|
12.0
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn default_ui_font_family() -> String {
|
||||||
|
// ".SystemUIFont" is a GPUI sentinel that resolves to the platform system UI font.
|
||||||
|
// This matches gpui-component's own Theme default.
|
||||||
|
".SystemUIFont".to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_terminal_font_family() -> String {
|
||||||
|
"Maple Mono NF CN".to_string()
|
||||||
|
}
|
||||||
|
|
||||||
pub struct ConfigStore {
|
pub struct ConfigStore {
|
||||||
path: PathBuf,
|
path: PathBuf,
|
||||||
cache: ConfigFile,
|
cache: ConfigFile,
|
||||||
@@ -99,6 +168,9 @@ impl ConfigStore {
|
|||||||
if let Some(parent) = path.parent() {
|
if let Some(parent) = path.parent() {
|
||||||
fs::create_dir_all(parent)
|
fs::create_dir_all(parent)
|
||||||
.with_context(|| format!("failed to create config dir {}", parent.display()))?;
|
.with_context(|| format!("failed to create config dir {}", parent.display()))?;
|
||||||
|
let tmp_dir = parent.join("tmp");
|
||||||
|
let _ = fs::remove_dir_all(&tmp_dir);
|
||||||
|
let _ = fs::create_dir_all(&tmp_dir);
|
||||||
}
|
}
|
||||||
|
|
||||||
let cache = if path.exists() {
|
let cache = if path.exists() {
|
||||||
@@ -132,10 +204,18 @@ impl ConfigStore {
|
|||||||
&self.cache.sessions
|
&self.cache.sessions
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn tmp_dir(&self) -> Option<PathBuf> {
|
||||||
|
self.path.parent().map(|p| p.join("tmp"))
|
||||||
|
}
|
||||||
|
|
||||||
pub fn follow_system_theme(&self) -> bool {
|
pub fn follow_system_theme(&self) -> bool {
|
||||||
self.cache.follow_system_theme
|
self.cache.follow_system_theme
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn theme_mode(&self) -> &str {
|
||||||
|
&self.cache.theme_mode
|
||||||
|
}
|
||||||
|
|
||||||
pub fn light_theme_name(&self) -> &str {
|
pub fn light_theme_name(&self) -> &str {
|
||||||
&self.cache.light_theme_name
|
&self.cache.light_theme_name
|
||||||
}
|
}
|
||||||
@@ -144,6 +224,26 @@ impl ConfigStore {
|
|||||||
&self.cache.dark_theme_name
|
&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 monitoring_position(&self) -> &str {
|
||||||
|
&self.cache.monitoring_position
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_monitoring_position(&mut self, pos: &str) {
|
||||||
|
self.cache.monitoring_position = pos.to_string();
|
||||||
|
}
|
||||||
|
|
||||||
pub fn terminal_font_size(&self) -> f32 {
|
pub fn terminal_font_size(&self) -> f32 {
|
||||||
if self.cache.terminal_font_size <= 0.0 {
|
if self.cache.terminal_font_size <= 0.0 {
|
||||||
default_terminal_font_size()
|
default_terminal_font_size()
|
||||||
@@ -155,18 +255,107 @@ impl ConfigStore {
|
|||||||
pub fn set_theme_preferences(
|
pub fn set_theme_preferences(
|
||||||
&mut self,
|
&mut self,
|
||||||
follow_system_theme: bool,
|
follow_system_theme: bool,
|
||||||
|
theme_mode: impl Into<String>,
|
||||||
light_theme_name: impl Into<String>,
|
light_theme_name: impl Into<String>,
|
||||||
dark_theme_name: impl Into<String>,
|
dark_theme_name: impl Into<String>,
|
||||||
) {
|
) {
|
||||||
self.cache.follow_system_theme = follow_system_theme;
|
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.light_theme_name = light_theme_name.into();
|
||||||
self.cache.dark_theme_name = dark_theme_name.into();
|
self.cache.dark_theme_name = dark_theme_name.into();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn window_bounds(&self) -> Option<&SavedWindowBounds> {
|
||||||
|
self.cache.window_bounds.as_ref()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn workspace_panels(&self) -> Option<&Vec<f32>> {
|
||||||
|
self.cache.workspace_panels.as_ref()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub fn body_panels(&self) -> Option<&Vec<f32>> {
|
||||||
|
self.cache.body_panels.as_ref()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn transfers(&self) -> Vec<crate::terminal::Transfer> {
|
||||||
|
self.cache.transfers.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_transfers(&mut self, transfers: Vec<crate::terminal::Transfer>) {
|
||||||
|
self.cache.transfers = transfers;
|
||||||
|
if let Err(err) = self.save() {
|
||||||
|
tracing::error!("failed to save config: {err:#}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_layout_state(
|
||||||
|
&mut self,
|
||||||
|
window_bounds: Option<SavedWindowBounds>,
|
||||||
|
workspace_panels: Option<Vec<f32>>,
|
||||||
|
body_panels: Option<Vec<f32>>,
|
||||||
|
) {
|
||||||
|
self.cache.window_bounds = window_bounds;
|
||||||
|
self.cache.workspace_panels = workspace_panels;
|
||||||
|
self.cache.body_panels = body_panels;
|
||||||
|
}
|
||||||
|
|
||||||
pub fn set_terminal_font_size(&mut self, terminal_font_size: f32) {
|
pub fn set_terminal_font_size(&mut self, terminal_font_size: f32) {
|
||||||
self.cache.terminal_font_size = terminal_font_size.max(10.0);
|
self.cache.terminal_font_size = terminal_font_size.max(10.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn ui_font_size(&self) -> f32 {
|
||||||
|
if self.cache.ui_font_size <= 0.0 {
|
||||||
|
default_ui_font_size()
|
||||||
|
} else {
|
||||||
|
self.cache.ui_font_size
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_ui_font_size(&mut self, ui_font_size: f32) {
|
||||||
|
self.cache.ui_font_size = ui_font_size.max(8.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn ui_font_family(&self) -> &str {
|
||||||
|
if self.cache.ui_font_family.is_empty() {
|
||||||
|
".SystemUIFont"
|
||||||
|
} else {
|
||||||
|
&self.cache.ui_font_family
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_ui_font_family(&mut self, family: &str) {
|
||||||
|
self.cache.ui_font_family = family.to_string();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn right_click_copy_paste(&self) -> bool {
|
||||||
|
self.cache.right_click_copy_paste
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_right_click_copy_paste(&mut self, val: bool) {
|
||||||
|
self.cache.right_click_copy_paste = val;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn terminal_font_family(&self) -> &str {
|
||||||
|
if self.cache.terminal_font_family.is_empty() {
|
||||||
|
"Maple Mono NF CN"
|
||||||
|
} else {
|
||||||
|
&self.cache.terminal_font_family
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_terminal_font_family(&mut self, family: &str) {
|
||||||
|
self.cache.terminal_font_family = family.to_string();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn show_hidden_files(&self) -> bool {
|
||||||
|
self.cache.show_hidden_files
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_show_hidden_files(&mut self, val: bool) {
|
||||||
|
self.cache.show_hidden_files = val;
|
||||||
|
}
|
||||||
|
|
||||||
pub fn get(&self, id: &str) -> Option<&Session> {
|
pub fn get(&self, id: &str) -> Option<&Session> {
|
||||||
self.cache.sessions.iter().find(|s| s.id == id)
|
self.cache.sessions.iter().find(|s| s.id == id)
|
||||||
}
|
}
|
||||||
@@ -188,6 +377,7 @@ impl ConfigStore {
|
|||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
let raw = serde_json::to_string_pretty(&self.cache)?;
|
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()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1324
File diff suppressed because it is too large
Load Diff
@@ -5,7 +5,7 @@ use std::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
use anyhow::{Context, Result};
|
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};
|
use crate::terminal::{BackendCommand, BackendEvent, BackendTx};
|
||||||
|
|
||||||
@@ -34,7 +34,10 @@ pub fn spawn_local_terminal(
|
|||||||
});
|
});
|
||||||
|
|
||||||
let mut cmd = CommandBuilder::new(&shell);
|
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(
|
cmd.env(
|
||||||
"COLORTERM",
|
"COLORTERM",
|
||||||
std::env::var("COLORTERM").unwrap_or_else(|_| "truecolor".into()),
|
std::env::var("COLORTERM").unwrap_or_else(|_| "truecolor".into()),
|
||||||
|
|||||||
+127
-3155
File diff suppressed because it is too large
Load Diff
+570
@@ -0,0 +1,570 @@
|
|||||||
|
use gpui::{
|
||||||
|
App, AppContext as _, Context, Entity, KeyDownEvent, MouseButton,
|
||||||
|
MouseDownEvent, SharedString, Window, px,
|
||||||
|
};
|
||||||
|
use gpui_component::{
|
||||||
|
Theme, WindowExt as _,
|
||||||
|
input::InputState,
|
||||||
|
};
|
||||||
|
use rust_i18n::t;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
Ashell, ConnectionProgress, SelectorEntry,
|
||||||
|
config::{AuthMethod, Session},
|
||||||
|
local_terminal,
|
||||||
|
sftp,
|
||||||
|
ssh_terminal,
|
||||||
|
terminal::{BackendCommand, RenderSnapshot, TabKind, TerminalTab},
|
||||||
|
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(
|
||||||
|
id.clone(),
|
||||||
|
DEFAULT_COLS,
|
||||||
|
DEFAULT_ROWS,
|
||||||
|
self.events_tx.clone(),
|
||||||
|
) {
|
||||||
|
Ok(backend) => {
|
||||||
|
let title = if cfg!(windows) { "PowerShell" } else { "Local" }.to_string();
|
||||||
|
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);
|
||||||
|
self.tabs_scroll_handle.scroll_to_item(self.tabs.len() - 1);
|
||||||
|
self.status = "local terminal opened".into();
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
self.status = format!("failed to open local terminal: {err:#}").into();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn connect_ssh(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
let session_name = self.session_name_input.read(cx).value().trim().to_string();
|
||||||
|
let host = self.host_input.read(cx).value().trim().to_string();
|
||||||
|
let port = self
|
||||||
|
.port_input
|
||||||
|
.read(cx)
|
||||||
|
.value()
|
||||||
|
.trim()
|
||||||
|
.parse::<u16>()
|
||||||
|
.unwrap_or(22);
|
||||||
|
let user = self.user_input.read(cx).value().trim().to_string();
|
||||||
|
let password = self.password_input.read(cx).value().to_string();
|
||||||
|
let key_path = self.key_path_input.read(cx).value().trim().to_string();
|
||||||
|
let key_inline = self.key_inline_input.read(cx).value().to_string();
|
||||||
|
|
||||||
|
if host.is_empty() || user.is_empty() {
|
||||||
|
self.status = t!("host_and_user_required").into();
|
||||||
|
cx.notify();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let name = if session_name.is_empty() {
|
||||||
|
host.clone()
|
||||||
|
} else {
|
||||||
|
session_name
|
||||||
|
};
|
||||||
|
let existing_id = self.editing_session_id.clone();
|
||||||
|
let existing_last_used = existing_id
|
||||||
|
.as_deref()
|
||||||
|
.and_then(|id| self.config.get(id))
|
||||||
|
.and_then(|session| session.last_used.clone());
|
||||||
|
|
||||||
|
let mut session = match self.ssh_auth_method {
|
||||||
|
AuthMethod::Password => Session::password(host, port, user, password),
|
||||||
|
AuthMethod::Key => {
|
||||||
|
if key_path.is_empty() && key_inline.trim().is_empty() {
|
||||||
|
self.status = "private key path or content is required".into();
|
||||||
|
cx.notify();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Session::key(host, port, user, key_path, key_inline)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
session.name = name;
|
||||||
|
if let Some(id) = existing_id {
|
||||||
|
session.id = id;
|
||||||
|
}
|
||||||
|
session.last_used = existing_last_used;
|
||||||
|
self.config.upsert(session.clone());
|
||||||
|
if let Err(err) = self.config.save() {
|
||||||
|
tracing::warn!("failed to save config: {err:#}");
|
||||||
|
}
|
||||||
|
|
||||||
|
self.open_ssh_session(session, cx);
|
||||||
|
self.editing_session_id = None;
|
||||||
|
window.close_dialog(cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn set_input_value(
|
||||||
|
input: &Entity<InputState>,
|
||||||
|
value: impl Into<SharedString>,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
input.update(cx, |state, cx| state.set_value(value, window, cx));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn reset_ssh_form(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
self.editing_session_id = None;
|
||||||
|
self.ssh_auth_method = AuthMethod::Password;
|
||||||
|
Self::set_input_value(&self.session_name_input, "", window, cx);
|
||||||
|
Self::set_input_value(&self.host_input, "", window, cx);
|
||||||
|
Self::set_input_value(&self.port_input, "22", window, cx);
|
||||||
|
Self::set_input_value(&self.user_input, "root", window, cx);
|
||||||
|
Self::set_input_value(&self.password_input, "", window, cx);
|
||||||
|
Self::set_input_value(&self.key_path_input, "", window, cx);
|
||||||
|
Self::set_input_value(&self.key_inline_input, "", window, cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn load_session_into_form(
|
||||||
|
&mut self,
|
||||||
|
session: &Session,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
self.editing_session_id = Some(session.id.clone());
|
||||||
|
self.ssh_auth_method = session.auth;
|
||||||
|
Self::set_input_value(&self.session_name_input, session.name.clone(), window, cx);
|
||||||
|
Self::set_input_value(&self.host_input, session.host.clone(), window, cx);
|
||||||
|
Self::set_input_value(&self.port_input, session.port.to_string(), window, cx);
|
||||||
|
Self::set_input_value(&self.user_input, session.user.clone(), window, cx);
|
||||||
|
Self::set_input_value(&self.password_input, session.password.clone(), window, cx);
|
||||||
|
Self::set_input_value(
|
||||||
|
&self.key_path_input,
|
||||||
|
session.private_key_path.clone(),
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
Self::set_input_value(
|
||||||
|
&self.key_inline_input,
|
||||||
|
session.private_key_inline.clone(),
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn open_new_ssh_dialog(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
self.reset_ssh_form(window, cx);
|
||||||
|
self.show_ssh_dialog(window, cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn edit_saved_session(
|
||||||
|
&mut self,
|
||||||
|
session_id: String,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
let Some(session) = self.config.get(&session_id).cloned() else {
|
||||||
|
self.status = "saved session not found".into();
|
||||||
|
cx.notify();
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
self.load_session_into_form(&session, window, cx);
|
||||||
|
self.show_ssh_dialog(window, cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn terminal_cell_width(&self) -> f32 {
|
||||||
|
(self.terminal_font_size * 0.646).max(6.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn terminal_line_height(&self) -> f32 {
|
||||||
|
(self.terminal_font_size * 1.385).max(self.terminal_font_size + 2.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn change_terminal_font_size(&mut self, delta: f32, cx: &mut Context<Self>) {
|
||||||
|
self.terminal_font_size = (self.terminal_font_size + delta).clamp(10.0, 24.0);
|
||||||
|
self.config.set_terminal_font_size(self.terminal_font_size);
|
||||||
|
if let Err(err) = self.config.save() {
|
||||||
|
tracing::warn!("failed to save terminal font size: {err:#}");
|
||||||
|
}
|
||||||
|
self.status = format!("terminal font size: {:.0}px", self.terminal_font_size).into();
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn change_ui_font_size(&mut self, delta: f32, cx: &mut Context<Self>) {
|
||||||
|
self.ui_font_size = (self.ui_font_size + delta).clamp(8.0, 24.0);
|
||||||
|
self.config.set_ui_font_size(self.ui_font_size);
|
||||||
|
if let Err(err) = self.config.save() {
|
||||||
|
tracing::warn!("failed to save UI font size: {err:#}");
|
||||||
|
}
|
||||||
|
Theme::global_mut(cx).font_size = px(self.ui_font_size);
|
||||||
|
self.status = format!("UI font size: {:.0}px", self.ui_font_size).into();
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn change_ui_font_family(
|
||||||
|
&mut self,
|
||||||
|
family: &str,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
self.ui_font_family = family.into();
|
||||||
|
self.config.set_ui_font_family(family);
|
||||||
|
if let Err(err) = self.config.save() {
|
||||||
|
tracing::warn!("failed to save UI font family: {err:#}");
|
||||||
|
}
|
||||||
|
crate::theme::set_theme_font_names(Theme::global_mut(cx), &self.ui_font_family);
|
||||||
|
cx.notify();
|
||||||
|
window.refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn change_terminal_font_family(&mut self, family: &str, cx: &mut Context<Self>) {
|
||||||
|
self.terminal_font_family = family.into();
|
||||||
|
self.config.set_terminal_font_family(family);
|
||||||
|
if let Err(err) = self.config.save() {
|
||||||
|
tracing::warn!("failed to save terminal font family: {err:#}");
|
||||||
|
}
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn reset_layout(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
self.config.set_layout_state(None, None, None);
|
||||||
|
let _ = self.config.save();
|
||||||
|
|
||||||
|
self.workspace_panels = cx.new(|_| gpui_component::resizable::ResizableState::default());
|
||||||
|
self.body_panels = cx.new(|_| gpui_component::resizable::ResizableState::default());
|
||||||
|
|
||||||
|
self.status = t!("reset_layout_success").into();
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn set_ssh_auth_method(&mut self, method: AuthMethod, cx: &mut Context<Self>) {
|
||||||
|
self.ssh_auth_method = method;
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn connect_saved_session(&mut self, session_id: String, cx: &mut Context<Self>) {
|
||||||
|
let Some(session) = self.config.get(&session_id).cloned() else {
|
||||||
|
self.status = "saved session not found".into();
|
||||||
|
cx.notify();
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
self.open_ssh_session(session, cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn selector_entries(&self) -> Vec<SelectorEntry> {
|
||||||
|
let mut entries = vec![SelectorEntry::Local, SelectorEntry::NewSsh];
|
||||||
|
entries.extend(
|
||||||
|
self.config
|
||||||
|
.sessions()
|
||||||
|
.iter()
|
||||||
|
.map(|session| SelectorEntry::Saved(session.id.clone())),
|
||||||
|
);
|
||||||
|
entries
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn default_selector_index(&self) -> usize {
|
||||||
|
if self.config.sessions().is_empty() {
|
||||||
|
0
|
||||||
|
} else {
|
||||||
|
2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn move_selector_selection(&mut self, delta: i32, cx: &mut Context<Self>) {
|
||||||
|
let entries = self.selector_entries();
|
||||||
|
if entries.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let current = self.selector_selection.min(entries.len().saturating_sub(1)) as i32;
|
||||||
|
let next = (current + delta).clamp(0, entries.len() as i32 - 1) as usize;
|
||||||
|
if next != self.selector_selection {
|
||||||
|
self.selector_selection = next;
|
||||||
|
if next >= 2 {
|
||||||
|
self.selector_scroll_handle.scroll_to_item(next - 2);
|
||||||
|
}
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn activate_selector_selection(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
let entries = self.selector_entries();
|
||||||
|
let Some(entry) = entries.get(self.selector_selection).cloned() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
match entry {
|
||||||
|
SelectorEntry::Local => {
|
||||||
|
self.open_local(cx);
|
||||||
|
window.close_dialog(cx);
|
||||||
|
}
|
||||||
|
SelectorEntry::NewSsh => {
|
||||||
|
window.close_dialog(cx);
|
||||||
|
self.open_new_ssh_dialog(window, cx);
|
||||||
|
}
|
||||||
|
SelectorEntry::Saved(session_id) => {
|
||||||
|
self.connect_saved_session(session_id, cx);
|
||||||
|
window.close_dialog(cx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn on_selector_key_down(
|
||||||
|
&mut self,
|
||||||
|
event: &KeyDownEvent,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
let key = event.keystroke.key.to_ascii_lowercase();
|
||||||
|
match key.as_str() {
|
||||||
|
"up" | "arrowup" => {
|
||||||
|
self.move_selector_selection(-1, cx);
|
||||||
|
window.prevent_default();
|
||||||
|
cx.stop_propagation();
|
||||||
|
}
|
||||||
|
"down" | "arrowdown" => {
|
||||||
|
self.move_selector_selection(1, cx);
|
||||||
|
window.prevent_default();
|
||||||
|
cx.stop_propagation();
|
||||||
|
}
|
||||||
|
"enter" | "return" => {
|
||||||
|
self.activate_selector_selection(window, cx);
|
||||||
|
window.prevent_default();
|
||||||
|
cx.stop_propagation();
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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(
|
||||||
|
self.runtime.handle(),
|
||||||
|
id.clone(),
|
||||||
|
session.clone(),
|
||||||
|
DEFAULT_COLS,
|
||||||
|
DEFAULT_ROWS,
|
||||||
|
self.events_tx.clone(),
|
||||||
|
);
|
||||||
|
self.tabs.push(TerminalTab::new_ssh(
|
||||||
|
id.clone(),
|
||||||
|
&session,
|
||||||
|
backend,
|
||||||
|
self.events_tx.clone(),
|
||||||
|
));
|
||||||
|
self.active_tab = Some(id.clone());
|
||||||
|
self.tabs_scroll_handle.scroll_to_item(self.tabs.len() - 1);
|
||||||
|
if let Some(session_id) = self.active_session_id() {
|
||||||
|
if let Some(index) = self.config.sessions().iter().position(|s| s.id == session_id) {
|
||||||
|
self.saved_scroll_handle.scroll_to_item(index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cx.notify();
|
||||||
|
let sftp_handle = sftp::spawn_sftp(
|
||||||
|
self.runtime.handle(),
|
||||||
|
id.clone(),
|
||||||
|
session,
|
||||||
|
self.events_tx.clone(),
|
||||||
|
);
|
||||||
|
self.sftp_handles.insert(id.clone(), sftp_handle);
|
||||||
|
self.active_tab = Some(id.clone());
|
||||||
|
self.pending_sftp_path_sync = Some("/".into());
|
||||||
|
self.connection_progress = Some(ConnectionProgress {
|
||||||
|
tab_id: id,
|
||||||
|
title: t!("connecting").into(),
|
||||||
|
lines: vec![t!("starting_connection").into()],
|
||||||
|
failed: false,
|
||||||
|
});
|
||||||
|
self.status = "ssh tab opened".into();
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn remove_saved_session(&mut self, session_id: String, cx: &mut Context<Self>) {
|
||||||
|
self.config.remove(&session_id);
|
||||||
|
if let Err(err) = self.config.save() {
|
||||||
|
tracing::warn!("failed to save config: {err:#}");
|
||||||
|
}
|
||||||
|
self.status = "session removed".into();
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn retry_connection_progress(&mut self, cx: &mut Context<Self>) {
|
||||||
|
let Some(progress) = self.connection_progress.clone() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Some(ix) = self.tabs.iter().position(|tab| tab.id == progress.tab_id) else {
|
||||||
|
self.connection_progress = None;
|
||||||
|
cx.notify();
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Some(session) = self.tabs[ix].session.clone() else {
|
||||||
|
self.connection_progress = None;
|
||||||
|
cx.notify();
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
self.tabs[ix].backend.send(BackendCommand::Close);
|
||||||
|
if let Some(handle) = self.sftp_handles.remove(&progress.tab_id) {
|
||||||
|
handle.close();
|
||||||
|
}
|
||||||
|
self.tabs.remove(ix);
|
||||||
|
if self.active_tab.as_deref() == Some(progress.tab_id.as_str()) {
|
||||||
|
self.active_tab = self
|
||||||
|
.tabs
|
||||||
|
.get(ix)
|
||||||
|
.or_else(|| self.tabs.get(ix.saturating_sub(1)))
|
||||||
|
.map(|tab| tab.id.clone());
|
||||||
|
}
|
||||||
|
self.connection_progress = None;
|
||||||
|
self.open_ssh_session(session, cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn cancel_connection_progress(&mut self, cx: &mut Context<Self>) {
|
||||||
|
let Some(progress) = self.connection_progress.clone() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
self.connection_progress = None;
|
||||||
|
self.close_tab(progress.tab_id, cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn activate_tab(&mut self, id: String, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
if let Some(path) = self
|
||||||
|
.tabs
|
||||||
|
.iter()
|
||||||
|
.find(|tab| tab.id == id)
|
||||||
|
.and_then(|tab| tab.sftp.as_ref())
|
||||||
|
.map(|sftp| sftp.current_path.clone())
|
||||||
|
{
|
||||||
|
self.pending_sftp_path_sync = Some(path);
|
||||||
|
}
|
||||||
|
self.active_tab = Some(id.clone());
|
||||||
|
self.cpu_history.clear();
|
||||||
|
self.net_rx_history.clear();
|
||||||
|
self.net_tx_history.clear();
|
||||||
|
if let Some(index) = self.tabs.iter().position(|t| t.id == id) {
|
||||||
|
self.tabs_scroll_handle.scroll_to_item(index);
|
||||||
|
}
|
||||||
|
if self.tabs.iter().any(|t| t.id == id) {
|
||||||
|
if let Some(session_id) = self.active_session_id() {
|
||||||
|
if let Some(index) = self.config.sessions().iter().position(|s| s.id == session_id) {
|
||||||
|
self.saved_scroll_handle.scroll_to_item(index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.remote_sample_in_flight = false;
|
||||||
|
self.request_active_system_snapshot();
|
||||||
|
self.focus_handle.focus(window, cx);
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn close_tab(&mut self, id: String, cx: &mut Context<Self>) {
|
||||||
|
if let Some(ix) = self.tabs.iter().position(|tab| tab.id == id) {
|
||||||
|
let was_active = self.active_tab.as_deref() == Some(id.as_str());
|
||||||
|
self.tabs[ix].backend.send(BackendCommand::Close);
|
||||||
|
if let Some(handle) = self.sftp_handles.remove(&id) {
|
||||||
|
handle.close();
|
||||||
|
}
|
||||||
|
self.tabs.remove(ix);
|
||||||
|
if was_active
|
||||||
|
|| self
|
||||||
|
.active_tab
|
||||||
|
.as_ref()
|
||||||
|
.is_some_and(|active_id| !self.tabs.iter().any(|tab| &tab.id == active_id))
|
||||||
|
{
|
||||||
|
self.active_tab = self
|
||||||
|
.tabs
|
||||||
|
.get(ix)
|
||||||
|
.or_else(|| self.tabs.get(ix.saturating_sub(1)))
|
||||||
|
.map(|tab| tab.id.clone());
|
||||||
|
self.cpu_history.clear();
|
||||||
|
self.net_rx_history.clear();
|
||||||
|
self.net_tx_history.clear();
|
||||||
|
self.remote_sample_in_flight = false;
|
||||||
|
self.request_active_system_snapshot();
|
||||||
|
}
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn focus_terminal(
|
||||||
|
&mut self,
|
||||||
|
event: &MouseDownEvent,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
self.focus_handle.focus(window, cx);
|
||||||
|
if event.button == MouseButton::Left {
|
||||||
|
self.begin_terminal_selection(event, cx);
|
||||||
|
}
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn active_snapshot(&self) -> Option<RenderSnapshot> {
|
||||||
|
self.active_tab
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|id| self.tabs.iter().find(|t| &t.id == id))
|
||||||
|
.map(TerminalTab::render_snapshot)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn active_kind(&self) -> Option<TabKind> {
|
||||||
|
self.active_tab
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|id| self.tabs.iter().find(|t| &t.id == id))
|
||||||
|
.map(|tab| tab.kind)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn active_title(&self) -> String {
|
||||||
|
self.active_tab
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|id| self.tabs.iter().find(|t| &t.id == id))
|
||||||
|
.map(|t| t.title.clone())
|
||||||
|
.unwrap_or_else(|| t!("idle_no_session").into())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn active_ssh_session(&self) -> Option<(String, Session)> {
|
||||||
|
let active_id = self.active_tab.as_ref()?;
|
||||||
|
let tab = self.tabs.iter().find(|tab| &tab.id == active_id)?;
|
||||||
|
if !tab.connected {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
Some((tab.id.clone(), tab.session.clone()?))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn active_session_id(&self) -> Option<&str> {
|
||||||
|
self.active_tab
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|id| self.tabs.iter().find(|tab| &tab.id == id))
|
||||||
|
.and_then(|tab| tab.session.as_ref())
|
||||||
|
.map(|session| session.id.as_str())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn session_detail(&self, session: &Session) -> String {
|
||||||
|
format!("{}@{}:{}", session.user, session.host, session.port)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn sync_terminal_size(&mut self, window: &Window, cx: &App) {
|
||||||
|
let viewport = window.viewport_size();
|
||||||
|
let sidebar_width = self
|
||||||
|
.workspace_panels
|
||||||
|
.read(cx)
|
||||||
|
.sizes()
|
||||||
|
.first()
|
||||||
|
.map(|size| size.as_f32())
|
||||||
|
.unwrap_or(SIDEBAR_WIDTH);
|
||||||
|
let terminal_height = self
|
||||||
|
.body_panels
|
||||||
|
.read(cx)
|
||||||
|
.sizes()
|
||||||
|
.first()
|
||||||
|
.map(|size| size.as_f32())
|
||||||
|
.unwrap_or(viewport.height.as_f32() - TAB_BAR_HEIGHT - 248.0);
|
||||||
|
let width = (viewport.width.as_f32() - sidebar_width - TERMINAL_PADDING_X - 8.0)
|
||||||
|
.max(self.terminal_cell_width());
|
||||||
|
let height = (terminal_height - TERMINAL_PADDING_Y).max(self.terminal_line_height());
|
||||||
|
let cols = (width / self.terminal_cell_width()).floor().max(1.0) as u16;
|
||||||
|
let rows = (height / self.terminal_line_height()).floor().max(1.0) as u16;
|
||||||
|
|
||||||
|
for tab in &mut self.tabs {
|
||||||
|
tab.resize(cols, rows);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+885
-123
File diff suppressed because it is too large
Load Diff
+333
@@ -0,0 +1,333 @@
|
|||||||
|
use gpui::{Context, PathPromptOptions, Pixels, Point, Window};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
Ashell, SftpContextMenuState,
|
||||||
|
sftp::{RemoteEntry, SftpHandle},
|
||||||
|
terminal,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub(crate) fn is_editable_text_file(filename: &str) -> bool {
|
||||||
|
let lower = filename.to_lowercase();
|
||||||
|
let ext = std::path::Path::new(&lower).extension().and_then(|s| s.to_str()).unwrap_or("");
|
||||||
|
let known_exts = ["txt", "conf", "json", "yaml", "yml", "xml", "ini", "sh", "py", "rs", "js", "ts", "html", "css", "md", "toml", "csv", "log", "cfg"];
|
||||||
|
if known_exts.contains(&ext) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
let known_names = ["dockerfile", "makefile", ".gitignore", ".env"];
|
||||||
|
if known_names.contains(&lower.as_str()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Ashell {
|
||||||
|
pub(crate) fn active_sftp(&self) -> Option<&terminal::SftpUiState> {
|
||||||
|
self.active_tab
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|id| self.tabs.iter().find(|tab| &tab.id == id))
|
||||||
|
.and_then(|tab| tab.sftp.as_ref())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn active_sftp_mut(&mut self) -> Option<&mut terminal::SftpUiState> {
|
||||||
|
let active_id = self.active_tab.clone()?;
|
||||||
|
self.tabs
|
||||||
|
.iter_mut()
|
||||||
|
.find(|tab| tab.id == active_id)
|
||||||
|
.and_then(|tab| tab.sftp.as_mut())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn active_sftp_handle(&self) -> Option<&SftpHandle> {
|
||||||
|
self.active_tab
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|id| self.sftp_handles.get(id))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn navigate_sftp(&mut self, path: String, cx: &mut Context<Self>) {
|
||||||
|
if let Some(handle) = self.active_sftp_handle() {
|
||||||
|
handle.list_dir(path.clone());
|
||||||
|
if let Some(sftp) = self.active_sftp_mut() {
|
||||||
|
sftp.current_path = path;
|
||||||
|
self.pending_sftp_path_sync = Some(sftp.current_path.clone());
|
||||||
|
}
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn select_sftp_entry(&mut self, entry: RemoteEntry, cx: &mut Context<Self>) {
|
||||||
|
if entry.is_dir {
|
||||||
|
self.navigate_sftp(entry.full_path, cx);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self.mark_sftp_entry_selected(&entry.full_path, cx);
|
||||||
|
if let Some(sftp) = self.active_sftp_mut() {
|
||||||
|
if !sftp.selected_entries.remove(&entry.full_path) {
|
||||||
|
sftp.selected_entries.insert(entry.full_path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn mark_sftp_entry_selected(&mut self, path: &str, cx: &mut Context<Self>) {
|
||||||
|
if let Some(sftp) = self.active_sftp_mut() {
|
||||||
|
sftp.selected_path = Some(path.to_string());
|
||||||
|
}
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn sftp_parent_path(path: &str) -> String {
|
||||||
|
if path == "/" {
|
||||||
|
return "/".to_string();
|
||||||
|
}
|
||||||
|
path.trim_end_matches('/')
|
||||||
|
.rsplit_once('/')
|
||||||
|
.map(|(parent, _)| {
|
||||||
|
if parent.is_empty() {
|
||||||
|
"/".to_string()
|
||||||
|
} else {
|
||||||
|
parent.to_string()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.unwrap_or_else(|| "/".to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn refresh_sftp(&mut self, cx: &mut Context<Self>) {
|
||||||
|
if let Some(path) = self.active_sftp().map(|sftp| sftp.current_path.clone()) {
|
||||||
|
self.navigate_sftp(path, cx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn sync_sftp_path_input(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
let Some(path) = self.pending_sftp_path_sync.take() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
self.sftp_path_input.update(cx, |state, cx| {
|
||||||
|
state.set_value(path, window, cx);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn open_sftp_context_menu(
|
||||||
|
&mut self,
|
||||||
|
remote_path: String,
|
||||||
|
is_dir: bool,
|
||||||
|
position: Point<Pixels>,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
self.sftp_context_menu = Some(SftpContextMenuState {
|
||||||
|
remote_path,
|
||||||
|
is_dir,
|
||||||
|
position,
|
||||||
|
});
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn dismiss_sftp_context_menu(&mut self, cx: &mut Context<Self>) {
|
||||||
|
if self.sftp_context_menu.take().is_some() {
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn trigger_sftp_context_download(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
let Some(menu) = self.sftp_context_menu.take() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
self.download_sftp_entry(menu.remote_path, window, cx);
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn trigger_sftp_context_edit(&mut self, cx: &mut Context<Self>) {
|
||||||
|
let Some(menu) = self.sftp_context_menu.take() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
if let Some(id) = self.active_tab.clone() {
|
||||||
|
if let Some(handle) = self.sftp_handles.get(&id) {
|
||||||
|
handle.edit_file(menu.remote_path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn download_sftp_entry(
|
||||||
|
&mut self,
|
||||||
|
remote_path: String,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
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| {
|
||||||
|
match path_prompt.await {
|
||||||
|
Ok(Ok(Some(mut paths))) => {
|
||||||
|
if let Some(folder) = paths.pop() {
|
||||||
|
handle.download(remote_path, folder.to_string_lossy().to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(Err(err)) => {
|
||||||
|
this.update(cx, |this, cx| {
|
||||||
|
this.status = format!("download picker failed: {err}").into();
|
||||||
|
cx.notify();
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
Ok::<(), anyhow::Error>(())
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn upload_sftp_files(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
let Some(handle) = self.active_sftp_handle().cloned() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let remote_dir = self
|
||||||
|
.active_sftp()
|
||||||
|
.map(|sftp| sftp.current_path.clone())
|
||||||
|
.unwrap_or_else(|| "/".into());
|
||||||
|
let path_prompt = cx.prompt_for_paths(PathPromptOptions {
|
||||||
|
files: true,
|
||||||
|
directories: false,
|
||||||
|
multiple: false,
|
||||||
|
prompt: Some("Select File to Upload".into()),
|
||||||
|
});
|
||||||
|
cx.spawn_in(window, async move |this, cx| {
|
||||||
|
match path_prompt.await {
|
||||||
|
Ok(Ok(Some(mut paths))) => {
|
||||||
|
if let Some(file) = paths.pop() {
|
||||||
|
handle.upload_paths(vec![file.to_string_lossy().to_string()], remote_dir);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(Err(err)) => {
|
||||||
|
this.update(cx, |this, cx| {
|
||||||
|
this.status = format!("upload picker failed: {err}").into();
|
||||||
|
cx.notify();
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
Ok::<(), anyhow::Error>(())
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn upload_sftp_folder(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
let Some(handle) = self.active_sftp_handle().cloned() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let remote_dir = self
|
||||||
|
.active_sftp()
|
||||||
|
.map(|sftp| sftp.current_path.clone())
|
||||||
|
.unwrap_or_else(|| "/".into());
|
||||||
|
let path_prompt = cx.prompt_for_paths(PathPromptOptions {
|
||||||
|
files: false,
|
||||||
|
directories: true,
|
||||||
|
multiple: false,
|
||||||
|
prompt: Some("Select Folder to Upload".into()),
|
||||||
|
});
|
||||||
|
cx.spawn_in(window, async move |this, cx| {
|
||||||
|
match path_prompt.await {
|
||||||
|
Ok(Ok(Some(mut paths))) => {
|
||||||
|
if let Some(folder) = paths.pop() {
|
||||||
|
handle.upload_paths(vec![folder.to_string_lossy().to_string()], remote_dir);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(Err(err)) => {
|
||||||
|
this.update(cx, |this, cx| {
|
||||||
|
this.status = format!("upload picker failed: {err}").into();
|
||||||
|
cx.notify();
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
Ok::<(), anyhow::Error>(())
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) 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();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) 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(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+6
-6
@@ -3,19 +3,19 @@ use std::{
|
|||||||
sync::Arc,
|
sync::Arc,
|
||||||
};
|
};
|
||||||
|
|
||||||
use anyhow::{anyhow, Context, Result};
|
use anyhow::{Context, Result, anyhow};
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use directories::BaseDirs;
|
use directories::BaseDirs;
|
||||||
use russh::{
|
use russh::{
|
||||||
client::{self, Handler},
|
|
||||||
keys::{decode_secret_key, key::PrivateKeyWithHashAlg, load_secret_key, HashAlg, PrivateKey},
|
|
||||||
ChannelMsg, Disconnect,
|
ChannelMsg, Disconnect,
|
||||||
|
client::{self, Handler},
|
||||||
|
keys::{HashAlg, PrivateKey, decode_secret_key, key::PrivateKeyWithHashAlg, load_secret_key},
|
||||||
};
|
};
|
||||||
use tokio::sync::mpsc;
|
use tokio::sync::mpsc;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
config::{AuthMethod, Session},
|
config::{AuthMethod, Session},
|
||||||
system::{remote_snapshot_from_kv, SystemSnapshot},
|
system::{SystemSnapshot, remote_snapshot_from_kv},
|
||||||
terminal::{BackendCommand, BackendEvent, BackendTx},
|
terminal::{BackendCommand, BackendEvent, BackendTx},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -365,7 +365,7 @@ EOF
|
|||||||
echo "SWAP_USED=$(( ${swap_total:-0} - ${swap_free:-0} ))"
|
echo "SWAP_USED=$(( ${swap_total:-0} - ${swap_free:-0} ))"
|
||||||
echo "NET_RX=$(( ${net_rx_2:-0} - ${net_rx_1:-0} ))"
|
echo "NET_RX=$(( ${net_rx_2:-0} - ${net_rx_1:-0} ))"
|
||||||
echo "NET_TX=$(( ${net_tx_2:-0} - ${net_tx_1:-0} ))"
|
echo "NET_TX=$(( ${net_tx_2:-0} - ${net_tx_1:-0} ))"
|
||||||
df -kP 2>/dev/null | awk "NR > 1 { printf \"DISK=%s\t%s\t%s\n\", \$6, \$4 * 1024, \$2 * 1024 }" | head -n 4
|
df -kP 2>/dev/null | awk "NR > 1 && \$1 !~ /^(tmpfs|devtmpfs|ramfs|overlay|aufs)\$/ { printf \"DISK=%s\t%s\t%s\n\", \$6, \$4 * 1024, \$2 * 1024 }" | head -n 6
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -404,7 +404,7 @@ EOF
|
|||||||
echo "SWAP_USED=${swap_used:-0}"
|
echo "SWAP_USED=${swap_used:-0}"
|
||||||
echo "NET_RX=$(( ${net_rx_2:-0} - ${net_rx_1:-0} ))"
|
echo "NET_RX=$(( ${net_rx_2:-0} - ${net_rx_1:-0} ))"
|
||||||
echo "NET_TX=$(( ${net_tx_2:-0} - ${net_tx_1:-0} ))"
|
echo "NET_TX=$(( ${net_tx_2:-0} - ${net_tx_1:-0} ))"
|
||||||
df -kP 2>/dev/null | awk "NR > 1 { printf \"DISK=%s\t%s\t%s\n\", \$6, \$4 * 1024, \$2 * 1024 }" | head -n 4
|
df -kP 2>/dev/null | awk "NR > 1 && \$1 !~ /^(devfs|tmpfs|devtmpfs|ramfs|overlay|aufs)\$/ { printf \"DISK=%s\t%s\t%s\n\", \$6, \$4 * 1024, \$2 * 1024 }" | head -n 6
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|||||||
+54
-16
@@ -1,8 +1,20 @@
|
|||||||
use std::{collections::BTreeMap, time::{Duration, Instant}};
|
use std::{
|
||||||
|
collections::BTreeMap,
|
||||||
|
ffi::OsStr,
|
||||||
|
time::{Duration, Instant},
|
||||||
|
};
|
||||||
|
|
||||||
use anyhow::{anyhow, Result};
|
use anyhow::{Result, anyhow};
|
||||||
use sysinfo::{Disks, Networks, System};
|
use sysinfo::{Disks, Networks, System};
|
||||||
|
|
||||||
|
/// Known virtual/ram filesystems to exclude from disk monitoring.
|
||||||
|
fn is_real_filesystem(fs: &OsStr) -> bool {
|
||||||
|
!matches!(
|
||||||
|
fs.to_str(),
|
||||||
|
Some("tmpfs" | "devtmpfs" | "ramfs" | "overlay" | "aufs")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Default)]
|
#[derive(Debug, Clone, Default)]
|
||||||
pub struct DiskSample {
|
pub struct DiskSample {
|
||||||
pub mount: String,
|
pub mount: String,
|
||||||
@@ -19,7 +31,10 @@ pub struct SystemSnapshot {
|
|||||||
pub swap_detail: String,
|
pub swap_detail: String,
|
||||||
pub net_rx: String,
|
pub net_rx: String,
|
||||||
pub net_tx: String,
|
pub net_tx: String,
|
||||||
|
pub net_rx_rate: u64,
|
||||||
|
pub net_tx_rate: u64,
|
||||||
pub disks: Vec<DiskSample>,
|
pub disks: Vec<DiskSample>,
|
||||||
|
pub total_swap: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct SystemSampler {
|
pub struct SystemSampler {
|
||||||
@@ -69,42 +84,48 @@ impl SystemSampler {
|
|||||||
let rx_total: u64 = self.nets.iter().map(|(_, d)| d.total_received()).sum();
|
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 tx_total: u64 = self.nets.iter().map(|(_, d)| d.total_transmitted()).sum();
|
||||||
let now = Instant::now();
|
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 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;
|
let tx_rate = (tx_total.saturating_sub(self.last_tx_total) as f64 / elapsed) as u64;
|
||||||
self.last_rx_total = rx_total;
|
self.last_rx_total = rx_total;
|
||||||
self.last_tx_total = tx_total;
|
self.last_tx_total = tx_total;
|
||||||
self.last_instant = now;
|
self.last_instant = now;
|
||||||
|
|
||||||
let disks = self
|
let mut disks: Vec<DiskSample> = self
|
||||||
.disks
|
.disks
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|disk| disk.total_space() > 0)
|
.filter(|disk| disk.total_space() > 0 && is_real_filesystem(disk.file_system()))
|
||||||
.take(4)
|
|
||||||
.map(|disk| DiskSample {
|
.map(|disk| DiskSample {
|
||||||
mount: disk.mount_point().to_string_lossy().to_string(),
|
mount: disk.mount_point().to_string_lossy().to_string(),
|
||||||
available_bytes: disk.available_space(),
|
available_bytes: disk.available_space(),
|
||||||
total_bytes: disk.total_space(),
|
total_bytes: disk.total_space(),
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
disks.sort_by(|a, b| {
|
||||||
|
if a.mount == "/" {
|
||||||
|
return std::cmp::Ordering::Less;
|
||||||
|
}
|
||||||
|
if b.mount == "/" {
|
||||||
|
return std::cmp::Ordering::Greater;
|
||||||
|
}
|
||||||
|
a.mount.cmp(&b.mount)
|
||||||
|
});
|
||||||
|
|
||||||
SystemSnapshot {
|
SystemSnapshot {
|
||||||
cpu_percent,
|
cpu_percent,
|
||||||
mem_percent: ratio(mem_used, mem_total),
|
mem_percent: ratio(mem_used, mem_total),
|
||||||
swap_percent: ratio(swap_used, swap_total),
|
swap_percent: ratio(swap_used, swap_total),
|
||||||
mem_detail: format!(
|
mem_detail: format!("{}/{}", format_bytes(mem_used), format_bytes(mem_total)),
|
||||||
"{}/{}",
|
swap_detail: format!("{}/{}", format_bytes(swap_used), format_bytes(swap_total)),
|
||||||
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_rx: format!("{}/s", format_bytes(rx_rate)),
|
||||||
net_tx: format!("{}/s", format_bytes(tx_rate)),
|
net_tx: format!("{}/s", format_bytes(tx_rate)),
|
||||||
|
net_rx_rate: rx_rate,
|
||||||
|
net_tx_rate: tx_rate,
|
||||||
disks,
|
disks,
|
||||||
|
total_swap: swap_total,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -178,6 +199,20 @@ pub fn remote_snapshot_from_kv(raw: &str) -> Result<SystemSnapshot> {
|
|||||||
let rx_rate = parse_u64(&kv, "NET_RX");
|
let rx_rate = parse_u64(&kv, "NET_RX");
|
||||||
let tx_rate = parse_u64(&kv, "NET_TX");
|
let tx_rate = parse_u64(&kv, "NET_TX");
|
||||||
|
|
||||||
|
// Safety filter: exclude entries with zero/negligible total size
|
||||||
|
// (catches any virtual fs lines that slipped past the script filter)
|
||||||
|
disks.retain(|d| d.total_bytes >= 1024 * 1024);
|
||||||
|
|
||||||
|
disks.sort_by(|a, b| {
|
||||||
|
if a.mount == "/" {
|
||||||
|
return std::cmp::Ordering::Less;
|
||||||
|
}
|
||||||
|
if b.mount == "/" {
|
||||||
|
return std::cmp::Ordering::Greater;
|
||||||
|
}
|
||||||
|
a.mount.cmp(&b.mount)
|
||||||
|
});
|
||||||
|
|
||||||
Ok(SystemSnapshot {
|
Ok(SystemSnapshot {
|
||||||
cpu_percent: cpu_percent.clamp(0.0, 1.0),
|
cpu_percent: cpu_percent.clamp(0.0, 1.0),
|
||||||
mem_percent: ratio(mem_used, mem_total),
|
mem_percent: ratio(mem_used, mem_total),
|
||||||
@@ -186,7 +221,10 @@ pub fn remote_snapshot_from_kv(raw: &str) -> Result<SystemSnapshot> {
|
|||||||
swap_detail: format!("{}/{}", format_bytes(swap_used), format_bytes(swap_total)),
|
swap_detail: format!("{}/{}", format_bytes(swap_used), format_bytes(swap_total)),
|
||||||
net_rx: format!("{}/s", format_bytes(rx_rate)),
|
net_rx: format!("{}/s", format_bytes(rx_rate)),
|
||||||
net_tx: format!("{}/s", format_bytes(tx_rate)),
|
net_tx: format!("{}/s", format_bytes(tx_rate)),
|
||||||
|
net_rx_rate: rx_rate,
|
||||||
|
net_tx_rate: tx_rate,
|
||||||
disks,
|
disks,
|
||||||
|
total_swap: swap_total,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+177
-30
@@ -5,10 +5,7 @@ use alacritty_terminal::{
|
|||||||
grid::{Dimensions, Scroll},
|
grid::{Dimensions, Scroll},
|
||||||
index::{Column, Point, Side},
|
index::{Column, Point, Side},
|
||||||
selection::{Selection, SelectionRange, SelectionType},
|
selection::{Selection, SelectionRange, SelectionType},
|
||||||
term::{
|
term::{Config, Term, TermMode, cell::Cell, point_to_viewport, viewport_to_point},
|
||||||
cell::Cell,
|
|
||||||
point_to_viewport, viewport_to_point, Config, Term, TermMode,
|
|
||||||
},
|
|
||||||
vte::ansi::{CursorShape, Processor},
|
vte::ansi::{CursorShape, Processor},
|
||||||
};
|
};
|
||||||
use gpui::Keystroke;
|
use gpui::Keystroke;
|
||||||
@@ -32,15 +29,62 @@ pub enum BackendCommand {
|
|||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub enum BackendEvent {
|
pub enum BackendEvent {
|
||||||
Output { tab_id: String, bytes: Vec<u8> },
|
Output {
|
||||||
Status { tab_id: String, text: String },
|
tab_id: String,
|
||||||
Connected { tab_id: String },
|
bytes: Vec<u8>,
|
||||||
SftpEntries { tab_id: String, path: String, entries: Vec<RemoteEntry> },
|
},
|
||||||
SftpPreview { tab_id: String, preview: PreviewData },
|
Status {
|
||||||
SftpStatus { tab_id: String, text: String },
|
tab_id: String,
|
||||||
RemoteSystem { tab_id: String, snapshot: SystemSnapshot },
|
text: String,
|
||||||
RemoteSystemUnavailable { tab_id: String, reason: String },
|
},
|
||||||
Closed { tab_id: String, reason: 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,
|
||||||
|
},
|
||||||
|
SftpHome {
|
||||||
|
tab_id: String,
|
||||||
|
home: String,
|
||||||
|
},
|
||||||
|
TransferProgress {
|
||||||
|
#[allow(dead_code)]
|
||||||
|
tab_id: String,
|
||||||
|
id: String,
|
||||||
|
transferred: u64,
|
||||||
|
total: Option<u64>,
|
||||||
|
state: TransferState,
|
||||||
|
},
|
||||||
|
TransferStarted {
|
||||||
|
tab_id: String,
|
||||||
|
info: TransferInfo,
|
||||||
|
},
|
||||||
|
Closed {
|
||||||
|
tab_id: String,
|
||||||
|
reason: String,
|
||||||
|
},
|
||||||
|
TerminalTitleChanged {
|
||||||
|
tab_id: String,
|
||||||
|
title: String,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
@@ -118,36 +162,68 @@ pub struct SftpUiState {
|
|||||||
pub entries: Vec<RemoteEntry>,
|
pub entries: Vec<RemoteEntry>,
|
||||||
pub selected_path: Option<String>,
|
pub selected_path: Option<String>,
|
||||||
pub preview: Option<PreviewData>,
|
pub preview: Option<PreviewData>,
|
||||||
|
pub selected_entries: std::collections::HashSet<String>,
|
||||||
|
pub home_dir: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TerminalTab {
|
impl TerminalTab {
|
||||||
pub fn new_local(id: String, title: String, backend: BackendTx) -> Self {
|
pub fn new_local(
|
||||||
Self::new(id, title, TabKind::Local, "local shell".into(), backend)
|
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(
|
let mut tab = Self::new(
|
||||||
id,
|
id,
|
||||||
session.name.clone(),
|
session.name.clone(),
|
||||||
TabKind::Ssh,
|
TabKind::Ssh,
|
||||||
format!("connecting {}@{}:{}", session.user, session.host, session.port),
|
format!(
|
||||||
|
"connecting {}@{}:{}",
|
||||||
|
session.user, session.host, session.port
|
||||||
|
),
|
||||||
backend,
|
backend,
|
||||||
|
events,
|
||||||
);
|
);
|
||||||
tab.session = Some(session.clone());
|
tab.session = Some(session.clone());
|
||||||
tab.connected = false;
|
tab.connected = false;
|
||||||
tab.sftp = Some(SftpUiState {
|
tab.sftp = Some(SftpUiState {
|
||||||
current_path: "/".into(),
|
current_path: "/".into(),
|
||||||
status: "sftp connecting...".into(),
|
status: t!("sftp_connecting").to_string(),
|
||||||
entries: Vec::new(),
|
entries: Vec::new(),
|
||||||
selected_path: None,
|
selected_path: None,
|
||||||
preview: None,
|
preview: None,
|
||||||
|
selected_entries: std::collections::HashSet::new(),
|
||||||
|
home_dir: "/".into(),
|
||||||
});
|
});
|
||||||
tab
|
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 {
|
Self {
|
||||||
id,
|
id: id.clone(),
|
||||||
title,
|
title,
|
||||||
kind,
|
kind,
|
||||||
status,
|
status,
|
||||||
@@ -155,7 +231,7 @@ impl TerminalTab {
|
|||||||
session: None,
|
session: None,
|
||||||
sftp: None,
|
sftp: None,
|
||||||
processor: Processor::new(),
|
processor: Processor::new(),
|
||||||
term: new_term(100, 30, backend.clone()),
|
term: new_term(100, 30, backend.clone(), id, events),
|
||||||
cols: 100,
|
cols: 100,
|
||||||
rows: 30,
|
rows: 30,
|
||||||
backend,
|
backend,
|
||||||
@@ -260,8 +336,11 @@ impl TerminalTab {
|
|||||||
self.term.scroll_display(Scroll::Bottom);
|
self.term.scroll_display(Scroll::Bottom);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
pub fn has_selection(&self) -> bool {
|
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) {
|
pub fn clear_selection(&mut self) {
|
||||||
@@ -269,7 +348,9 @@ impl TerminalTab {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn selection_text(&self) -> Option<String> {
|
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(
|
pub fn begin_selection(
|
||||||
@@ -297,19 +378,25 @@ impl TerminalTab {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn paste_text(&mut self, text: &str) {
|
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
|
self.backend
|
||||||
.send(BackendCommand::Input(paste_text.into_bytes()));
|
.send(BackendCommand::Input(paste_text.into_bytes()));
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn viewport_selection_from_range(
|
fn viewport_selection_from_range(
|
||||||
display_offset: usize,
|
display_offset: usize,
|
||||||
selection: &Option<SelectionRange>,
|
selection: &Option<SelectionRange>,
|
||||||
) -> Option<ViewportSelection> {
|
) -> 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 start = point_to_viewport(display_offset, start)?;
|
||||||
let end = point_to_viewport(display_offset, end)?;
|
let end = point_to_viewport(display_offset, end)?;
|
||||||
|
|
||||||
@@ -324,13 +411,17 @@ fn viewport_selection_from_range(
|
|||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
struct TerminalListener {
|
struct TerminalListener {
|
||||||
|
tab_id: String,
|
||||||
backend: BackendTx,
|
backend: BackendTx,
|
||||||
|
events: std::sync::mpsc::Sender<BackendEvent>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl EventListener for TerminalListener {
|
impl EventListener for TerminalListener {
|
||||||
fn send_event(&self, event: Event) {
|
fn send_event(&self, event: Event) {
|
||||||
match 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) => {
|
Event::TextAreaSizeRequest(format) => {
|
||||||
let size = alacritty_terminal::event::WindowSize {
|
let size = alacritty_terminal::event::WindowSize {
|
||||||
num_lines: 30,
|
num_lines: 30,
|
||||||
@@ -341,19 +432,36 @@ impl EventListener for TerminalListener {
|
|||||||
self.backend
|
self.backend
|
||||||
.send(BackendCommand::Input(format(size).into_bytes()));
|
.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> {
|
use rust_i18n::t;
|
||||||
|
fn new_term(
|
||||||
|
cols: u16,
|
||||||
|
rows: u16,
|
||||||
|
backend: BackendTx,
|
||||||
|
tab_id: String,
|
||||||
|
events: std::sync::mpsc::Sender<BackendEvent>,
|
||||||
|
) -> Term<TerminalListener> {
|
||||||
Term::new(
|
Term::new(
|
||||||
Config {
|
Config {
|
||||||
scrolling_history: 2000,
|
scrolling_history: 2000,
|
||||||
..Config::default()
|
..Config::default()
|
||||||
},
|
},
|
||||||
&TerminalSize::new(cols, rows),
|
&TerminalSize::new(cols, rows),
|
||||||
TerminalListener { backend },
|
TerminalListener {
|
||||||
|
tab_id,
|
||||||
|
backend,
|
||||||
|
events,
|
||||||
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -385,7 +493,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)
|
zed_like_to_esc_str(keystroke, app_cursor_mode, option_as_meta)
|
||||||
.map(|text| text.into_owned().into_bytes())
|
.map(|text| text.into_owned().into_bytes())
|
||||||
}
|
}
|
||||||
@@ -555,3 +667,38 @@ fn modifier_code(keystroke: &Keystroke) -> u32 {
|
|||||||
}
|
}
|
||||||
modifier_code + 1
|
modifier_code + 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||||
|
pub enum TransferType {
|
||||||
|
Upload,
|
||||||
|
Download,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||||
|
pub enum TransferState {
|
||||||
|
Running,
|
||||||
|
Paused,
|
||||||
|
Completed,
|
||||||
|
Failed(String),
|
||||||
|
Cancelled,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
|
pub struct TransferInfo {
|
||||||
|
pub id: String,
|
||||||
|
pub name: String,
|
||||||
|
pub source: String,
|
||||||
|
pub target: String,
|
||||||
|
pub kind: TransferType,
|
||||||
|
pub total_bytes: Option<u64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
|
||||||
|
pub struct Transfer {
|
||||||
|
pub tab_id: String,
|
||||||
|
pub tab_title: String,
|
||||||
|
pub info: TransferInfo,
|
||||||
|
pub transferred: u64,
|
||||||
|
pub total: Option<u64>,
|
||||||
|
pub state: TransferState,
|
||||||
|
}
|
||||||
|
|||||||
+66
-33
@@ -3,9 +3,10 @@ use alacritty_terminal::{
|
|||||||
vte::ansi::{Color as AnsiColor, CursorShape, NamedColor},
|
vte::ansi::{Color as AnsiColor, CursorShape, NamedColor},
|
||||||
};
|
};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
App, Bounds, Element, ElementId, Entity, FocusHandle, Font, FontStyle, FontWeight, GlobalElementId, Hsla, InputHandler, IntoElement,
|
App, Bounds, Element, ElementId, Entity, FocusHandle, Font, FontStyle, FontWeight,
|
||||||
LayoutId, Pixels, Point, Rgba, StrikethroughStyle, TextRun, TextStyle, UTF16Selection, UnderlineStyle, Window, fill, point, px,
|
GlobalElementId, Hsla, InputHandler, IntoElement, LayoutId, Pixels, Point, Rgba, SharedString,
|
||||||
relative, rgb,
|
StrikethroughStyle, TextRun, TextStyle, UTF16Selection, UnderlineStyle, Window, fill, point,
|
||||||
|
px, relative, rgb,
|
||||||
};
|
};
|
||||||
use gpui_component::ActiveTheme as _;
|
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(
|
let pos = point(
|
||||||
origin.x + metrics.cell_width * self.col as f32,
|
origin.x + metrics.cell_width * self.col as f32,
|
||||||
origin.y + metrics.line_height * self.row as f32,
|
origin.y + metrics.line_height * self.row as f32,
|
||||||
@@ -120,7 +127,7 @@ pub struct TerminalElement {
|
|||||||
focus_handle: FocusHandle,
|
focus_handle: FocusHandle,
|
||||||
snapshot: RenderSnapshot,
|
snapshot: RenderSnapshot,
|
||||||
marked_text: Option<String>,
|
marked_text: Option<String>,
|
||||||
font_family: &'static str,
|
font_family: SharedString,
|
||||||
font_size: Pixels,
|
font_size: Pixels,
|
||||||
line_height: Pixels,
|
line_height: Pixels,
|
||||||
cell_width: Pixels,
|
cell_width: Pixels,
|
||||||
@@ -137,6 +144,8 @@ pub struct PrepaintState {
|
|||||||
struct TerminalInputHandler {
|
struct TerminalInputHandler {
|
||||||
view: Entity<Ashell>,
|
view: Entity<Ashell>,
|
||||||
element_bounds: Bounds<Pixels>,
|
element_bounds: Bounds<Pixels>,
|
||||||
|
cell_width: f32,
|
||||||
|
line_height: f32,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl InputHandler for TerminalInputHandler {
|
impl InputHandler for TerminalInputHandler {
|
||||||
@@ -146,10 +155,13 @@ impl InputHandler for TerminalInputHandler {
|
|||||||
_window: &mut Window,
|
_window: &mut Window,
|
||||||
cx: &mut App,
|
cx: &mut App,
|
||||||
) -> Option<UTF16Selection> {
|
) -> Option<UTF16Selection> {
|
||||||
self.view.read(cx).terminal_accepts_text_input().then_some(UTF16Selection {
|
self.view
|
||||||
range: 0..0,
|
.read(cx)
|
||||||
reversed: false,
|
.terminal_accepts_text_input()
|
||||||
})
|
.then_some(UTF16Selection {
|
||||||
|
range: 0..0,
|
||||||
|
reversed: false,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn marked_text_range(
|
fn marked_text_range(
|
||||||
@@ -209,7 +221,7 @@ impl InputHandler for TerminalInputHandler {
|
|||||||
) -> Option<Bounds<Pixels>> {
|
) -> Option<Bounds<Pixels>> {
|
||||||
self.view
|
self.view
|
||||||
.read(cx)
|
.read(cx)
|
||||||
.terminal_ime_bounds_for_range(range_utf16, self.element_bounds)
|
.terminal_ime_bounds_for_range(range_utf16, self.element_bounds, self.cell_width, self.line_height)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn character_index_for_point(
|
fn character_index_for_point(
|
||||||
@@ -240,7 +252,7 @@ impl TerminalElement {
|
|||||||
focus_handle: FocusHandle,
|
focus_handle: FocusHandle,
|
||||||
snapshot: RenderSnapshot,
|
snapshot: RenderSnapshot,
|
||||||
marked_text: Option<String>,
|
marked_text: Option<String>,
|
||||||
font_family: &'static str,
|
font_family: SharedString,
|
||||||
font_size: Pixels,
|
font_size: Pixels,
|
||||||
line_height: Pixels,
|
line_height: Pixels,
|
||||||
cell_width: Pixels,
|
cell_width: Pixels,
|
||||||
@@ -260,7 +272,7 @@ impl TerminalElement {
|
|||||||
fn base_text_style(&self, cx: &App) -> TextStyle {
|
fn base_text_style(&self, cx: &App) -> TextStyle {
|
||||||
TextStyle {
|
TextStyle {
|
||||||
color: cx.theme().foreground,
|
color: cx.theme().foreground,
|
||||||
font_family: self.font_family.into(),
|
font_family: self.font_family.clone(),
|
||||||
font_size: self.font_size.into(),
|
font_size: self.font_size.into(),
|
||||||
line_height: self.line_height.into(),
|
line_height: self.line_height.into(),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
@@ -277,15 +289,21 @@ impl TerminalElement {
|
|||||||
fg.a *= 0.7;
|
fg.a *= 0.7;
|
||||||
}
|
}
|
||||||
|
|
||||||
let underline = cell.flags.intersects(Flags::ALL_UNDERLINES).then(|| UnderlineStyle {
|
let underline = cell
|
||||||
color: Some(fg),
|
.flags
|
||||||
thickness: px(1.0),
|
.intersects(Flags::ALL_UNDERLINES)
|
||||||
wavy: cell.flags.contains(Flags::UNDERCURL),
|
.then(|| UnderlineStyle {
|
||||||
});
|
color: Some(fg),
|
||||||
let strikethrough = cell.flags.contains(Flags::STRIKEOUT).then(|| StrikethroughStyle {
|
thickness: px(1.0),
|
||||||
color: Some(fg),
|
wavy: cell.flags.contains(Flags::UNDERCURL),
|
||||||
thickness: px(1.0),
|
});
|
||||||
});
|
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) {
|
let weight = if cell.flags.intersects(Flags::BOLD | Flags::DIM_BOLD) {
|
||||||
FontWeight::BOLD
|
FontWeight::BOLD
|
||||||
@@ -303,7 +321,7 @@ impl TerminalElement {
|
|||||||
color: fg,
|
color: fg,
|
||||||
background_color: None,
|
background_color: None,
|
||||||
font: Font {
|
font: Font {
|
||||||
family: self.font_family.into(),
|
family: self.font_family.clone(),
|
||||||
weight,
|
weight,
|
||||||
style,
|
style,
|
||||||
..Font::default()
|
..Font::default()
|
||||||
@@ -326,10 +344,9 @@ impl TerminalElement {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let selected = self
|
let selected = self.snapshot.selection.is_some_and(|selection| {
|
||||||
.snapshot
|
selection_contains(selection, render_cell.row, render_cell.col)
|
||||||
.selection
|
});
|
||||||
.is_some_and(|selection| selection_contains(selection, render_cell.row, render_cell.col));
|
|
||||||
let bg = color_to_hsla(cell.bg, false, cx);
|
let bg = color_to_hsla(cell.bg, false, cx);
|
||||||
if selected || !is_default_bg(cell.bg) {
|
if selected || !is_default_bg(cell.bg) {
|
||||||
rects.push(LayoutRect {
|
rects.push(LayoutRect {
|
||||||
@@ -477,6 +494,8 @@ impl Element for TerminalElement {
|
|||||||
TerminalInputHandler {
|
TerminalInputHandler {
|
||||||
view: self.view.clone(),
|
view: self.view.clone(),
|
||||||
element_bounds: prepaint.bounds,
|
element_bounds: prepaint.bounds,
|
||||||
|
cell_width: prepaint.metrics.cell_width.as_f32(),
|
||||||
|
line_height: prepaint.metrics.line_height.as_f32(),
|
||||||
},
|
},
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
@@ -499,7 +518,7 @@ impl Element for TerminalElement {
|
|||||||
&[TextRun {
|
&[TextRun {
|
||||||
len: marked_text.len(),
|
len: marked_text.len(),
|
||||||
font: Font {
|
font: Font {
|
||||||
family: self.font_family.into(),
|
family: self.font_family.clone(),
|
||||||
..Font::default()
|
..Font::default()
|
||||||
},
|
},
|
||||||
color: base_style.color,
|
color: base_style.color,
|
||||||
@@ -525,7 +544,11 @@ impl Element for TerminalElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if let Some(cursor) = prepaint.cursor {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
let x = prepaint.bounds.origin.x + prepaint.metrics.cell_width * cursor.col as f32;
|
let x = prepaint.bounds.origin.x + prepaint.metrics.cell_width * cursor.col as f32;
|
||||||
@@ -534,7 +557,10 @@ impl Element for TerminalElement {
|
|||||||
CursorShape::Hidden => {}
|
CursorShape::Hidden => {}
|
||||||
CursorShape::Beam => {
|
CursorShape::Beam => {
|
||||||
window.paint_quad(fill(
|
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,
|
cursor.color,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
@@ -572,7 +598,10 @@ fn merge_rects(mut rects: Vec<LayoutRect>) -> Vec<LayoutRect> {
|
|||||||
|
|
||||||
for rect in rects {
|
for rect in rects {
|
||||||
if let Some(last) = merged.last_mut() {
|
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;
|
last.cells += rect.cells;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -601,7 +630,11 @@ fn selection_contains(selection: ViewportSelection, row: i32, col: i32) -> bool
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn is_blank(cell: &alacritty_terminal::term::cell::Cell) -> 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 {
|
fn is_default_bg(color: AnsiColor) -> bool {
|
||||||
@@ -623,8 +656,8 @@ fn color_to_hsla(color: AnsiColor, foreground: bool, cx: &App) -> Hsla {
|
|||||||
|
|
||||||
fn ansi_index_color(index: u8, _cx: &App) -> Hsla {
|
fn ansi_index_color(index: u8, _cx: &App) -> Hsla {
|
||||||
const ANSI_16: [u32; 16] = [
|
const ANSI_16: [u32; 16] = [
|
||||||
0x1f2430, 0xff5c57, 0x5af78e, 0xf3f99d, 0x57c7ff, 0xff6ac1, 0x9aedfe, 0xf1f1f0,
|
0x1f2430, 0xff5c57, 0x5af78e, 0xf3f99d, 0x57c7ff, 0xff6ac1, 0x9aedfe, 0xf1f1f0, 0x686868,
|
||||||
0x686868, 0xff5c57, 0x5af78e, 0xf3f99d, 0x57c7ff, 0xff6ac1, 0x9aedfe, 0xffffff,
|
0xff5c57, 0x5af78e, 0xf3f99d, 0x57c7ff, 0xff6ac1, 0x9aedfe, 0xffffff,
|
||||||
];
|
];
|
||||||
|
|
||||||
if (index as usize) < ANSI_16.len() {
|
if (index as usize) < ANSI_16.len() {
|
||||||
|
|||||||
@@ -0,0 +1,347 @@
|
|||||||
|
use std::ops::Range;
|
||||||
|
|
||||||
|
use alacritty_terminal::index::Side;
|
||||||
|
use alacritty_terminal::selection::SelectionType;
|
||||||
|
use gpui::{
|
||||||
|
ClipboardItem, Context, KeyDownEvent, MouseButton, MouseDownEvent, MouseMoveEvent,
|
||||||
|
MouseUpEvent, Pixels, Point, ScrollDelta, ScrollWheelEvent, Window, px,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
Ashell,
|
||||||
|
terminal::{BackendCommand, encode_key},
|
||||||
|
TerminalBacktabKey, TerminalTabKey,
|
||||||
|
};
|
||||||
|
|
||||||
|
impl Ashell {
|
||||||
|
pub(crate) fn on_terminal_key_down(
|
||||||
|
&mut self,
|
||||||
|
event: &KeyDownEvent,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
if event.keystroke.modifiers.secondary() && event.keystroke.key == "," {
|
||||||
|
self.show_settings_dialog(window, cx);
|
||||||
|
window.prevent_default();
|
||||||
|
cx.stop_propagation();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if event.keystroke.modifiers.shift
|
||||||
|
&& event.keystroke.modifiers.secondary()
|
||||||
|
&& event.keystroke.key == "o"
|
||||||
|
{
|
||||||
|
self.show_selector_dialog(window, cx);
|
||||||
|
window.prevent_default();
|
||||||
|
cx.stop_propagation();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if event.keystroke.modifiers.secondary() && event.keystroke.key.eq_ignore_ascii_case("c") {
|
||||||
|
if let Some(text) = self.active_terminal_selection_text() {
|
||||||
|
cx.write_to_clipboard(ClipboardItem::new_string(text));
|
||||||
|
window.prevent_default();
|
||||||
|
cx.stop_propagation();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if event.keystroke.modifiers.secondary() && event.keystroke.key.eq_ignore_ascii_case("v") {
|
||||||
|
if let Some(clipboard) = cx.read_from_clipboard() {
|
||||||
|
if let Some(text) = clipboard.text() {
|
||||||
|
self.paste_into_terminal(&text, window, cx);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if event.prefer_character_input {
|
||||||
|
if let Some(text) = event.keystroke.key_char.as_deref() {
|
||||||
|
if !text.is_empty()
|
||||||
|
&& !event.keystroke.modifiers.control
|
||||||
|
&& !event.keystroke.modifiers.function
|
||||||
|
&& !event.keystroke.modifiers.platform
|
||||||
|
{
|
||||||
|
self.send_terminal_input(text.as_bytes().to_vec(), window, cx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some(active_id) = self.active_tab.clone() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Some(tab) = self.tabs.iter_mut().find(|t| t.id == active_id) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
if tab.render_snapshot().display_offset > 0 {
|
||||||
|
tab.scroll_to_bottom();
|
||||||
|
}
|
||||||
|
tab.clear_selection();
|
||||||
|
|
||||||
|
if let Some(bytes) = encode_key(&event.keystroke, tab.app_cursor_mode(), false) {
|
||||||
|
tab.backend.send(BackendCommand::Input(bytes));
|
||||||
|
window.prevent_default();
|
||||||
|
cx.stop_propagation();
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn on_terminal_tab_action(
|
||||||
|
&mut self,
|
||||||
|
_: &TerminalTabKey,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
self.send_terminal_input(vec![b'\t'], window, cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn on_terminal_backtab_action(
|
||||||
|
&mut self,
|
||||||
|
_: &TerminalBacktabKey,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
self.send_terminal_input(b"\x1b[Z".to_vec(), window, cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn send_terminal_input(&mut self, bytes: Vec<u8>, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
let Some(active_id) = self.active_tab.clone() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Some(tab) = self.tabs.iter_mut().find(|t| t.id == active_id) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
if tab.render_snapshot().display_offset > 0 {
|
||||||
|
tab.scroll_to_bottom();
|
||||||
|
}
|
||||||
|
|
||||||
|
tab.clear_selection();
|
||||||
|
tab.backend.send(BackendCommand::Input(bytes));
|
||||||
|
window.prevent_default();
|
||||||
|
cx.stop_propagation();
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn active_terminal_selection_text(&self) -> Option<String> {
|
||||||
|
let active_id = self.active_tab.as_ref()?;
|
||||||
|
self.tabs
|
||||||
|
.iter()
|
||||||
|
.find(|tab| &tab.id == active_id)
|
||||||
|
.and_then(|tab| tab.selection_text())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn paste_into_terminal(&mut self, text: &str, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
let Some(active_id) = self.active_tab.clone() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Some(tab) = self.tabs.iter_mut().find(|tab| tab.id == active_id) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
if tab.render_snapshot().display_offset > 0 {
|
||||||
|
tab.scroll_to_bottom();
|
||||||
|
}
|
||||||
|
tab.clear_selection();
|
||||||
|
tab.paste_text(text);
|
||||||
|
window.prevent_default();
|
||||||
|
cx.stop_propagation();
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn terminal_accepts_text_input(&self) -> bool {
|
||||||
|
self.active_tab.is_some()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn terminal_marked_text_range(&self) -> Option<Range<usize>> {
|
||||||
|
self.terminal_marked_text
|
||||||
|
.as_ref()
|
||||||
|
.map(|text| 0..text.encode_utf16().count())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn set_terminal_marked_text(
|
||||||
|
&mut self,
|
||||||
|
text: String,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
self.terminal_marked_text = if text.is_empty() { None } else { Some(text) };
|
||||||
|
window.invalidate_character_coordinates();
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn clear_terminal_marked_text(
|
||||||
|
&mut self,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
if self.terminal_marked_text.take().is_some() {
|
||||||
|
window.invalidate_character_coordinates();
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn commit_terminal_ime_text(
|
||||||
|
&mut self,
|
||||||
|
text: &str,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
let Some(active_id) = self.active_tab.clone() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Some(tab) = self.tabs.iter_mut().find(|tab| tab.id == active_id) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
if tab.render_snapshot().display_offset > 0 {
|
||||||
|
tab.scroll_to_bottom();
|
||||||
|
}
|
||||||
|
tab.clear_selection();
|
||||||
|
self.terminal_marked_text = None;
|
||||||
|
tab.backend
|
||||||
|
.send(BackendCommand::Input(text.as_bytes().to_vec()));
|
||||||
|
window.invalidate_character_coordinates();
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn on_terminal_right_click(
|
||||||
|
&mut self,
|
||||||
|
_event: &MouseDownEvent,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
if !self.config.right_click_copy_paste() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut handled = false;
|
||||||
|
if let Some(text) = self.active_terminal_selection_text() {
|
||||||
|
if !text.is_empty() {
|
||||||
|
cx.write_to_clipboard(gpui::ClipboardItem::new_string(text));
|
||||||
|
|
||||||
|
let active_id = self.active_tab.clone();
|
||||||
|
if let Some(active_id) = active_id {
|
||||||
|
if let Some(tab) = self.tabs.iter_mut().find(|tab| tab.id == active_id) {
|
||||||
|
tab.clear_selection();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cx.notify();
|
||||||
|
handled = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !handled {
|
||||||
|
if let Some(clipboard_item) = cx.read_from_clipboard() {
|
||||||
|
if let Some(text) = clipboard_item.text() {
|
||||||
|
if !text.is_empty() {
|
||||||
|
self.paste_into_terminal(&text, window, cx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn begin_terminal_selection(&mut self, event: &MouseDownEvent, cx: &mut Context<Self>) {
|
||||||
|
let click_count = event.click_count.max(1);
|
||||||
|
let selection_type = match click_count {
|
||||||
|
1 => SelectionType::Simple,
|
||||||
|
2 => SelectionType::Semantic,
|
||||||
|
3 => SelectionType::Lines,
|
||||||
|
_ => SelectionType::Simple,
|
||||||
|
};
|
||||||
|
let Some((row, col, side)) = self.terminal_grid_point_and_side(event.position) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Some(active_id) = self.active_tab.clone() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
if let Some(tab) = self.tabs.iter_mut().find(|tab| tab.id == active_id) {
|
||||||
|
tab.begin_selection(row, col, side, selection_type);
|
||||||
|
self.terminal_selecting = true;
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn on_terminal_mouse_move(
|
||||||
|
&mut self,
|
||||||
|
event: &MouseMoveEvent,
|
||||||
|
_window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
if !self.terminal_selecting || event.pressed_button != Some(MouseButton::Left) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let Some((row, col, side)) = self.terminal_grid_point_and_side(event.position) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Some(active_id) = self.active_tab.clone() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
if let Some(tab) = self.tabs.iter_mut().find(|tab| tab.id == active_id) {
|
||||||
|
tab.update_selection(row, col, side);
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn on_terminal_mouse_up(
|
||||||
|
&mut self,
|
||||||
|
_event: &MouseUpEvent,
|
||||||
|
_window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
self.terminal_selecting = false;
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn terminal_grid_point_and_side(
|
||||||
|
&self,
|
||||||
|
position: Point<Pixels>,
|
||||||
|
) -> Option<(usize, usize, Side)> {
|
||||||
|
let bounds = self.terminal_bounds?;
|
||||||
|
if !bounds.contains(&position) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let local_x = (position.x - bounds.origin.x).max(px(0.));
|
||||||
|
let local_y = (position.y - bounds.origin.y).max(px(0.));
|
||||||
|
let cell_width = px(self.terminal_cell_width());
|
||||||
|
let line_height = px(self.terminal_line_height());
|
||||||
|
let snapshot = self.active_snapshot()?;
|
||||||
|
let max_col = snapshot.cols.saturating_sub(1);
|
||||||
|
let max_row = snapshot.rows.saturating_sub(1);
|
||||||
|
let col = ((local_x / cell_width).floor() as usize).min(max_col);
|
||||||
|
let row = ((local_y / line_height).floor() as usize).min(max_row);
|
||||||
|
let cell_offset_x = px(local_x.as_f32() % cell_width.as_f32());
|
||||||
|
let side = if cell_offset_x >= (cell_width / 2.) {
|
||||||
|
Side::Right
|
||||||
|
} else {
|
||||||
|
Side::Left
|
||||||
|
};
|
||||||
|
Some((row, col, side))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn on_terminal_scroll(
|
||||||
|
&mut self,
|
||||||
|
event: &ScrollWheelEvent,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
let delta_lines = match event.delta {
|
||||||
|
ScrollDelta::Lines(point) => point.y.round() as i32,
|
||||||
|
ScrollDelta::Pixels(point) => {
|
||||||
|
(point.y.as_f32() / self.terminal_line_height()).round() as i32
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if delta_lines == 0 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let Some(active_id) = self.active_tab.clone() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
if let Some(tab) = self.tabs.iter_mut().find(|tab| tab.id == active_id) {
|
||||||
|
tab.scroll_history(delta_lines);
|
||||||
|
window.prevent_default();
|
||||||
|
cx.stop_propagation();
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+210
@@ -0,0 +1,210 @@
|
|||||||
|
use gpui::{Anchor, Context, IntoElement, SharedString, Window, px};
|
||||||
|
use gpui_component::{
|
||||||
|
ActiveTheme as _, IconName, Sizable as _, Theme, ThemeMode, ThemeRegistry,
|
||||||
|
button::{Button, ButtonVariants as _},
|
||||||
|
menu::{DropdownMenu as _, PopupMenuItem},
|
||||||
|
};
|
||||||
|
use rust_i18n::t;
|
||||||
|
|
||||||
|
use crate::Ashell;
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Ashell {
|
||||||
|
pub(crate) 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);
|
||||||
|
self.status = format!("theme mode: {}", cx.theme().mode.name()).into();
|
||||||
|
self.persist_theme_preferences();
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn apply_theme(&mut self, name: SharedString, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
let Some(theme_config) = ThemeRegistry::global(cx).themes().get(&name).cloned() else {
|
||||||
|
self.status = format!("theme not found: {name}").into();
|
||||||
|
cx.notify();
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
if theme_config.mode.is_dark() {
|
||||||
|
self.dark_theme_name = name.clone();
|
||||||
|
} else {
|
||||||
|
self.light_theme_name = name.clone();
|
||||||
|
}
|
||||||
|
self.apply_theme_preferences(window, cx);
|
||||||
|
self.status = format!("theme: {name}").into();
|
||||||
|
self.persist_theme_preferences();
|
||||||
|
window.refresh();
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn set_follow_system_theme(
|
||||||
|
&mut self,
|
||||||
|
follow: bool,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
self.follow_system_theme = follow;
|
||||||
|
if follow {
|
||||||
|
self.status = "theme mode: system".into();
|
||||||
|
} else {
|
||||||
|
self.status = format!("theme mode: {}", cx.theme().mode.name()).into();
|
||||||
|
}
|
||||||
|
self.apply_theme_preferences(window, cx);
|
||||||
|
self.persist_theme_preferences();
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) 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();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn apply_theme_preferences(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
let light_theme = ThemeRegistry::global(cx)
|
||||||
|
.themes()
|
||||||
|
.get(&self.light_theme_name)
|
||||||
|
.cloned()
|
||||||
|
.unwrap_or_else(|| ThemeRegistry::global(cx).default_light_theme().clone());
|
||||||
|
let dark_theme = ThemeRegistry::global(cx)
|
||||||
|
.themes()
|
||||||
|
.get(&self.dark_theme_name)
|
||||||
|
.cloned()
|
||||||
|
.unwrap_or_else(|| ThemeRegistry::global(cx).default_dark_theme().clone());
|
||||||
|
let theme = Theme::global_mut(cx);
|
||||||
|
theme.light_theme = light_theme;
|
||||||
|
theme.dark_theme = dark_theme;
|
||||||
|
theme.font_size = px(self.ui_font_size);
|
||||||
|
set_theme_font_names(theme, &self.ui_font_family);
|
||||||
|
|
||||||
|
if self.follow_system_theme {
|
||||||
|
Theme::sync_system_appearance(Some(window), cx);
|
||||||
|
} else {
|
||||||
|
Theme::change(self.theme_mode, Some(window), cx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) 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(),
|
||||||
|
);
|
||||||
|
if let Err(err) = self.config.save() {
|
||||||
|
tracing::warn!("failed to save theme preferences: {err:#}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn theme_dropdown(&self, cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
|
let view = cx.entity();
|
||||||
|
let themes = ThemeRegistry::global(cx)
|
||||||
|
.sorted_themes()
|
||||||
|
.into_iter()
|
||||||
|
.cloned()
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
let light_themes = themes
|
||||||
|
.iter()
|
||||||
|
.filter(|theme| !theme.mode.is_dark())
|
||||||
|
.map(|theme| theme.name.clone())
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
let dark_themes = themes
|
||||||
|
.iter()
|
||||||
|
.filter(|theme| theme.mode.is_dark())
|
||||||
|
.map(|theme| theme.name.clone())
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
let follow_system = self.follow_system_theme;
|
||||||
|
let is_dark_mode = cx.theme().mode.is_dark();
|
||||||
|
let light_theme_name = self.light_theme_name.clone();
|
||||||
|
let dark_theme_name = self.dark_theme_name.clone();
|
||||||
|
let icon = if follow_system {
|
||||||
|
IconName::Sun
|
||||||
|
} else if is_dark_mode {
|
||||||
|
IconName::Moon
|
||||||
|
} else {
|
||||||
|
IconName::Sun
|
||||||
|
};
|
||||||
|
|
||||||
|
Button::new("theme-dropdown")
|
||||||
|
.ghost()
|
||||||
|
.small()
|
||||||
|
.icon(icon)
|
||||||
|
.dropdown_menu_with_anchor(Anchor::BottomRight, move |mut menu, window, _| {
|
||||||
|
menu = menu
|
||||||
|
.min_w(220.)
|
||||||
|
.item(
|
||||||
|
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(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(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(t!("light_theme").to_string());
|
||||||
|
|
||||||
|
for theme_name in light_themes.clone() {
|
||||||
|
let checked = theme_name == light_theme_name;
|
||||||
|
menu = menu.item(
|
||||||
|
PopupMenuItem::new(theme_name.clone())
|
||||||
|
.checked(checked)
|
||||||
|
.on_click(window.listener_for(&view, move |this, _, window, cx| {
|
||||||
|
this.apply_theme(theme_name.clone(), window, cx)
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
menu = menu.separator();
|
||||||
|
menu = menu.label(t!("dark_theme").to_string());
|
||||||
|
|
||||||
|
for theme_name in dark_themes.clone() {
|
||||||
|
let checked = theme_name == dark_theme_name;
|
||||||
|
menu = menu.item(
|
||||||
|
PopupMenuItem::new(theme_name.clone())
|
||||||
|
.checked(checked)
|
||||||
|
.on_click(window.listener_for(&view, move |this, _, window, cx| {
|
||||||
|
this.apply_theme(theme_name.clone(), window, cx)
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
menu
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user