mirror of
https://github.com/rust-kotlin/ashell.git
synced 2026-10-03 16:00:18 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
47ec91d099 | ||
|
|
e2501ca764 | ||
|
|
a34a673b52 | ||
|
|
0708ac7baa | ||
|
|
ff4079a60f | ||
|
|
9d0868dd7c | ||
|
|
f63b8b418c | ||
|
|
e1d39d8dc4 | ||
|
|
c9f722071a | ||
|
|
2e279e206f | ||
|
|
7b410afea7 |
@@ -21,6 +21,7 @@ on:
|
|||||||
options:
|
options:
|
||||||
- all
|
- all
|
||||||
- windows-x64
|
- windows-x64
|
||||||
|
- macos-arm64
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: write # needed to create / update the GitHub Release
|
contents: write # needed to create / update the GitHub Release
|
||||||
@@ -29,6 +30,7 @@ jobs:
|
|||||||
build:
|
build:
|
||||||
if: >-
|
if: >-
|
||||||
github.ref != 'refs/heads/build/windows-x64' &&
|
github.ref != 'refs/heads/build/windows-x64' &&
|
||||||
|
github.ref != 'refs/heads/build/macos-arm64' &&
|
||||||
(github.event_name != 'workflow_dispatch' || inputs.platform == 'all')
|
(github.event_name != 'workflow_dispatch' || inputs.platform == 'all')
|
||||||
name: ${{ matrix.name }}
|
name: ${{ matrix.name }}
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
@@ -338,6 +340,122 @@ jobs:
|
|||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
retention-days: 14
|
retention-days: 14
|
||||||
|
|
||||||
|
build-macos-arm64:
|
||||||
|
if: >-
|
||||||
|
github.ref == 'refs/heads/build/macos-arm64' ||
|
||||||
|
(github.event_name == 'workflow_dispatch' && inputs.platform == 'macos-arm64')
|
||||||
|
name: macos-arm64
|
||||||
|
runs-on: macos-14
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install Rust toolchain
|
||||||
|
uses: dtolnay/rust-toolchain@stable
|
||||||
|
with:
|
||||||
|
targets: aarch64-apple-darwin
|
||||||
|
|
||||||
|
- name: Cache cargo build
|
||||||
|
uses: Swatinem/rust-cache@v2
|
||||||
|
with:
|
||||||
|
key: aarch64-apple-darwin
|
||||||
|
|
||||||
|
- name: Build (release)
|
||||||
|
run: cargo build --release --target aarch64-apple-darwin
|
||||||
|
|
||||||
|
- name: Set package metadata
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
VERSION="$(sed -n 's/^version = "\([^"]*\)"/\1/p' Cargo.toml | head -n 1)"
|
||||||
|
if [ -z "$VERSION" ]; then
|
||||||
|
echo "Unable to read the package version from Cargo.toml" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
PACKAGE_VERSION="${VERSION}-dev.${GITHUB_SHA:0:7}"
|
||||||
|
echo "VERSION=$VERSION" >> "$GITHUB_ENV"
|
||||||
|
echo "PACKAGE_VERSION=$PACKAGE_VERSION" >> "$GITHUB_ENV"
|
||||||
|
echo "PACKAGE_BASENAME=ashell-v${PACKAGE_VERSION}-macos-arm64" >> "$GITHUB_ENV"
|
||||||
|
|
||||||
|
- name: Package macOS app, DMG, and portable archive
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
APP_ROOT="$RUNNER_TEMP/${PACKAGE_BASENAME}-app"
|
||||||
|
DMG_ROOT="$RUNNER_TEMP/${PACKAGE_BASENAME}-dmg"
|
||||||
|
APP="$APP_ROOT/ashell.app"
|
||||||
|
CONTENTS="$APP/Contents"
|
||||||
|
|
||||||
|
mkdir -p "$CONTENTS/MacOS" "$CONTENTS/Resources" "$DMG_ROOT" dist
|
||||||
|
cp "target/aarch64-apple-darwin/release/ashell" "$CONTENTS/MacOS/ashell"
|
||||||
|
chmod 755 "$CONTENTS/MacOS/ashell"
|
||||||
|
cp "assets/icons/ashell.icns" "$CONTENTS/Resources/ashell.icns"
|
||||||
|
|
||||||
|
cat > "$CONTENTS/Info.plist" <<EOF
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
|
||||||
|
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleDevelopmentRegion</key>
|
||||||
|
<string>en</string>
|
||||||
|
<key>CFBundleExecutable</key>
|
||||||
|
<string>ashell</string>
|
||||||
|
<key>CFBundleIconFile</key>
|
||||||
|
<string>ashell.icns</string>
|
||||||
|
<key>CFBundleIdentifier</key>
|
||||||
|
<string>dev.ashell.app</string>
|
||||||
|
<key>CFBundleInfoDictionaryVersion</key>
|
||||||
|
<string>6.0</string>
|
||||||
|
<key>CFBundleName</key>
|
||||||
|
<string>ashell</string>
|
||||||
|
<key>CFBundlePackageType</key>
|
||||||
|
<string>APPL</string>
|
||||||
|
<key>CFBundleShortVersionString</key>
|
||||||
|
<string>${VERSION}</string>
|
||||||
|
<key>CFBundleVersion</key>
|
||||||
|
<string>${GITHUB_RUN_NUMBER}</string>
|
||||||
|
<key>LSMinimumSystemVersion</key>
|
||||||
|
<string>12.0</string>
|
||||||
|
<key>NSHighResolutionCapable</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
|
EOF
|
||||||
|
|
||||||
|
printf 'APPL????' > "$CONTENTS/PkgInfo"
|
||||||
|
plutil -lint "$CONTENTS/Info.plist"
|
||||||
|
|
||||||
|
codesign --force --deep --sign - "$APP"
|
||||||
|
codesign --verify --deep --strict --verbose=2 "$APP"
|
||||||
|
|
||||||
|
ditto -c -k --sequesterRsrc --keepParent \
|
||||||
|
"$APP" "dist/${PACKAGE_BASENAME}-portable.zip"
|
||||||
|
ditto "$APP" "$DMG_ROOT/ashell.app"
|
||||||
|
ln -s /Applications "$DMG_ROOT/Applications"
|
||||||
|
hdiutil create \
|
||||||
|
-volname "ashell ${VERSION}" \
|
||||||
|
-srcfolder "$DMG_ROOT" \
|
||||||
|
-ov \
|
||||||
|
-format UDZO \
|
||||||
|
"dist/${PACKAGE_BASENAME}.dmg"
|
||||||
|
|
||||||
|
EXPECTED_ARCH="arm64"
|
||||||
|
ACTUAL_ARCHS="$(lipo -archs "$CONTENTS/MacOS/ashell")"
|
||||||
|
if [ "$ACTUAL_ARCHS" != "$EXPECTED_ARCH" ]; then
|
||||||
|
echo "Expected $EXPECTED_ARCH binary, found $ACTUAL_ARCHS" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
shasum -a 256 dist/*
|
||||||
|
|
||||||
|
- name: Upload workflow artifact
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: macos-arm64
|
||||||
|
path: dist/*
|
||||||
|
if-no-files-found: error
|
||||||
|
retention-days: 14
|
||||||
|
|
||||||
publish:
|
publish:
|
||||||
name: Publish Release
|
name: Publish Release
|
||||||
needs: build
|
needs: build
|
||||||
|
|||||||
Generated
+1
-1
@@ -315,7 +315,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ashell"
|
name = "ashell"
|
||||||
version = "0.5.0"
|
version = "0.5.2-fix1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"alacritty_terminal",
|
"alacritty_terminal",
|
||||||
"anyhow",
|
"anyhow",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "ashell"
|
name = "ashell"
|
||||||
version = "0.5.0"
|
version = "0.5.2-fix1"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
authors = ["ashell contributors"]
|
authors = ["ashell contributors"]
|
||||||
description = "A GPUI Component based SSH and local terminal client"
|
description = "A GPUI Component based SSH and local terminal client"
|
||||||
|
|||||||
+3
-4
@@ -107,10 +107,9 @@ native notification and a flashing bell on the corresponding tab. Unread notific
|
|||||||
red badge to the macOS Dock icon or a red overlay to the Windows taskbar icon. Clicking the native
|
red badge to the macOS Dock icon or a red overlay to the Windows taskbar icon. Clicking the native
|
||||||
notification activates ashell and switches to the originating terminal tab; viewing that tab clears
|
notification activates ashell and switches to the originating terminal tab; viewing that tab clears
|
||||||
its unread state. No reminder is shown when ashell is active and the notification originates in the
|
its unread state. No reminder is shown when ashell is active and the notification originates in the
|
||||||
currently visible tab. The loading indicator beside a tab title prioritizes OSC 133/633
|
currently visible tab. An unselected tab shows a loading indicator beside its title for two seconds
|
||||||
shell-integration command lifecycle markers. Shells and CLI tools without those markers fall back to
|
after receiving terminal output. Continued output refreshes the indicator, while the selected tab
|
||||||
recent terminal output activity, while an OSC 9 completion notification clears the task's loading
|
does not show it.
|
||||||
state immediately.
|
|
||||||
|
|
||||||
You can test a generic OSC 9 notification with:
|
You can test a generic OSC 9 notification with:
|
||||||
|
|
||||||
|
|||||||
@@ -106,9 +106,8 @@ OSC 9 携带通知正文;OSC 777 和 OSC 99 可以同时携带标题与正文
|
|||||||
符合显示时机的通知来自后台或非当前可见标签时,ashell 会显示系统通知,标签按钮以
|
符合显示时机的通知来自后台或非当前可见标签时,ashell 会显示系统通知,标签按钮以
|
||||||
闪动铃铛标记未读状态,并在 macOS Dock 或 Windows 任务栏图标上显示红色徽标。点击
|
闪动铃铛标记未读状态,并在 macOS Dock 或 Windows 任务栏图标上显示红色徽标。点击
|
||||||
系统通知会激活 ashell 并切换到对应的终端标签,查看对应标签后自动清除未读状态。
|
系统通知会激活 ashell 并切换到对应的终端标签,查看对应标签后自动清除未读状态。
|
||||||
如果应用已激活且通知来自当前可见标签,则不重复提醒。标签标题左侧的 Loading 动画
|
如果应用已激活且通知来自当前可见标签,则不重复提醒。未选中的标签在最近 2 秒内收到
|
||||||
优先识别 OSC 133/633 shell integration 的命令开始和结束标记;未提供协议标记时根据
|
终端输出时,会在标题左侧显示 Loading 动画;持续输出会刷新显示时间,当前标签不会显示。
|
||||||
近期终端输出活动回退显示。
|
|
||||||
|
|
||||||
可以分别使用下面的命令测试四种提醒:
|
可以分别使用下面的命令测试四种提醒:
|
||||||
|
|
||||||
|
|||||||
@@ -262,6 +262,10 @@ scroll: "Scroll ↓"
|
|||||||
clone: "Clone"
|
clone: "Clone"
|
||||||
about_feedback_hint: "If you encounter a problem or have a suggestion, please report it on our GitHub project."
|
about_feedback_hint: "If you encounter a problem or have a suggestion, please report it on our GitHub project."
|
||||||
about_description: "A GPUI Component based SSH and local terminal client"
|
about_description: "A GPUI Component based SSH and local terminal client"
|
||||||
|
about_tips_title: "Shortcuts & Usage Tips:"
|
||||||
|
about_tip_backspace: "Unbinding Shortcuts: Press Backspace while recording a shortcut in Settings -> Keybindings to clear/unbind it."
|
||||||
|
about_tip_close_shortcuts: "Default Close Shortcuts: macOS uses ⌘+Q (Quit App) / ⌘+W (Close Tab); Windows/Linux uses Alt+F4 or Ctrl+Q (Quit App) / Ctrl+W (Close Tab)."
|
||||||
|
about_tip_conflict_shortcuts: "Potential Shortcut Conflicts: On Linux/Windows, default Ctrl shortcuts (e.g. Ctrl+W to close tab, Ctrl+L to focus pane) will intercept terminal Shell bindings (e.g. Ctrl+W to backward-kill-word, Ctrl+L to clear screen). Rebind to Ctrl+Shift+W or press Backspace to unbind in Settings -> Keybindings."
|
||||||
version: "Version"
|
version: "Version"
|
||||||
close: "Close"
|
close: "Close"
|
||||||
menu_about_ashell: "About Ashell"
|
menu_about_ashell: "About Ashell"
|
||||||
|
|||||||
@@ -262,6 +262,10 @@ scroll: "滑动查看 ↓"
|
|||||||
clone: "克隆"
|
clone: "克隆"
|
||||||
about_feedback_hint: "如果遇到问题或者有什么建议,请前往我们的 GitHub 项目中反馈。"
|
about_feedback_hint: "如果遇到问题或者有什么建议,请前往我们的 GitHub 项目中反馈。"
|
||||||
about_description: "基于 GPUI Component 的 SSH 与本地终端客户端"
|
about_description: "基于 GPUI Component 的 SSH 与本地终端客户端"
|
||||||
|
about_tips_title: "快捷键与使用提示:"
|
||||||
|
about_tip_backspace: "快捷键解绑:在“设置 -> 快捷键”中录制按键时按 Backspace (退格键) 即可置空/解绑该快捷键。"
|
||||||
|
about_tip_close_shortcuts: "默认关闭快捷键:macOS 为 ⌘+Q (退出应用) / ⌘+W (关闭标签);Windows / Linux 为 Alt+F4 或 Ctrl+Q (退出应用) / Ctrl+W (关闭标签)。"
|
||||||
|
about_tip_conflict_shortcuts: "潜在快捷键冲突:Linux / Windows 默认修饰键为 Ctrl,应用默认快捷键(如 Ctrl+W 关闭标签、Ctrl+L 切换窗格等)会挡住终端 Shell 的原生快捷键(如 Bash/Zsh 中 Ctrl+W 清除单词、Ctrl+L 清屏)。建议在“设置 -> 快捷键”中改为 Ctrl+Shift+W 或按 Backspace 置空解绑。"
|
||||||
version: "版本"
|
version: "版本"
|
||||||
close: "关闭"
|
close: "关闭"
|
||||||
menu_about_ashell: "关于 Ashell"
|
menu_about_ashell: "关于 Ashell"
|
||||||
|
|||||||
@@ -632,7 +632,8 @@ impl Ashell {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn export_connections(&mut self, window: &mut gpui::Window, cx: &mut Context<Self>) {
|
pub(crate) fn export_connections(&mut self, window: &mut gpui::Window, cx: &mut Context<Self>) {
|
||||||
if self.active_dialog.is_some() {
|
let parent_dialog = self.active_dialog;
|
||||||
|
if parent_dialog.is_some() && parent_dialog != Some(DialogKind::Settings) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -695,7 +696,7 @@ impl Ashell {
|
|||||||
let sessions = sessions_for_export.clone();
|
let sessions = sessions_for_export.clone();
|
||||||
view_for_ok.update(cx, |this, cx| {
|
view_for_ok.update(cx, |this, cx| {
|
||||||
if this.active_dialog == Some(DialogKind::ConnectionExport) {
|
if this.active_dialog == Some(DialogKind::ConnectionExport) {
|
||||||
this.active_dialog = None;
|
this.active_dialog = parent_dialog;
|
||||||
}
|
}
|
||||||
this.start_connection_csv_export(sessions, window, cx);
|
this.start_connection_csv_export(sessions, window, cx);
|
||||||
cx.notify();
|
cx.notify();
|
||||||
@@ -707,7 +708,7 @@ impl Ashell {
|
|||||||
move |_, _, cx| {
|
move |_, _, cx| {
|
||||||
view.update(cx, |this, cx| {
|
view.update(cx, |this, cx| {
|
||||||
if this.active_dialog == Some(DialogKind::ConnectionExport) {
|
if this.active_dialog == Some(DialogKind::ConnectionExport) {
|
||||||
this.active_dialog = None;
|
this.active_dialog = parent_dialog;
|
||||||
}
|
}
|
||||||
cx.notify();
|
cx.notify();
|
||||||
});
|
});
|
||||||
|
|||||||
+91
-45
@@ -729,50 +729,6 @@ impl Ashell {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.child(
|
|
||||||
div()
|
|
||||||
.w_full()
|
|
||||||
.p_2()
|
|
||||||
.rounded_md()
|
|
||||||
.border_1()
|
|
||||||
.border_color(if selected_index == 1 {
|
|
||||||
_cx.theme().primary
|
|
||||||
} else {
|
|
||||||
_cx.theme().border
|
|
||||||
})
|
|
||||||
.bg(if selected_index == 1 {
|
|
||||||
_cx.theme().tab_active
|
|
||||||
} else {
|
|
||||||
_cx.theme().muted
|
|
||||||
})
|
|
||||||
.cursor_pointer()
|
|
||||||
.hover(|this| this.bg(_cx.theme().secondary))
|
|
||||||
.on_mouse_down(
|
|
||||||
MouseButton::Left,
|
|
||||||
window.listener_for(&view, |this, _, window, cx| {
|
|
||||||
this.active_dialog = None;
|
|
||||||
window.close_dialog(cx);
|
|
||||||
this.open_new_ssh_dialog(window, cx);
|
|
||||||
cx.notify();
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
v_flex()
|
|
||||||
.gap_1()
|
|
||||||
.child(
|
|
||||||
div()
|
|
||||||
.text_size(ui_rems(1.0))
|
|
||||||
.font_weight(FontWeight::SEMIBOLD)
|
|
||||||
.child(t!("new_connection")),
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
div()
|
|
||||||
.text_size(ui_rems(0.917))
|
|
||||||
.text_color(_cx.theme().muted_foreground)
|
|
||||||
.child(t!("create_or_edit_ssh_session")),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
.relative()
|
.relative()
|
||||||
@@ -790,7 +746,7 @@ impl Ashell {
|
|||||||
|(ix, session)| {
|
|(ix, session)| {
|
||||||
let connect_id = session.id.clone();
|
let connect_id = session.id.clone();
|
||||||
let is_selected =
|
let is_selected =
|
||||||
selected_index == ix + 2;
|
selected_index == ix + 1;
|
||||||
let name = session.name.clone();
|
let name = session.name.clone();
|
||||||
let detail = if session.protocol
|
let detail = if session.protocol
|
||||||
== "serial"
|
== "serial"
|
||||||
@@ -3285,6 +3241,51 @@ impl Ashell {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
v_flex()
|
||||||
|
.w_full()
|
||||||
|
.mt_2()
|
||||||
|
.p_3()
|
||||||
|
.gap_2()
|
||||||
|
.rounded_md()
|
||||||
|
.bg(cx.theme().secondary.opacity(0.4))
|
||||||
|
.border_1()
|
||||||
|
.border_color(cx.theme().border)
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.text_size(ui_rems(0.85))
|
||||||
|
.font_weight(FontWeight::BOLD)
|
||||||
|
.text_color(cx.theme().foreground)
|
||||||
|
.child(t!("about_tips_title")),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
v_flex()
|
||||||
|
.gap_2()
|
||||||
|
.text_size(ui_rems(0.8))
|
||||||
|
.text_color(cx.theme().muted_foreground)
|
||||||
|
.child(
|
||||||
|
h_flex()
|
||||||
|
.gap_2()
|
||||||
|
.items_start()
|
||||||
|
.child(div().flex_none().child("•"))
|
||||||
|
.child(div().flex_1().min_w(px(0.)).child(t!("about_tip_backspace"))),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
h_flex()
|
||||||
|
.gap_2()
|
||||||
|
.items_start()
|
||||||
|
.child(div().flex_none().child("•"))
|
||||||
|
.child(div().flex_1().min_w(px(0.)).child(t!("about_tip_close_shortcuts"))),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
h_flex()
|
||||||
|
.gap_2()
|
||||||
|
.items_start()
|
||||||
|
.child(div().flex_none().child("•"))
|
||||||
|
.child(div().flex_1().min_w(px(0.)).child(t!("about_tip_conflict_shortcuts"))),
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
@@ -4429,6 +4430,51 @@ impl Ashell {
|
|||||||
let _ = open::that("https://github.com/rust-kotlin/ashell");
|
let _ = open::that("https://github.com/rust-kotlin/ashell");
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
.child(
|
||||||
|
v_flex()
|
||||||
|
.w_full()
|
||||||
|
.mt_3()
|
||||||
|
.p_3()
|
||||||
|
.gap_2()
|
||||||
|
.rounded_md()
|
||||||
|
.bg(cx.theme().secondary.opacity(0.4))
|
||||||
|
.border_1()
|
||||||
|
.border_color(cx.theme().border)
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.text_size(ui_rems(0.85))
|
||||||
|
.font_weight(FontWeight::BOLD)
|
||||||
|
.text_color(cx.theme().foreground)
|
||||||
|
.child(t!("about_tips_title")),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
v_flex()
|
||||||
|
.gap_2()
|
||||||
|
.text_size(ui_rems(0.8))
|
||||||
|
.text_color(cx.theme().muted_foreground)
|
||||||
|
.child(
|
||||||
|
h_flex()
|
||||||
|
.gap_2()
|
||||||
|
.items_start()
|
||||||
|
.child(div().flex_none().child("•"))
|
||||||
|
.child(div().flex_1().min_w(px(0.)).child(t!("about_tip_backspace"))),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
h_flex()
|
||||||
|
.gap_2()
|
||||||
|
.items_start()
|
||||||
|
.child(div().flex_none().child("•"))
|
||||||
|
.child(div().flex_1().min_w(px(0.)).child(t!("about_tip_close_shortcuts"))),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
h_flex()
|
||||||
|
.gap_2()
|
||||||
|
.items_start()
|
||||||
|
.child(div().flex_none().child("•"))
|
||||||
|
.child(div().flex_1().min_w(px(0.)).child(t!("about_tip_conflict_shortcuts"))),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
}))
|
}))
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|||||||
+26
-2
@@ -421,6 +421,7 @@ pub(crate) struct Ashell {
|
|||||||
pub(crate) search_bar_bounds: Option<Bounds<Pixels>>,
|
pub(crate) search_bar_bounds: Option<Bounds<Pixels>>,
|
||||||
|
|
||||||
pub(crate) system_tab_id: Option<String>,
|
pub(crate) system_tab_id: Option<String>,
|
||||||
|
pub(crate) last_applied_system_tab_id: Option<String>,
|
||||||
pub(crate) sftp_handles: std::collections::HashMap<String, crate::sftp::SftpHandle>,
|
pub(crate) sftp_handles: std::collections::HashMap<String, crate::sftp::SftpHandle>,
|
||||||
|
|
||||||
pub(crate) remote_sample_in_flight: bool,
|
pub(crate) remote_sample_in_flight: bool,
|
||||||
@@ -449,7 +450,6 @@ pub(crate) struct HoveredUrl {
|
|||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub(crate) enum SelectorEntry {
|
pub(crate) enum SelectorEntry {
|
||||||
Local,
|
Local,
|
||||||
NewSsh,
|
|
||||||
Saved(String),
|
Saved(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -944,6 +944,7 @@ impl Ashell {
|
|||||||
search_bar_bounds: None,
|
search_bar_bounds: None,
|
||||||
|
|
||||||
system_tab_id: None,
|
system_tab_id: None,
|
||||||
|
last_applied_system_tab_id: None,
|
||||||
sftp_handles: std::collections::HashMap::new(),
|
sftp_handles: std::collections::HashMap::new(),
|
||||||
|
|
||||||
remote_sample_in_flight: false,
|
remote_sample_in_flight: false,
|
||||||
@@ -1283,7 +1284,11 @@ impl Ashell {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn on_window_activation_changed(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
fn on_window_activation_changed(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
let was_active = self.window_active;
|
||||||
self.sync_window_activation(window);
|
self.sync_window_activation(window);
|
||||||
|
if !was_active && self.window_active {
|
||||||
|
window.activate_window();
|
||||||
|
}
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1390,6 +1395,7 @@ impl Ashell {
|
|||||||
tab.disconnected_reason = None;
|
tab.disconnected_reason = None;
|
||||||
}
|
}
|
||||||
self.sync_sftp_path_from_terminal_title(&tab_id, cx);
|
self.sync_sftp_path_from_terminal_title(&tab_id, cx);
|
||||||
|
self.sync_sftp_to_active_tab();
|
||||||
self.sync_system_tab_to_active_group();
|
self.sync_system_tab_to_active_group();
|
||||||
if self.system_tab_id.as_deref() == Some(tab_id.as_str()) {
|
if self.system_tab_id.as_deref() == Some(tab_id.as_str()) {
|
||||||
self.system_status = None;
|
self.system_status = None;
|
||||||
@@ -1488,6 +1494,12 @@ impl Ashell {
|
|||||||
if self.is_connected_system_tab(&tab_id) {
|
if self.is_connected_system_tab(&tab_id) {
|
||||||
self.remote_sample_in_flight = false;
|
self.remote_sample_in_flight = false;
|
||||||
self.system_status = None;
|
self.system_status = None;
|
||||||
|
if self.last_applied_system_tab_id.as_deref() != Some(&tab_id) {
|
||||||
|
self.last_applied_system_tab_id = Some(tab_id.clone());
|
||||||
|
self.cpu_history.clear();
|
||||||
|
self.net_rx_history.clear();
|
||||||
|
self.net_tx_history.clear();
|
||||||
|
}
|
||||||
self.apply_system_snapshot(snapshot);
|
self.apply_system_snapshot(snapshot);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1496,6 +1508,11 @@ impl Ashell {
|
|||||||
self.remote_sample_in_flight = false;
|
self.remote_sample_in_flight = false;
|
||||||
self.system_status = Some(reason.clone().into());
|
self.system_status = Some(reason.clone().into());
|
||||||
self.status = reason.into();
|
self.status = reason.into();
|
||||||
|
self.last_applied_system_tab_id = Some(tab_id);
|
||||||
|
self.system = SystemSnapshot::default();
|
||||||
|
self.cpu_history.clear();
|
||||||
|
self.net_rx_history.clear();
|
||||||
|
self.net_tx_history.clear();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
BackendEvent::RemoteProcesses { tab_id, processes } => {
|
BackendEvent::RemoteProcesses { tab_id, processes } => {
|
||||||
@@ -1565,7 +1582,7 @@ impl Ashell {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if let Some(tab) = self.tabs.iter_mut().find(|t| t.id == tab_id) {
|
if let Some(tab) = self.tabs.iter_mut().find(|t| t.id == tab_id) {
|
||||||
tab.clear_command_activity();
|
tab.clear_terminal_activity();
|
||||||
tab.connected = false;
|
tab.connected = false;
|
||||||
tab.status = reason.clone();
|
tab.status = reason.clone();
|
||||||
tab.disconnected_reason = Some(reason.clone());
|
tab.disconnected_reason = Some(reason.clone());
|
||||||
@@ -1734,6 +1751,12 @@ impl Ashell {
|
|||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
if self.last_applied_system_tab_id.is_some() {
|
||||||
|
self.last_applied_system_tab_id = None;
|
||||||
|
self.cpu_history.clear();
|
||||||
|
self.net_rx_history.clear();
|
||||||
|
self.net_tx_history.clear();
|
||||||
|
}
|
||||||
let snapshot = self.system_sampler.sample();
|
let snapshot = self.system_sampler.sample();
|
||||||
self.apply_system_snapshot(snapshot);
|
self.apply_system_snapshot(snapshot);
|
||||||
return true;
|
return true;
|
||||||
@@ -1760,6 +1783,7 @@ impl Ashell {
|
|||||||
self.cpu_history.clear();
|
self.cpu_history.clear();
|
||||||
self.net_rx_history.clear();
|
self.net_rx_history.clear();
|
||||||
self.net_tx_history.clear();
|
self.net_tx_history.clear();
|
||||||
|
self.last_applied_system_tab_id = None;
|
||||||
self.remote_sample_in_flight = false;
|
self.remote_sample_in_flight = false;
|
||||||
self.remote_processes_in_flight = false;
|
self.remote_processes_in_flight = false;
|
||||||
self.remote_processes.clear();
|
self.remote_processes.clear();
|
||||||
|
|||||||
+270
-117
@@ -1884,10 +1884,26 @@ impl Ashell {
|
|||||||
};
|
};
|
||||||
let mem_pct = self.system.mem_percent;
|
let mem_pct = self.system.mem_percent;
|
||||||
let swap_pct = self.system.swap_percent;
|
let swap_pct = self.system.swap_percent;
|
||||||
let mem_detail = self.system.mem_detail.clone();
|
let mem_detail = if self.system.mem_detail.is_empty() {
|
||||||
let swap_detail = self.system.swap_detail.clone();
|
"-- / --".to_string()
|
||||||
let net_rx = self.system.net_rx.clone();
|
} else {
|
||||||
let net_tx = self.system.net_tx.clone();
|
self.system.mem_detail.clone()
|
||||||
|
};
|
||||||
|
let swap_detail = if self.system.swap_detail.is_empty() {
|
||||||
|
"-- / --".to_string()
|
||||||
|
} else {
|
||||||
|
self.system.swap_detail.clone()
|
||||||
|
};
|
||||||
|
let net_rx = if self.system.net_rx.is_empty() {
|
||||||
|
"--".to_string()
|
||||||
|
} else {
|
||||||
|
self.system.net_rx.clone()
|
||||||
|
};
|
||||||
|
let net_tx = if self.system.net_tx.is_empty() {
|
||||||
|
"--".to_string()
|
||||||
|
} else {
|
||||||
|
self.system.net_tx.clone()
|
||||||
|
};
|
||||||
|
|
||||||
let (disk_used, disk_total) = self.system.disks.iter().fold((0u64, 0u64), |(u, t), d| {
|
let (disk_used, disk_total) = self.system.disks.iter().fold((0u64, 0u64), |(u, t), d| {
|
||||||
(u + (d.total_bytes - d.available_bytes), t + d.total_bytes)
|
(u + (d.total_bytes - d.available_bytes), t + d.total_bytes)
|
||||||
@@ -2208,7 +2224,11 @@ impl Ashell {
|
|||||||
div()
|
div()
|
||||||
.text_size(ui_rems(0.833))
|
.text_size(ui_rems(0.833))
|
||||||
.text_color(muted_fg)
|
.text_color(muted_fg)
|
||||||
.child(format!("{:.0}%", disk_pct)),
|
.child(if disk_total > 0 {
|
||||||
|
format!("{:.0}%", disk_pct)
|
||||||
|
} else {
|
||||||
|
"--".to_string()
|
||||||
|
}),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
@@ -2222,40 +2242,72 @@ impl Ashell {
|
|||||||
.track_scroll(&self.disk_scroll_handle)
|
.track_scroll(&self.disk_scroll_handle)
|
||||||
.overflow_y_scroll()
|
.overflow_y_scroll()
|
||||||
.size_full()
|
.size_full()
|
||||||
.children(disks.iter().map(|disk| {
|
.children(if disks.is_empty() {
|
||||||
let pct = if disk.total_bytes > 0 {
|
vec![
|
||||||
(disk.total_bytes - disk.available_bytes) as f64
|
h_flex()
|
||||||
/ disk.total_bytes as f64
|
.w_full()
|
||||||
* 100.0
|
.items_center()
|
||||||
} else {
|
.gap_1()
|
||||||
0.0
|
.child(
|
||||||
};
|
div()
|
||||||
let mount_short = disk.mount.clone();
|
.w(px(20.))
|
||||||
let mount_id = format!("disk-{}", mount_short);
|
.h(px(8.))
|
||||||
h_flex()
|
.rounded_sm()
|
||||||
.w_full()
|
.bg(cx.theme().skeleton),
|
||||||
.items_center()
|
)
|
||||||
.gap_1()
|
.child(
|
||||||
.child(
|
div()
|
||||||
div()
|
.flex_1()
|
||||||
.text_size(ui_rems(0.667))
|
.h(px(4.))
|
||||||
.text_color(muted_fg)
|
.rounded_full()
|
||||||
.child(mount_short),
|
.bg(cx.theme().skeleton.opacity(0.4)),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
Progress::new(mount_id)
|
div()
|
||||||
.value(pct as f32)
|
.w(px(22.))
|
||||||
.color(disk_color)
|
.h(px(8.))
|
||||||
.with_size(px(4.))
|
.rounded_sm()
|
||||||
.flex_1(),
|
.bg(cx.theme().skeleton.opacity(0.7)),
|
||||||
)
|
)
|
||||||
.child(
|
.into_any_element(),
|
||||||
div()
|
]
|
||||||
.text_size(ui_rems(0.667))
|
} else {
|
||||||
.text_color(muted_fg)
|
disks.iter().map(|disk| {
|
||||||
.child(format!("{:.0}%", pct)),
|
let pct = if disk.total_bytes > 0 {
|
||||||
)
|
(disk.total_bytes - disk.available_bytes) as f64
|
||||||
})),
|
/ disk.total_bytes as f64
|
||||||
|
* 100.0
|
||||||
|
} else {
|
||||||
|
0.0
|
||||||
|
};
|
||||||
|
let mount_short = disk.mount.clone();
|
||||||
|
let mount_id = format!("disk-{}", mount_short);
|
||||||
|
h_flex()
|
||||||
|
.w_full()
|
||||||
|
.items_center()
|
||||||
|
.gap_1()
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.text_size(ui_rems(0.667))
|
||||||
|
.text_color(muted_fg)
|
||||||
|
.child(mount_short),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
Progress::new(mount_id)
|
||||||
|
.value(pct as f32)
|
||||||
|
.color(disk_color)
|
||||||
|
.with_size(px(4.))
|
||||||
|
.flex_1(),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.text_size(ui_rems(0.667))
|
||||||
|
.text_color(muted_fg)
|
||||||
|
.child(format!("{:.0}%", pct)),
|
||||||
|
)
|
||||||
|
.into_any_element()
|
||||||
|
}).collect()
|
||||||
|
}),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
@@ -2313,7 +2365,32 @@ impl Ashell {
|
|||||||
let disk_color = cx.theme().chart_5;
|
let disk_color = cx.theme().chart_5;
|
||||||
let net_color = cx.theme().chart_4;
|
let net_color = cx.theme().chart_4;
|
||||||
let muted_fg = cx.theme().muted_foreground;
|
let muted_fg = cx.theme().muted_foreground;
|
||||||
|
let mem_detail = if self.system.mem_detail.is_empty() {
|
||||||
|
"-- / --".to_string()
|
||||||
|
} else {
|
||||||
|
self.system.mem_detail.clone()
|
||||||
|
};
|
||||||
|
let swap_detail = if self.system.swap_detail.is_empty() {
|
||||||
|
"-- / --".to_string()
|
||||||
|
} else {
|
||||||
|
self.system.swap_detail.clone()
|
||||||
|
};
|
||||||
|
let net_rx = if self.system.net_rx.is_empty() {
|
||||||
|
"--".to_string()
|
||||||
|
} else {
|
||||||
|
self.system.net_rx.clone()
|
||||||
|
};
|
||||||
|
let net_tx = if self.system.net_tx.is_empty() {
|
||||||
|
"--".to_string()
|
||||||
|
} else {
|
||||||
|
self.system.net_tx.clone()
|
||||||
|
};
|
||||||
let active_is_ssh = matches!(self.active_kind(), Some(TabKind::Ssh));
|
let active_is_ssh = matches!(self.active_kind(), Some(TabKind::Ssh));
|
||||||
|
let is_connecting_ssh = self
|
||||||
|
.active_tab
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|active_id| self.tabs.iter().find(|tab| tab.id == *active_id))
|
||||||
|
.is_some_and(|tab| tab.kind == TabKind::Ssh && !tab.connected && tab.disconnected_reason.is_none());
|
||||||
let (monitor_title, monitor_detail) = self
|
let (monitor_title, monitor_detail) = self
|
||||||
.active_tab
|
.active_tab
|
||||||
.as_ref()
|
.as_ref()
|
||||||
@@ -2323,6 +2400,8 @@ impl Ashell {
|
|||||||
.unwrap_or_else(|| (t!("system_info").to_string(), t!("live").to_string()));
|
.unwrap_or_else(|| (t!("system_info").to_string(), t!("live").to_string()));
|
||||||
let status_color = if interactive {
|
let status_color = if interactive {
|
||||||
cx.theme().success
|
cx.theme().success
|
||||||
|
} else if is_connecting_ssh {
|
||||||
|
cx.theme().warning
|
||||||
} else if active_is_ssh {
|
} else if active_is_ssh {
|
||||||
cx.theme().danger
|
cx.theme().danger
|
||||||
} else {
|
} else {
|
||||||
@@ -2448,7 +2527,7 @@ impl Ashell {
|
|||||||
div()
|
div()
|
||||||
.text_size(ui_rems(0.85))
|
.text_size(ui_rems(0.85))
|
||||||
.text_color(muted_fg)
|
.text_color(muted_fg)
|
||||||
.child(self.system.mem_detail.clone()),
|
.child(mem_detail),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
@@ -2475,7 +2554,7 @@ impl Ashell {
|
|||||||
div()
|
div()
|
||||||
.text_size(ui_rems(0.85))
|
.text_size(ui_rems(0.85))
|
||||||
.text_color(muted_fg)
|
.text_color(muted_fg)
|
||||||
.child(self.system.swap_detail.clone()),
|
.child(swap_detail),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
@@ -2514,49 +2593,146 @@ impl Ashell {
|
|||||||
div()
|
div()
|
||||||
.relative()
|
.relative()
|
||||||
.w_full()
|
.w_full()
|
||||||
|
.h(px(90.))
|
||||||
.child(
|
.child(
|
||||||
v_flex()
|
v_flex()
|
||||||
.id("sidebar-disk-scroll")
|
.id("sidebar-disk-scroll")
|
||||||
.track_scroll(&self.disk_scroll_handle)
|
.track_scroll(&self.disk_scroll_handle)
|
||||||
.overflow_y_scroll()
|
.overflow_y_scroll()
|
||||||
.max_h(px(90.))
|
.size_full()
|
||||||
.gap_2()
|
.gap_2()
|
||||||
.children(self.system.disks.iter().map(|disk| {
|
.children(if self.system.disks.is_empty() {
|
||||||
let pct = if disk.total_bytes > 0 {
|
vec![
|
||||||
(disk.total_bytes - disk.available_bytes) as f64
|
v_flex()
|
||||||
/ disk.total_bytes as f64
|
.gap_0p5()
|
||||||
* 100.0
|
.child(
|
||||||
} else {
|
h_flex()
|
||||||
0.0
|
.justify_between()
|
||||||
};
|
.items_center()
|
||||||
let mount_short = disk.mount.clone();
|
.h(px(14.))
|
||||||
let mount_id = format!("sidebar-disk-{}", mount_short);
|
.child(
|
||||||
v_flex()
|
div()
|
||||||
.gap_0p5()
|
.w(px(44.))
|
||||||
.child(
|
.h(px(10.))
|
||||||
h_flex()
|
.rounded_sm()
|
||||||
.justify_between()
|
.bg(cx.theme().skeleton),
|
||||||
.child(
|
)
|
||||||
div()
|
.child(
|
||||||
.text_size(ui_rems(0.75))
|
div()
|
||||||
.text_color(muted_fg)
|
.w(px(28.))
|
||||||
.child(mount_short),
|
.h(px(10.))
|
||||||
)
|
.rounded_sm()
|
||||||
.child(
|
.bg(cx.theme().skeleton.opacity(0.7)),
|
||||||
div()
|
),
|
||||||
.text_size(ui_rems(0.75))
|
)
|
||||||
.text_color(muted_fg)
|
.child(
|
||||||
.child(format!("{:.1}%", pct)),
|
div()
|
||||||
),
|
.w_full()
|
||||||
)
|
.h(px(4.))
|
||||||
.child(
|
.rounded_full()
|
||||||
Progress::new(mount_id)
|
.bg(cx.theme().skeleton.opacity(0.4)),
|
||||||
.value(pct as f32)
|
)
|
||||||
.color(disk_color)
|
.into_any_element(),
|
||||||
.with_size(px(4.))
|
v_flex()
|
||||||
.w_full(),
|
.gap_0p5()
|
||||||
)
|
.child(
|
||||||
})),
|
h_flex()
|
||||||
|
.justify_between()
|
||||||
|
.items_center()
|
||||||
|
.h(px(14.))
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.w(px(64.))
|
||||||
|
.h(px(10.))
|
||||||
|
.rounded_sm()
|
||||||
|
.bg(cx.theme().skeleton),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.w(px(32.))
|
||||||
|
.h(px(10.))
|
||||||
|
.rounded_sm()
|
||||||
|
.bg(cx.theme().skeleton.opacity(0.7)),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.w_full()
|
||||||
|
.h(px(4.))
|
||||||
|
.rounded_full()
|
||||||
|
.bg(cx.theme().skeleton.opacity(0.4)),
|
||||||
|
)
|
||||||
|
.into_any_element(),
|
||||||
|
v_flex()
|
||||||
|
.gap_0p5()
|
||||||
|
.child(
|
||||||
|
h_flex()
|
||||||
|
.justify_between()
|
||||||
|
.items_center()
|
||||||
|
.h(px(14.))
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.w(px(36.))
|
||||||
|
.h(px(10.))
|
||||||
|
.rounded_sm()
|
||||||
|
.bg(cx.theme().skeleton),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.w(px(26.))
|
||||||
|
.h(px(10.))
|
||||||
|
.rounded_sm()
|
||||||
|
.bg(cx.theme().skeleton.opacity(0.7)),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.w_full()
|
||||||
|
.h(px(4.))
|
||||||
|
.rounded_full()
|
||||||
|
.bg(cx.theme().skeleton.opacity(0.4)),
|
||||||
|
)
|
||||||
|
.into_any_element(),
|
||||||
|
]
|
||||||
|
} else {
|
||||||
|
self.system.disks.iter().map(|disk| {
|
||||||
|
let pct = if disk.total_bytes > 0 {
|
||||||
|
(disk.total_bytes - disk.available_bytes) as f64
|
||||||
|
/ disk.total_bytes as f64
|
||||||
|
* 100.0
|
||||||
|
} else {
|
||||||
|
0.0
|
||||||
|
};
|
||||||
|
let mount_short = disk.mount.clone();
|
||||||
|
let mount_id = format!("sidebar-disk-{}", mount_short);
|
||||||
|
v_flex()
|
||||||
|
.gap_0p5()
|
||||||
|
.child(
|
||||||
|
h_flex()
|
||||||
|
.justify_between()
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.text_size(ui_rems(0.75))
|
||||||
|
.text_color(muted_fg)
|
||||||
|
.child(mount_short),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.text_size(ui_rems(0.75))
|
||||||
|
.text_color(muted_fg)
|
||||||
|
.child(format!("{:.1}%", pct)),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
Progress::new(mount_id)
|
||||||
|
.value(pct as f32)
|
||||||
|
.color(disk_color)
|
||||||
|
.with_size(px(4.))
|
||||||
|
.w_full(),
|
||||||
|
)
|
||||||
|
.into_any_element()
|
||||||
|
}).collect()
|
||||||
|
}),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
@@ -2621,7 +2797,7 @@ impl Ashell {
|
|||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
.text_size(ui_rems(0.75))
|
.text_size(ui_rems(0.75))
|
||||||
.child(self.system.net_rx.clone()),
|
.child(net_rx),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
@@ -2639,7 +2815,7 @@ impl Ashell {
|
|||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
.text_size(ui_rems(0.75))
|
.text_size(ui_rems(0.75))
|
||||||
.child(self.system.net_tx.clone()),
|
.child(net_tx),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -3336,7 +3512,6 @@ impl Ashell {
|
|||||||
active_session_id: Option<&str>,
|
active_session_id: Option<&str>,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) -> gpui::AnyElement {
|
) -> gpui::AnyElement {
|
||||||
let connect_id = session.id.clone();
|
|
||||||
let edit_id = session.id.clone();
|
let edit_id = session.id.clone();
|
||||||
let delete_id = session.id.clone();
|
let delete_id = session.id.clone();
|
||||||
let management_mode = self.connection_management_mode;
|
let management_mode = self.connection_management_mode;
|
||||||
@@ -3347,8 +3522,6 @@ impl Ashell {
|
|||||||
let selection_id = session.id.clone();
|
let selection_id = session.id.clone();
|
||||||
let row_selection_id = selection_id.clone();
|
let row_selection_id = selection_id.clone();
|
||||||
let row_id = ElementId::Name(format!("saved-connect-{}", session.id).into());
|
let row_id = ElementId::Name(format!("saved-connect-{}", session.id).into());
|
||||||
let connect_button_id =
|
|
||||||
ElementId::Name(format!("connect-saved-session-{}", session.id).into());
|
|
||||||
|
|
||||||
div()
|
div()
|
||||||
.id(row_id)
|
.id(row_id)
|
||||||
@@ -3477,33 +3650,7 @@ impl Ashell {
|
|||||||
.text_size(ui_rems(0.75))
|
.text_size(ui_rems(0.75))
|
||||||
.text_color(cx.theme().muted_foreground)
|
.text_color(cx.theme().muted_foreground)
|
||||||
.child(detail),
|
.child(detail),
|
||||||
)
|
),
|
||||||
.when(management_mode, |this| {
|
|
||||||
this.child(
|
|
||||||
h_flex()
|
|
||||||
.w(px(24.))
|
|
||||||
.ml_1()
|
|
||||||
.flex_none()
|
|
||||||
.items_center()
|
|
||||||
.justify_center()
|
|
||||||
.on_mouse_down(MouseButton::Left, |_, _, cx| {
|
|
||||||
cx.stop_propagation();
|
|
||||||
})
|
|
||||||
.on_mouse_down(MouseButton::Right, |_, _, cx| {
|
|
||||||
cx.stop_propagation();
|
|
||||||
})
|
|
||||||
.child(
|
|
||||||
pointer_button(connect_button_id)
|
|
||||||
.ghost()
|
|
||||||
.small()
|
|
||||||
.icon(IconName::ExternalLink)
|
|
||||||
.tooltip(t!("connect").to_string())
|
|
||||||
.on_click(cx.listener(move |this, _, window, cx| {
|
|
||||||
this.connect_saved_session(connect_id.clone(), window, cx);
|
|
||||||
})),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}),
|
|
||||||
)
|
)
|
||||||
.into_any_element()
|
.into_any_element()
|
||||||
}
|
}
|
||||||
@@ -4380,12 +4527,13 @@ impl Ashell {
|
|||||||
pane_ids.iter().any(|id| {
|
pane_ids.iter().any(|id| {
|
||||||
self.unread_terminal_notifications.contains(id)
|
self.unread_terminal_notifications.contains(id)
|
||||||
});
|
});
|
||||||
let output_active = pane_ids.iter().any(|id| {
|
let output_active = ix != selected
|
||||||
self.tabs
|
&& pane_ids.iter().any(|id| {
|
||||||
.iter()
|
self.tabs
|
||||||
.find(|tab| tab.id == *id)
|
.iter()
|
||||||
.is_some_and(TerminalTab::is_command_active)
|
.find(|tab| tab.id == *id)
|
||||||
});
|
.is_some_and(TerminalTab::has_recent_output)
|
||||||
|
});
|
||||||
h_flex()
|
h_flex()
|
||||||
.id(("ashell-tab", ix))
|
.id(("ashell-tab", ix))
|
||||||
.relative()
|
.relative()
|
||||||
@@ -5339,7 +5487,12 @@ impl Render for Ashell {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let has_ssh_session = self.active_ssh_session().is_some();
|
let has_ssh_session = self.active_ssh_session().is_some()
|
||||||
|
|| self
|
||||||
|
.active_group
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|id| self.tab_groups.iter().find(|g| &g.id == id))
|
||||||
|
.is_some_and(|g| g.sftp.is_some());
|
||||||
let is_monitor_bottom = self.config.monitoring_position() == "Bottom";
|
let is_monitor_bottom = self.config.monitoring_position() == "Bottom";
|
||||||
let is_active_ssh_connected = self
|
let is_active_ssh_connected = self
|
||||||
.active_tab
|
.active_tab
|
||||||
|
|||||||
+6
-14
@@ -409,9 +409,6 @@ fn default_terminal_font_size() -> f32 {
|
|||||||
12.0
|
12.0
|
||||||
}
|
}
|
||||||
|
|
||||||
const LEGACY_DEFAULT_TERMINAL_FONT_SIZE: f32 = 18.0;
|
|
||||||
const LEGACY_DEFAULT_UI_FONT_SIZE: f32 = 16.0;
|
|
||||||
|
|
||||||
fn default_ui_font_size() -> f32 {
|
fn default_ui_font_size() -> f32 {
|
||||||
14.0
|
14.0
|
||||||
}
|
}
|
||||||
@@ -1167,9 +1164,7 @@ impl ConfigStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
||||||
|| (self.cache.terminal_font_size - LEGACY_DEFAULT_TERMINAL_FONT_SIZE).abs() < 0.01
|
|
||||||
{
|
|
||||||
default_terminal_font_size()
|
default_terminal_font_size()
|
||||||
} else {
|
} else {
|
||||||
self.cache.terminal_font_size
|
self.cache.terminal_font_size
|
||||||
@@ -1287,10 +1282,7 @@ impl ConfigStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn ui_font_size(&self) -> f32 {
|
pub fn ui_font_size(&self) -> f32 {
|
||||||
if self.cache.ui_font_size <= 0.0
|
if self.cache.ui_font_size <= 0.0 {
|
||||||
|| (self.cache.ui_font_size - LEGACY_DEFAULT_UI_FONT_SIZE).abs() < 0.01
|
|
||||||
{
|
|
||||||
// Migrate the previous application default to the current compact default.
|
|
||||||
default_ui_font_size()
|
default_ui_font_size()
|
||||||
} else {
|
} else {
|
||||||
self.cache.ui_font_size
|
self.cache.ui_font_size
|
||||||
@@ -1942,17 +1934,17 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn font_sizes_migrate_the_previous_defaults() {
|
fn font_sizes_preserve_user_configurations() {
|
||||||
let mut store = ConfigStore::in_memory();
|
let mut store = ConfigStore::in_memory();
|
||||||
|
|
||||||
assert_eq!(store.terminal_font_size(), 12.0);
|
assert_eq!(store.terminal_font_size(), 12.0);
|
||||||
assert_eq!(store.ui_font_size(), 14.0);
|
assert_eq!(store.ui_font_size(), 14.0);
|
||||||
|
|
||||||
store.set_terminal_font_size(18.0);
|
store.set_terminal_font_size(18.0);
|
||||||
assert_eq!(store.terminal_font_size(), 12.0);
|
assert_eq!(store.terminal_font_size(), 18.0);
|
||||||
|
|
||||||
store.set_ui_font_size(14.0);
|
store.set_ui_font_size(16.0);
|
||||||
assert_eq!(store.ui_font_size(), 14.0);
|
assert_eq!(store.ui_font_size(), 16.0);
|
||||||
|
|
||||||
store.set_ui_font_size(20.0);
|
store.set_ui_font_size(20.0);
|
||||||
assert_eq!(store.ui_font_size(), 20.0);
|
assert_eq!(store.ui_font_size(), 20.0);
|
||||||
|
|||||||
+150
-30
@@ -1267,7 +1267,7 @@ impl Ashell {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn selector_entries(&self) -> Vec<SelectorEntry> {
|
pub(crate) fn selector_entries(&self) -> Vec<SelectorEntry> {
|
||||||
let mut entries = vec![SelectorEntry::Local, SelectorEntry::NewSsh];
|
let mut entries = vec![SelectorEntry::Local];
|
||||||
entries.extend(
|
entries.extend(
|
||||||
self.config
|
self.config
|
||||||
.sessions()
|
.sessions()
|
||||||
@@ -1278,11 +1278,7 @@ impl Ashell {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn default_selector_index(&self) -> usize {
|
pub(crate) fn default_selector_index(&self) -> usize {
|
||||||
if self.config.sessions().is_empty() {
|
0
|
||||||
0
|
|
||||||
} else {
|
|
||||||
2
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn move_selector_selection(&mut self, delta: i32, cx: &mut Context<Self>) {
|
pub(crate) fn move_selector_selection(&mut self, delta: i32, cx: &mut Context<Self>) {
|
||||||
@@ -1294,8 +1290,8 @@ impl Ashell {
|
|||||||
let next = (current + delta).clamp(0, entries.len() as i32 - 1) as usize;
|
let next = (current + delta).clamp(0, entries.len() as i32 - 1) as usize;
|
||||||
if next != self.selector_selection {
|
if next != self.selector_selection {
|
||||||
self.selector_selection = next;
|
self.selector_selection = next;
|
||||||
if next >= 2 {
|
if next >= 1 {
|
||||||
self.selector_scroll_handle.scroll_to_item(next - 2);
|
self.selector_scroll_handle.scroll_to_item(next - 1);
|
||||||
}
|
}
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
@@ -1317,10 +1313,6 @@ impl Ashell {
|
|||||||
self.open_local(cx);
|
self.open_local(cx);
|
||||||
window.close_dialog(cx);
|
window.close_dialog(cx);
|
||||||
}
|
}
|
||||||
SelectorEntry::NewSsh => {
|
|
||||||
window.close_dialog(cx);
|
|
||||||
self.open_new_ssh_dialog(window, cx);
|
|
||||||
}
|
|
||||||
SelectorEntry::Saved(session_id) => {
|
SelectorEntry::Saved(session_id) => {
|
||||||
self.connect_saved_session(session_id, window, cx);
|
self.connect_saved_session(session_id, window, cx);
|
||||||
window.close_dialog(cx);
|
window.close_dialog(cx);
|
||||||
@@ -2111,12 +2103,28 @@ impl Ashell {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn active_ssh_session(&self) -> Option<(String, Session)> {
|
pub(crate) fn active_ssh_session(&self) -> Option<(String, Session)> {
|
||||||
let active_id = self.active_tab.as_ref()?;
|
if let Some(active_id) = self.active_tab.as_ref() {
|
||||||
let tab = self.tabs.iter().find(|tab| &tab.id == active_id)?;
|
if let Some(tab) = self.tabs.iter().find(|tab| &tab.id == active_id) {
|
||||||
if !tab.connected {
|
if tab.kind == TabKind::Ssh {
|
||||||
return None;
|
if let Some(session) = tab.session.clone() {
|
||||||
|
return Some((tab.id.clone(), session));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Some((tab.id.clone(), tab.session.clone()?))
|
if let Some(group_id) = self.active_group.as_ref() {
|
||||||
|
if let Some(group) = self.tab_groups.iter().find(|g| &g.id == group_id) {
|
||||||
|
let pane_tab_ids = group.pane_root.tab_ids();
|
||||||
|
for tab in &self.tabs {
|
||||||
|
if pane_tab_ids.contains(&tab.id.as_str()) && tab.kind == TabKind::Ssh {
|
||||||
|
if let Some(session) = tab.session.clone() {
|
||||||
|
return Some((tab.id.clone(), session));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn active_session_id(&self) -> Option<&str> {
|
pub(crate) fn active_session_id(&self) -> Option<&str> {
|
||||||
@@ -2293,9 +2301,6 @@ impl Ashell {
|
|||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) {
|
) {
|
||||||
if self.focused_pane_path.is_empty() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let previous_active_tab = self.active_tab.clone();
|
let previous_active_tab = self.active_tab.clone();
|
||||||
let mut active_tab_changed = false;
|
let mut active_tab_changed = false;
|
||||||
let path = self.focused_pane_path.clone();
|
let path = self.focused_pane_path.clone();
|
||||||
@@ -2319,6 +2324,28 @@ impl Ashell {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
cx.notify();
|
cx.notify();
|
||||||
|
} else if (direction == "left" || direction == "right") && self.tab_groups.len() > 1 {
|
||||||
|
if let Some(current_group_id) = self.active_group.clone() {
|
||||||
|
if let Some(current_idx) = self
|
||||||
|
.tab_groups
|
||||||
|
.iter()
|
||||||
|
.position(|g| g.id == current_group_id)
|
||||||
|
{
|
||||||
|
let total_groups = self.tab_groups.len();
|
||||||
|
let target_idx = if direction == "left" {
|
||||||
|
if current_idx > 0 {
|
||||||
|
current_idx - 1
|
||||||
|
} else {
|
||||||
|
total_groups - 1
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
(current_idx + 1) % total_groups
|
||||||
|
};
|
||||||
|
let target_group_id = self.tab_groups[target_idx].id.clone();
|
||||||
|
self.activate_group(target_group_id, window, cx);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if active_tab_changed {
|
if active_tab_changed {
|
||||||
self.update_terminal_focus(previous_active_tab.as_deref());
|
self.update_terminal_focus(previous_active_tab.as_deref());
|
||||||
@@ -2446,7 +2473,13 @@ impl Ashell {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Load new group state
|
// Load new group state
|
||||||
if let Some(group) = self.tab_groups.iter().find(|g| g.id == group_id) {
|
if let Some((index, group)) = self
|
||||||
|
.tab_groups
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.find(|(_, g)| g.id == group_id)
|
||||||
|
{
|
||||||
|
self.tabs_scroll_handle.scroll_to_item(index);
|
||||||
self.pane_root = group.pane_root.clone();
|
self.pane_root = group.pane_root.clone();
|
||||||
self.active_group = Some(group_id);
|
self.active_group = Some(group_id);
|
||||||
if let Some(first_id) = group.pane_root.first_tab_id() {
|
if let Some(first_id) = group.pane_root.first_tab_id() {
|
||||||
@@ -2511,7 +2544,7 @@ impl Ashell {
|
|||||||
let Some(group_id) = self.active_group.clone() else {
|
let Some(group_id) = self.active_group.clone() else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let target = self.active_tab.as_ref().and_then(|active_id| {
|
let mut target = self.active_tab.as_ref().and_then(|active_id| {
|
||||||
self.tabs
|
self.tabs
|
||||||
.iter()
|
.iter()
|
||||||
.find(|tab| {
|
.find(|tab| {
|
||||||
@@ -2519,12 +2552,49 @@ impl Ashell {
|
|||||||
})
|
})
|
||||||
.and_then(|tab| tab.session.clone().map(|session| (tab.id.clone(), session)))
|
.and_then(|tab| tab.session.clone().map(|session| (tab.id.clone(), session)))
|
||||||
});
|
});
|
||||||
let target_tab_id = target.as_ref().map(|(tab_id, _)| tab_id.as_str());
|
|
||||||
|
if target.is_none() {
|
||||||
|
if let Some(group) = self.tab_groups.iter().find(|g| g.id == group_id) {
|
||||||
|
let pane_tab_ids = group.pane_root.tab_ids();
|
||||||
|
target = self
|
||||||
|
.tabs
|
||||||
|
.iter()
|
||||||
|
.find(|tab| {
|
||||||
|
pane_tab_ids.contains(&tab.id.as_str())
|
||||||
|
&& tab.kind == TabKind::Ssh
|
||||||
|
&& (tab.connected || force)
|
||||||
|
})
|
||||||
|
.and_then(|tab| tab.session.clone().map(|session| (tab.id.clone(), session)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let current_tab_id = self
|
let current_tab_id = self
|
||||||
.tab_groups
|
.tab_groups
|
||||||
.iter()
|
.iter()
|
||||||
.find(|group| group.id == group_id)
|
.find(|group| group.id == group_id)
|
||||||
.and_then(|group| group.sftp_tab_id.clone());
|
.and_then(|group| group.sftp_tab_id.clone());
|
||||||
|
|
||||||
|
let group_has_ssh = self
|
||||||
|
.tab_groups
|
||||||
|
.iter()
|
||||||
|
.find(|g| g.id == group_id)
|
||||||
|
.map(|g| {
|
||||||
|
let pane_tab_ids = g.pane_root.tab_ids();
|
||||||
|
self.tabs
|
||||||
|
.iter()
|
||||||
|
.any(|t| pane_tab_ids.contains(&t.id.as_str()) && t.kind == TabKind::Ssh)
|
||||||
|
})
|
||||||
|
.unwrap_or(false);
|
||||||
|
|
||||||
|
if target.is_none()
|
||||||
|
&& !force
|
||||||
|
&& group_has_ssh
|
||||||
|
&& (self.sftp_handles.contains_key(&group_id) || current_tab_id.is_some())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let target_tab_id = target.as_ref().map(|(tab_id, _)| tab_id.as_str());
|
||||||
let current_session_id = current_tab_id.as_ref().and_then(|tab_id| {
|
let current_session_id = current_tab_id.as_ref().and_then(|tab_id| {
|
||||||
self.tabs
|
self.tabs
|
||||||
.iter()
|
.iter()
|
||||||
@@ -2535,7 +2605,7 @@ impl Ashell {
|
|||||||
let target_session_id = target.as_ref().map(|(_, session)| session.id.as_str());
|
let target_session_id = target.as_ref().map(|(_, session)| session.id.as_str());
|
||||||
|
|
||||||
if !force {
|
if !force {
|
||||||
if current_tab_id.as_deref() == target_tab_id {
|
if current_tab_id.as_deref() == target_tab_id && target_tab_id.is_some() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if current_session_id.as_deref() == target_session_id
|
if current_session_id.as_deref() == target_session_id
|
||||||
@@ -2580,12 +2650,51 @@ impl Ashell {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn sync_system_tab_to_active_group(&mut self) {
|
pub(crate) fn sync_system_tab_to_active_group(&mut self) {
|
||||||
let active_ssh_tab = self.active_tab.as_ref().and_then(|id| {
|
let active_group = self.active_group.as_ref().and_then(|group_id| {
|
||||||
self.tabs
|
self.tab_groups.iter().find(|group| &group.id == group_id)
|
||||||
.iter()
|
|
||||||
.find(|tab| tab.id == *id && tab.kind == TabKind::Ssh)
|
|
||||||
});
|
});
|
||||||
let new_id = active_ssh_tab.map(|tab| tab.id.clone());
|
|
||||||
|
if let Some(current_sys_id) = &self.system_tab_id {
|
||||||
|
if let Some(group) = active_group {
|
||||||
|
let pane_tab_ids = group.pane_root.tab_ids();
|
||||||
|
if pane_tab_ids.contains(¤t_sys_id.as_str()) {
|
||||||
|
if self
|
||||||
|
.tabs
|
||||||
|
.iter()
|
||||||
|
.any(|t| &t.id == current_sys_id && t.kind == TabKind::Ssh && t.connected)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let new_id = self
|
||||||
|
.active_tab
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|id| {
|
||||||
|
self.tabs
|
||||||
|
.iter()
|
||||||
|
.find(|tab| tab.id == *id && tab.kind == TabKind::Ssh)
|
||||||
|
.map(|tab| tab.id.clone())
|
||||||
|
})
|
||||||
|
.or_else(|| {
|
||||||
|
active_group.and_then(|group| {
|
||||||
|
let pane_tab_ids = group.pane_root.tab_ids();
|
||||||
|
self.tabs
|
||||||
|
.iter()
|
||||||
|
.find(|tab| {
|
||||||
|
pane_tab_ids.contains(&tab.id.as_str())
|
||||||
|
&& tab.kind == TabKind::Ssh
|
||||||
|
&& tab.connected
|
||||||
|
})
|
||||||
|
.map(|tab| tab.id.clone())
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
let active_ssh_tab = new_id
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|id| self.tabs.iter().find(|tab| &tab.id == id && tab.kind == TabKind::Ssh));
|
||||||
let active_ssh_status = active_ssh_tab.and_then(|tab| {
|
let active_ssh_status = active_ssh_tab.and_then(|tab| {
|
||||||
(!tab.connected).then(|| {
|
(!tab.connected).then(|| {
|
||||||
tab.disconnected_reason
|
tab.disconnected_reason
|
||||||
@@ -2596,13 +2705,24 @@ impl Ashell {
|
|||||||
|
|
||||||
if self.system_tab_id != new_id {
|
if self.system_tab_id != new_id {
|
||||||
self.system_tab_id = new_id;
|
self.system_tab_id = new_id;
|
||||||
self.reset_system_monitor_state();
|
self.remote_sample_in_flight = false;
|
||||||
|
self.remote_processes_in_flight = false;
|
||||||
|
self.remote_ports_in_flight = false;
|
||||||
|
|
||||||
if let Some(status) = active_ssh_status {
|
if let Some(status) = active_ssh_status {
|
||||||
self.system_status = Some(status.clone().into());
|
self.system_status = Some(status.clone().into());
|
||||||
self.remote_process_status = Some(status.into());
|
self.remote_process_status = Some(status.into());
|
||||||
|
self.system = crate::system::SystemSnapshot::default();
|
||||||
|
self.cpu_history.clear();
|
||||||
|
self.net_rx_history.clear();
|
||||||
|
self.net_tx_history.clear();
|
||||||
|
self.remote_processes.clear();
|
||||||
|
self.remote_ports.clear();
|
||||||
|
self.last_applied_system_tab_id = self.system_tab_id.clone();
|
||||||
} else {
|
} else {
|
||||||
self.system_status = None;
|
self.system_status = None;
|
||||||
}
|
}
|
||||||
|
|
||||||
self.request_active_system_snapshot();
|
self.request_active_system_snapshot();
|
||||||
if self.active_dialog == Some(crate::app::DialogKind::Processes) {
|
if self.active_dialog == Some(crate::app::DialogKind::Processes) {
|
||||||
self.request_active_process_snapshot();
|
self.request_active_process_snapshot();
|
||||||
|
|||||||
+129
-40
@@ -12,6 +12,7 @@ use gpui_component::ActiveTheme as _;
|
|||||||
|
|
||||||
use crate::Ashell;
|
use crate::Ashell;
|
||||||
use crate::terminal::custom_blocks::{is_custom_block_supported, paint_custom_block};
|
use crate::terminal::custom_blocks::{is_custom_block_supported, paint_custom_block};
|
||||||
|
use crate::terminal::highlight::HighlightStyle;
|
||||||
use crate::terminal::{RenderSnapshot, ViewportSelection};
|
use crate::terminal::{RenderSnapshot, ViewportSelection};
|
||||||
|
|
||||||
#[derive(Clone, Copy)]
|
#[derive(Clone, Copy)]
|
||||||
@@ -338,21 +339,16 @@ impl TerminalElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn cell_run_style(&self, cell: &alacritty_terminal::term::cell::Cell, cx: &App) -> TextRun {
|
fn cell_run_style(
|
||||||
let mut fg = color_to_hsla(cell.fg, true, cx);
|
&self,
|
||||||
let mut bg = color_to_hsla(cell.bg, false, cx);
|
cell: &alacritty_terminal::term::cell::Cell,
|
||||||
if cell.flags.contains(Flags::INVERSE) {
|
foreground: Hsla,
|
||||||
std::mem::swap(&mut fg, &mut bg);
|
) -> TextRun {
|
||||||
}
|
|
||||||
if cell.flags.contains(Flags::DIM) {
|
|
||||||
fg.a *= 0.7;
|
|
||||||
}
|
|
||||||
|
|
||||||
let underline = cell
|
let underline = cell
|
||||||
.flags
|
.flags
|
||||||
.intersects(Flags::ALL_UNDERLINES)
|
.intersects(Flags::ALL_UNDERLINES)
|
||||||
.then(|| UnderlineStyle {
|
.then(|| UnderlineStyle {
|
||||||
color: Some(fg),
|
color: Some(foreground),
|
||||||
thickness: px(1.0),
|
thickness: px(1.0),
|
||||||
wavy: cell.flags.contains(Flags::UNDERCURL),
|
wavy: cell.flags.contains(Flags::UNDERCURL),
|
||||||
});
|
});
|
||||||
@@ -360,7 +356,7 @@ impl TerminalElement {
|
|||||||
.flags
|
.flags
|
||||||
.contains(Flags::STRIKEOUT)
|
.contains(Flags::STRIKEOUT)
|
||||||
.then(|| StrikethroughStyle {
|
.then(|| StrikethroughStyle {
|
||||||
color: Some(fg),
|
color: Some(foreground),
|
||||||
thickness: px(1.0),
|
thickness: px(1.0),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -377,7 +373,7 @@ impl TerminalElement {
|
|||||||
|
|
||||||
TextRun {
|
TextRun {
|
||||||
len: cell.c.len_utf8(),
|
len: cell.c.len_utf8(),
|
||||||
color: fg,
|
color: foreground,
|
||||||
background_color: None,
|
background_color: None,
|
||||||
font: Font {
|
font: Font {
|
||||||
family: self.font_family.clone(),
|
family: self.font_family.clone(),
|
||||||
@@ -408,12 +404,6 @@ impl TerminalElement {
|
|||||||
let mut underlines = Vec::new();
|
let mut underlines = Vec::new();
|
||||||
let mut current_run: Option<BatchedTextRun> = None;
|
let mut current_run: Option<BatchedTextRun> = None;
|
||||||
|
|
||||||
// Retrieve cached keyword highlights and merge with search highlights
|
|
||||||
let mut highlights = self.snapshot.highlights.clone();
|
|
||||||
if let Some(sm) = self.search_highlights.as_ref() {
|
|
||||||
highlights.extend(sm.iter().map(|(k, v)| (*k, *v)));
|
|
||||||
}
|
|
||||||
|
|
||||||
for render_cell in &self.snapshot.cells {
|
for render_cell in &self.snapshot.cells {
|
||||||
let cell = &render_cell.cell;
|
let cell = &render_cell.cell;
|
||||||
if cell.flags.intersects(
|
if cell.flags.intersects(
|
||||||
@@ -425,24 +415,54 @@ impl TerminalElement {
|
|||||||
let selected = self.snapshot.selection.is_some_and(|selection| {
|
let selected = self.snapshot.selection.is_some_and(|selection| {
|
||||||
selection_contains(selection, render_cell.row, render_cell.col)
|
selection_contains(selection, render_cell.row, render_cell.col)
|
||||||
});
|
});
|
||||||
let bg = color_to_hsla(cell.bg, false, cx);
|
let cell_position = (render_cell.row, render_cell.col);
|
||||||
if selected || !is_default_bg(cell.bg) || cell.flags.contains(Flags::INVERSE) {
|
let keyword_highlight = self.snapshot.highlights.get(&cell_position).copied();
|
||||||
|
let search_highlight = self
|
||||||
|
.search_highlights
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|highlights| highlights.get(&cell_position))
|
||||||
|
.copied();
|
||||||
|
let (foreground, background) = resolve_cell_colors(
|
||||||
|
color_to_hsla(cell.fg, true, cx),
|
||||||
|
color_to_hsla(cell.bg, false, cx),
|
||||||
|
keyword_highlight,
|
||||||
|
cell.flags,
|
||||||
|
);
|
||||||
|
let cell_span = if cell.flags.contains(Flags::WIDE_CHAR) {
|
||||||
|
2
|
||||||
|
} else {
|
||||||
|
1
|
||||||
|
};
|
||||||
|
|
||||||
|
if selected {
|
||||||
rects.push(LayoutRect {
|
rects.push(LayoutRect {
|
||||||
row: render_cell.row,
|
row: render_cell.row,
|
||||||
col: render_cell.col,
|
col: render_cell.col,
|
||||||
cells: if cell.flags.contains(Flags::WIDE_CHAR) {
|
cells: cell_span,
|
||||||
2
|
color: cx.theme().selection,
|
||||||
} else {
|
|
||||||
1
|
|
||||||
},
|
|
||||||
color: if selected {
|
|
||||||
cx.theme().selection
|
|
||||||
} else if cell.flags.contains(Flags::INVERSE) {
|
|
||||||
color_to_hsla(cell.fg, true, cx)
|
|
||||||
} else {
|
|
||||||
bg
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
let has_highlight_background =
|
||||||
|
keyword_highlight.is_some_and(|style| style.background.is_some());
|
||||||
|
if !is_default_bg(cell.bg)
|
||||||
|
|| cell.flags.contains(Flags::INVERSE)
|
||||||
|
|| has_highlight_background
|
||||||
|
{
|
||||||
|
rects.push(LayoutRect {
|
||||||
|
row: render_cell.row,
|
||||||
|
col: render_cell.col,
|
||||||
|
cells: cell_span,
|
||||||
|
color: background,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if let Some(search_background) = search_highlight {
|
||||||
|
rects.push(LayoutRect {
|
||||||
|
row: render_cell.row,
|
||||||
|
col: render_cell.col,
|
||||||
|
cells: cell_span,
|
||||||
|
color: search_background,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if is_blank(cell) {
|
if is_blank(cell) {
|
||||||
@@ -452,12 +472,7 @@ impl TerminalElement {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut style = self.cell_run_style(cell, cx);
|
let style = self.cell_run_style(cell, foreground);
|
||||||
|
|
||||||
// Apply keyword highlight color if this cell was matched.
|
|
||||||
if let Some(&hl_color) = highlights.get(&(render_cell.row, render_cell.col)) {
|
|
||||||
style.color = hl_color;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply hover underline if mouse is hovering over this URL
|
// Apply hover underline if mouse is hovering over this URL
|
||||||
if let Some(hu) = &hovered_url {
|
if let Some(hu) = &hovered_url {
|
||||||
@@ -871,6 +886,31 @@ fn selection_contains(selection: ViewportSelection, row: i32, col: i32) -> bool
|
|||||||
after_start && before_end
|
after_start && before_end
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn resolve_cell_colors(
|
||||||
|
mut foreground: Hsla,
|
||||||
|
mut background: Hsla,
|
||||||
|
highlight: Option<HighlightStyle>,
|
||||||
|
flags: Flags,
|
||||||
|
) -> (Hsla, Hsla) {
|
||||||
|
if let Some(highlight) = highlight {
|
||||||
|
if let Some(highlight_foreground) = highlight.foreground {
|
||||||
|
foreground = highlight_foreground;
|
||||||
|
}
|
||||||
|
if let Some(highlight_background) = highlight.background {
|
||||||
|
background = highlight_background;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if flags.contains(Flags::INVERSE) {
|
||||||
|
std::mem::swap(&mut foreground, &mut background);
|
||||||
|
}
|
||||||
|
if flags.contains(Flags::DIM) {
|
||||||
|
foreground.a *= 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
(foreground, background)
|
||||||
|
}
|
||||||
|
|
||||||
fn is_blank(cell: &alacritty_terminal::term::cell::Cell) -> bool {
|
fn is_blank(cell: &alacritty_terminal::term::cell::Cell) -> bool {
|
||||||
cell.c == ' '
|
cell.c == ' '
|
||||||
&& cell.zerowidth().is_none()
|
&& cell.zerowidth().is_none()
|
||||||
@@ -965,7 +1005,14 @@ fn named_color(named: NamedColor, _foreground: bool, cx: &App) -> Hsla {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::should_render_cursor;
|
use super::{resolve_cell_colors, should_render_cursor};
|
||||||
|
use crate::terminal::highlight::HighlightStyle;
|
||||||
|
use alacritty_terminal::term::cell::Flags;
|
||||||
|
use gpui::Hsla;
|
||||||
|
|
||||||
|
fn color(h: f32, s: f32, l: f32) -> Hsla {
|
||||||
|
Hsla { h, s, l, a: 1.0 }
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn renders_cursor_only_for_focused_pane_in_focused_terminal_window() {
|
fn renders_cursor_only_for_focused_pane_in_focused_terminal_window() {
|
||||||
@@ -974,4 +1021,46 @@ mod tests {
|
|||||||
assert!(!should_render_cursor(true, false, true));
|
assert!(!should_render_cursor(true, false, true));
|
||||||
assert!(!should_render_cursor(true, true, false));
|
assert!(!should_render_cursor(true, true, false));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn applies_highlight_channels_before_inverse_terminal_style() {
|
||||||
|
let original_foreground = color(0.0, 0.0, 0.8);
|
||||||
|
let original_background = color(0.0, 0.0, 0.1);
|
||||||
|
let highlight_foreground = color(0.1, 0.8, 0.5);
|
||||||
|
let highlight_background = color(0.6, 0.8, 0.4);
|
||||||
|
|
||||||
|
let (foreground, background) = resolve_cell_colors(
|
||||||
|
original_foreground,
|
||||||
|
original_background,
|
||||||
|
Some(HighlightStyle {
|
||||||
|
foreground: Some(highlight_foreground),
|
||||||
|
background: Some(highlight_background),
|
||||||
|
}),
|
||||||
|
Flags::INVERSE,
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(foreground, highlight_background);
|
||||||
|
assert_eq!(background, highlight_foreground);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn foreground_highlight_preserves_background_and_dim_style() {
|
||||||
|
let original_foreground = color(0.0, 0.0, 0.8);
|
||||||
|
let original_background = color(0.0, 0.0, 0.1);
|
||||||
|
let highlight_foreground = color(0.1, 0.8, 0.5);
|
||||||
|
|
||||||
|
let (foreground, background) = resolve_cell_colors(
|
||||||
|
original_foreground,
|
||||||
|
original_background,
|
||||||
|
Some(HighlightStyle {
|
||||||
|
foreground: Some(highlight_foreground),
|
||||||
|
background: None,
|
||||||
|
}),
|
||||||
|
Flags::DIM,
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(foreground.alpha(1.0), highlight_foreground);
|
||||||
|
assert!((foreground.a - 0.7).abs() < f32::EPSILON);
|
||||||
|
assert_eq!(background, original_background);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+154
-82
@@ -1,41 +1,19 @@
|
|||||||
use crate::terminal::RenderCell;
|
use crate::terminal::RenderCell;
|
||||||
use gpui::Hsla;
|
use gpui::{Hsla, Rgba};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
trait HslaExt {
|
#[derive(Debug, Clone, Copy, Default, PartialEq)]
|
||||||
fn into_rgba_like(self, r: u8, g: u8, b: u8) -> Self;
|
pub struct HighlightStyle {
|
||||||
|
pub foreground: Option<Hsla>,
|
||||||
|
pub background: Option<Hsla>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl HslaExt for Hsla {
|
impl HighlightStyle {
|
||||||
fn into_rgba_like(self, r: u8, g: u8, b: u8) -> Self {
|
fn from_foreground(color: Hsla) -> Self {
|
||||||
let rf = r as f32 / 255.0;
|
Self {
|
||||||
let gf = g as f32 / 255.0;
|
foreground: Some(color),
|
||||||
let bf = b as f32 / 255.0;
|
background: None,
|
||||||
let max = rf.max(gf).max(bf);
|
|
||||||
let min = rf.min(gf).min(bf);
|
|
||||||
let l = (max + min) / 2.0;
|
|
||||||
if max == min {
|
|
||||||
return Hsla {
|
|
||||||
h: 0.0,
|
|
||||||
s: 0.0,
|
|
||||||
l,
|
|
||||||
a: 1.0,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
let d = max - min;
|
|
||||||
let s = if l > 0.5 {
|
|
||||||
d / (2.0 - max - min)
|
|
||||||
} else {
|
|
||||||
d / (max + min)
|
|
||||||
};
|
|
||||||
let h = if max == rf {
|
|
||||||
((gf - bf) / d + if gf < bf { 6.0 } else { 0.0 }) / 6.0
|
|
||||||
} else if max == gf {
|
|
||||||
((bf - rf) / d + 2.0) / 6.0
|
|
||||||
} else {
|
|
||||||
((rf - gf) / d + 4.0) / 6.0
|
|
||||||
};
|
|
||||||
Hsla { h, s, l, a: 1.0 }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -97,13 +75,12 @@ struct HighlightColors {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn hsla(r: u8, g: u8, b: u8) -> Hsla {
|
fn hsla(r: u8, g: u8, b: u8) -> Hsla {
|
||||||
Hsla {
|
Hsla::from(Rgba {
|
||||||
h: 0.0,
|
r: r as f32 / 255.0,
|
||||||
s: 0.0,
|
g: g as f32 / 255.0,
|
||||||
l: 0.0,
|
b: b as f32 / 255.0,
|
||||||
a: 1.0,
|
a: 1.0,
|
||||||
}
|
})
|
||||||
.into_rgba_like(r, g, b)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn highlight_colors() -> HighlightColors {
|
fn highlight_colors() -> HighlightColors {
|
||||||
@@ -168,7 +145,7 @@ fn highlight_colors() -> HighlightColors {
|
|||||||
struct KeywordMatch {
|
struct KeywordMatch {
|
||||||
start_col: i32,
|
start_col: i32,
|
||||||
end_col: i32,
|
end_col: i32,
|
||||||
color: Hsla,
|
style: HighlightStyle,
|
||||||
priority: usize,
|
priority: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -184,7 +161,7 @@ impl KeywordMatch {
|
|||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
struct KeywordHighlights {
|
struct KeywordHighlights {
|
||||||
colors: HashMap<(i32, i32), Hsla>,
|
styles: HashMap<(i32, i32), HighlightStyle>,
|
||||||
pending: Vec<KeywordMatch>,
|
pending: Vec<KeywordMatch>,
|
||||||
next_priority: usize,
|
next_priority: usize,
|
||||||
}
|
}
|
||||||
@@ -214,7 +191,7 @@ impl KeywordHighlights {
|
|||||||
|
|
||||||
for candidate in selected {
|
for candidate in selected {
|
||||||
for col in candidate.start_col..=candidate.end_col {
|
for col in candidate.start_col..=candidate.end_col {
|
||||||
self.colors.insert((row_i32, col), candidate.color);
|
self.styles.insert((row_i32, col), candidate.style);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -262,7 +239,7 @@ fn highlight_keywords(
|
|||||||
map.pending.push(KeywordMatch {
|
map.pending.push(KeywordMatch {
|
||||||
start_col,
|
start_col,
|
||||||
end_col,
|
end_col,
|
||||||
color,
|
style: HighlightStyle::from_foreground(color),
|
||||||
priority,
|
priority,
|
||||||
});
|
});
|
||||||
start = abs + kw.len();
|
start = abs + kw.len();
|
||||||
@@ -276,7 +253,7 @@ fn highlight_keywords(
|
|||||||
/// Highlight HTTP status codes (200, 301, 404, 500, etc.)
|
/// Highlight HTTP status codes (200, 301, 404, 500, etc.)
|
||||||
/// Only matches specific common HTTP codes, not all 3-digit numbers.
|
/// Only matches specific common HTTP codes, not all 3-digit numbers.
|
||||||
fn highlight_http_codes(
|
fn highlight_http_codes(
|
||||||
map: &mut HashMap<(i32, i32), Hsla>,
|
map: &mut HashMap<(i32, i32), HighlightStyle>,
|
||||||
text: &str,
|
text: &str,
|
||||||
byte_to_col: &[i32],
|
byte_to_col: &[i32],
|
||||||
row_i32: i32,
|
row_i32: i32,
|
||||||
@@ -352,12 +329,13 @@ fn highlight_http_codes(
|
|||||||
let start_col = byte_to_col[i];
|
let start_col = byte_to_col[i];
|
||||||
let end_col = byte_to_col[(i + 2).min(byte_to_col.len() - 1)];
|
let end_col = byte_to_col[(i + 2).min(byte_to_col.len() - 1)];
|
||||||
for c in start_col..=end_col {
|
for c in start_col..=end_col {
|
||||||
map.entry((row_i32, c)).or_insert(color);
|
map.entry((row_i32, c))
|
||||||
|
.or_insert_with(|| HighlightStyle::from_foreground(color));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn highlight_cells(cells: &[RenderCell], rows: usize) -> HashMap<(i32, i32), Hsla> {
|
pub fn highlight_cells(cells: &[RenderCell], rows: usize) -> HashMap<(i32, i32), HighlightStyle> {
|
||||||
let colors = highlight_colors();
|
let colors = highlight_colors();
|
||||||
|
|
||||||
let mut row_chars: Vec<Vec<(i32, char)>> = vec![Vec::with_capacity(128); rows];
|
let mut row_chars: Vec<Vec<(i32, char)>> = vec![Vec::with_capacity(128); rows];
|
||||||
@@ -365,7 +343,9 @@ pub fn highlight_cells(cells: &[RenderCell], rows: usize) -> HashMap<(i32, i32),
|
|||||||
if rc.row < 0 || (rc.row as usize) >= rows {
|
if rc.row < 0 || (rc.row as usize) >= rows {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
row_chars[rc.row as usize].push((rc.col, rc.cell.c));
|
if let Some(character) = rendered_text_character(&rc.cell) {
|
||||||
|
row_chars[rc.row as usize].push((rc.col, character));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
for row in row_chars.iter_mut() {
|
for row in row_chars.iter_mut() {
|
||||||
row.sort_by_key(|&(col, _)| col);
|
row.sort_by_key(|&(col, _)| col);
|
||||||
@@ -852,7 +832,7 @@ pub fn highlight_cells(cells: &[RenderCell], rows: usize) -> HashMap<(i32, i32),
|
|||||||
map.apply_row(row_i32);
|
map.apply_row(row_i32);
|
||||||
|
|
||||||
// ── 30. HTTP status codes ──────────────────────────────
|
// ── 30. HTTP status codes ──────────────────────────────
|
||||||
highlight_http_codes(&mut map.colors, text, &byte_to_col, row_i32, &colors);
|
highlight_http_codes(&mut map.styles, text, &byte_to_col, row_i32, &colors);
|
||||||
|
|
||||||
// ── 31. IP addresses ───────────────────────────────────
|
// ── 31. IP addresses ───────────────────────────────────
|
||||||
for m in find_ip_addresses(text) {
|
for m in find_ip_addresses(text) {
|
||||||
@@ -860,7 +840,9 @@ pub fn highlight_cells(cells: &[RenderCell], rows: usize) -> HashMap<(i32, i32),
|
|||||||
let start_col = byte_to_col[m];
|
let start_col = byte_to_col[m];
|
||||||
let end_col = byte_to_col[(m + ip_len - 1).min(byte_to_col.len() - 1)];
|
let end_col = byte_to_col[(m + ip_len - 1).min(byte_to_col.len() - 1)];
|
||||||
for c in start_col..=end_col {
|
for c in start_col..=end_col {
|
||||||
map.colors.entry((row_i32, c)).or_insert(colors.network);
|
map.styles
|
||||||
|
.entry((row_i32, c))
|
||||||
|
.or_insert_with(|| HighlightStyle::from_foreground(colors.network));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -870,7 +852,9 @@ pub fn highlight_cells(cells: &[RenderCell], rows: usize) -> HashMap<(i32, i32),
|
|||||||
let start_col = byte_to_col[m];
|
let start_col = byte_to_col[m];
|
||||||
let end_col = byte_to_col[(m + port_len - 1).min(byte_to_col.len() - 1)];
|
let end_col = byte_to_col[(m + port_len - 1).min(byte_to_col.len() - 1)];
|
||||||
for c in start_col..=end_col {
|
for c in start_col..=end_col {
|
||||||
map.colors.entry((row_i32, c)).or_insert(colors.port);
|
map.styles
|
||||||
|
.entry((row_i32, c))
|
||||||
|
.or_insert_with(|| HighlightStyle::from_foreground(colors.port));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -884,13 +868,15 @@ pub fn highlight_cells(cells: &[RenderCell], rows: usize) -> HashMap<(i32, i32),
|
|||||||
let idx = m + i;
|
let idx = m + i;
|
||||||
if idx < line.byte_to_cell.len() {
|
if idx < line.byte_to_cell.len() {
|
||||||
let (r, c) = line.byte_to_cell[idx];
|
let (r, c) = line.byte_to_cell[idx];
|
||||||
map.colors.entry((r as i32, c as i32)).or_insert(colors.url);
|
map.styles
|
||||||
|
.entry((r as i32, c as i32))
|
||||||
|
.or_insert_with(|| HighlightStyle::from_foreground(colors.url));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
map.colors
|
map.styles
|
||||||
}
|
}
|
||||||
|
|
||||||
fn find_ip_len(text: &str) -> usize {
|
fn find_ip_len(text: &str) -> usize {
|
||||||
@@ -967,32 +953,69 @@ fn find_urls(text: &str) -> Vec<usize> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn find_url_len(text: &str) -> usize {
|
fn find_url_len(text: &str) -> usize {
|
||||||
let end = text
|
let mut end = text.len();
|
||||||
.find(|c: char| c.is_ascii_whitespace())
|
let mut expected_closings = Vec::new();
|
||||||
.unwrap_or(text.len());
|
for (index, character) in text.char_indices() {
|
||||||
let mut url = &text[..end];
|
if is_url_terminator(character) {
|
||||||
|
end = index;
|
||||||
loop {
|
|
||||||
let Some(&closing) = url.as_bytes().last() else {
|
|
||||||
break;
|
|
||||||
};
|
|
||||||
let opening = match closing {
|
|
||||||
b')' => b'(',
|
|
||||||
b']' => b'[',
|
|
||||||
b'}' => b'{',
|
|
||||||
_ => break,
|
|
||||||
};
|
|
||||||
let opening_count = url.bytes().filter(|byte| *byte == opening).count();
|
|
||||||
let closing_count = url.bytes().filter(|byte| *byte == closing).count();
|
|
||||||
if closing_count <= opening_count {
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
url = &url[..url.len() - 1];
|
|
||||||
|
match character {
|
||||||
|
'(' => expected_closings.push(')'),
|
||||||
|
'[' => expected_closings.push(']'),
|
||||||
|
'{' => expected_closings.push('}'),
|
||||||
|
')' | ']' | '}' => {
|
||||||
|
if expected_closings.last() == Some(&character) {
|
||||||
|
expected_closings.pop();
|
||||||
|
} else {
|
||||||
|
end = index;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut url = &text[..end];
|
||||||
|
while let Some(character) = url.chars().next_back() {
|
||||||
|
if !matches!(character, ',' | '.' | ';' | ':' | '!' | '?') {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
url = &url[..url.len() - character.len_utf8()];
|
||||||
}
|
}
|
||||||
|
|
||||||
url.len()
|
url.len()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn is_url_terminator(character: char) -> bool {
|
||||||
|
character.is_whitespace()
|
||||||
|
|| matches!(
|
||||||
|
character,
|
||||||
|
'"' | '\''
|
||||||
|
| '`'
|
||||||
|
| '<'
|
||||||
|
| '>'
|
||||||
|
| ','
|
||||||
|
| '。'
|
||||||
|
| ';'
|
||||||
|
| ':'
|
||||||
|
| '!'
|
||||||
|
| '?'
|
||||||
|
| '、'
|
||||||
|
| '('
|
||||||
|
| ')'
|
||||||
|
| '【'
|
||||||
|
| '】'
|
||||||
|
| '《'
|
||||||
|
| '》'
|
||||||
|
| '“'
|
||||||
|
| '”'
|
||||||
|
| '‘'
|
||||||
|
| '’'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
fn find_ports(text: &str) -> Vec<usize> {
|
fn find_ports(text: &str) -> Vec<usize> {
|
||||||
let mut positions = Vec::new();
|
let mut positions = Vec::new();
|
||||||
let bytes = text.as_bytes();
|
let bytes = text.as_bytes();
|
||||||
@@ -1090,10 +1113,12 @@ pub fn build_logical_lines<'a>(cells: &'a [RenderCell], rows: usize) -> Vec<Logi
|
|||||||
});
|
});
|
||||||
|
|
||||||
for rc in row_cells {
|
for rc in row_cells {
|
||||||
line.text.push(rc.cell.c);
|
if let Some(character) = rendered_text_character(&rc.cell) {
|
||||||
let end_len = line.text.len();
|
line.text.push(character);
|
||||||
while line.byte_to_cell.len() < end_len {
|
let end_len = line.text.len();
|
||||||
line.byte_to_cell.push((rc.row as usize, rc.col as usize));
|
while line.byte_to_cell.len() < end_len {
|
||||||
|
line.byte_to_cell.push((rc.row as usize, rc.col as usize));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
line.row_cells.push(rc);
|
line.row_cells.push(rc);
|
||||||
}
|
}
|
||||||
@@ -1108,6 +1133,21 @@ pub fn build_logical_lines<'a>(cells: &'a [RenderCell], rows: usize) -> Vec<Logi
|
|||||||
logical_lines
|
logical_lines
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn rendered_text_character(cell: &alacritty_terminal::term::cell::Cell) -> Option<char> {
|
||||||
|
use alacritty_terminal::term::cell::Flags;
|
||||||
|
|
||||||
|
if cell
|
||||||
|
.flags
|
||||||
|
.intersects(Flags::WIDE_CHAR_SPACER | Flags::LEADING_WIDE_CHAR_SPACER)
|
||||||
|
{
|
||||||
|
None
|
||||||
|
} else if cell.flags.contains(Flags::HIDDEN) {
|
||||||
|
Some(' ')
|
||||||
|
} else {
|
||||||
|
Some(cell.c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn find_url_at_cell(
|
pub fn find_url_at_cell(
|
||||||
cells: &[RenderCell],
|
cells: &[RenderCell],
|
||||||
rows: usize,
|
rows: usize,
|
||||||
@@ -1142,7 +1182,7 @@ pub fn find_url_at_cell(
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::{KeywordHighlights, KeywordMatch, find_url_len, hsla};
|
use super::{HighlightStyle, KeywordHighlights, KeywordMatch, find_url_len, hsla};
|
||||||
|
|
||||||
fn detected_url(text: &str) -> &str {
|
fn detected_url(text: &str) -> &str {
|
||||||
&text[..find_url_len(text)]
|
&text[..find_url_len(text)]
|
||||||
@@ -1172,6 +1212,28 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn stops_before_text_after_an_unmatched_closing_delimiter() {
|
||||||
|
assert_eq!(
|
||||||
|
detected_url(
|
||||||
|
"https://github.com/gnachman/iTerm2/blob/master/sources/PTYSession/PTYSession.m#L6371-L6380)、PTYTab.m"
|
||||||
|
),
|
||||||
|
"https://github.com/gnachman/iTerm2/blob/master/sources/PTYSession/PTYSession.m#L6371-L6380"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn excludes_chinese_and_ascii_trailing_punctuation() {
|
||||||
|
assert_eq!(
|
||||||
|
detected_url("https://example.com/path、后续文字"),
|
||||||
|
"https://example.com/path"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
detected_url("https://example.com/path, next"),
|
||||||
|
"https://example.com/path"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn overlapping_keywords_keep_the_longest_match_color() {
|
fn overlapping_keywords_keep_the_longest_match_color() {
|
||||||
let long_color = hsla(10, 20, 30);
|
let long_color = hsla(10, 20, 30);
|
||||||
@@ -1181,21 +1243,26 @@ mod tests {
|
|||||||
KeywordMatch {
|
KeywordMatch {
|
||||||
start_col: 3,
|
start_col: 3,
|
||||||
end_col: 11,
|
end_col: 11,
|
||||||
color: short_color,
|
style: HighlightStyle::from_foreground(short_color),
|
||||||
priority: 0,
|
priority: 0,
|
||||||
},
|
},
|
||||||
KeywordMatch {
|
KeywordMatch {
|
||||||
start_col: 0,
|
start_col: 0,
|
||||||
end_col: 11,
|
end_col: 11,
|
||||||
color: long_color,
|
style: HighlightStyle::from_foreground(long_color),
|
||||||
priority: 1,
|
priority: 1,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
highlights.apply_row(0);
|
highlights.apply_row(0);
|
||||||
|
|
||||||
assert_eq!(highlights.colors.len(), 12);
|
assert_eq!(highlights.styles.len(), 12);
|
||||||
assert!(highlights.colors.values().all(|color| *color == long_color));
|
assert!(
|
||||||
|
highlights
|
||||||
|
.styles
|
||||||
|
.values()
|
||||||
|
.all(|style| style.foreground == Some(long_color) && style.background.is_none())
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -1208,8 +1275,13 @@ mod tests {
|
|||||||
super::highlight_keywords(&mut highlights, text, &byte_to_col, 0, &["BOOT"], color);
|
super::highlight_keywords(&mut highlights, text, &byte_to_col, 0, &["BOOT"], color);
|
||||||
highlights.apply_row(0);
|
highlights.apply_row(0);
|
||||||
|
|
||||||
assert_eq!(highlights.colors.len(), 4);
|
assert_eq!(highlights.styles.len(), 4);
|
||||||
assert!((0..9).all(|col| !highlights.colors.contains_key(&(0, col))));
|
assert!((0..9).all(|col| !highlights.styles.contains_key(&(0, col))));
|
||||||
assert!((10..14).all(|col| highlights.colors.get(&(0, col)) == Some(&color)));
|
assert!((10..14).all(|col| {
|
||||||
|
highlights
|
||||||
|
.styles
|
||||||
|
.get(&(0, col))
|
||||||
|
.is_some_and(|style| style.foreground == Some(color) && style.background.is_none())
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+101
-26
@@ -209,7 +209,7 @@ impl Ashell {
|
|||||||
}
|
}
|
||||||
|
|
||||||
tab.clear_selection();
|
tab.clear_selection();
|
||||||
tab.record_terminal_input(&bytes);
|
tab.prepare_for_terminal_input();
|
||||||
let encoded = tab.encode_input(&bytes);
|
let encoded = tab.encode_input(&bytes);
|
||||||
tab.send_backend(BackendCommand::Input(encoded));
|
tab.send_backend(BackendCommand::Input(encoded));
|
||||||
window.prevent_default();
|
window.prevent_default();
|
||||||
@@ -325,7 +325,7 @@ impl Ashell {
|
|||||||
}
|
}
|
||||||
tab.clear_selection();
|
tab.clear_selection();
|
||||||
self.terminal_marked_text = None;
|
self.terminal_marked_text = None;
|
||||||
tab.record_terminal_input(&bytes);
|
tab.prepare_for_terminal_input();
|
||||||
let encoded = tab.encode_input(&bytes);
|
let encoded = tab.encode_input(&bytes);
|
||||||
tab.send_backend(BackendCommand::Input(encoded));
|
tab.send_backend(BackendCommand::Input(encoded));
|
||||||
window.invalidate_character_coordinates();
|
window.invalidate_character_coordinates();
|
||||||
@@ -345,7 +345,7 @@ impl Ashell {
|
|||||||
};
|
};
|
||||||
(
|
(
|
||||||
session.id.clone(),
|
session.id.clone(),
|
||||||
tab.cursor_state().map(|cursor| (cursor.row, cursor.col)),
|
tab.buffer_cursor_position(),
|
||||||
tab.is_alternate_screen_active(),
|
tab.is_alternate_screen_active(),
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
@@ -1284,6 +1284,11 @@ fn terminal_command_text(
|
|||||||
start: (usize, usize),
|
start: (usize, usize),
|
||||||
end: Option<(usize, usize)>,
|
end: Option<(usize, usize)>,
|
||||||
) -> Option<String> {
|
) -> Option<String> {
|
||||||
|
let start = buffer_position_in_viewport(snapshot, start)?;
|
||||||
|
let end = match end {
|
||||||
|
Some(position) => Some(buffer_position_in_viewport(snapshot, position)?),
|
||||||
|
None => None,
|
||||||
|
};
|
||||||
let logical_lines =
|
let logical_lines =
|
||||||
crate::terminal::highlight::build_logical_lines(&snapshot.cells, snapshot.rows);
|
crate::terminal::highlight::build_logical_lines(&snapshot.cells, snapshot.rows);
|
||||||
for line in logical_lines {
|
for line in logical_lines {
|
||||||
@@ -1315,42 +1320,38 @@ fn terminal_command_text(
|
|||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn buffer_position_in_viewport(
|
||||||
|
snapshot: &crate::terminal::RenderSnapshot,
|
||||||
|
position: (usize, usize),
|
||||||
|
) -> Option<(usize, usize)> {
|
||||||
|
let viewport_start = snapshot
|
||||||
|
.history_size
|
||||||
|
.saturating_sub(snapshot.display_offset);
|
||||||
|
let row = position.0.checked_sub(viewport_start)?;
|
||||||
|
(row < snapshot.rows && position.1 < snapshot.cols).then_some((row, position.1))
|
||||||
|
}
|
||||||
|
|
||||||
fn command_history_text(rendered: Option<&str>, buffered: &str, input_uncertain: bool) -> String {
|
fn command_history_text(rendered: Option<&str>, buffered: &str, input_uncertain: bool) -> String {
|
||||||
|
let rendered = rendered.unwrap_or_default().trim();
|
||||||
let buffered = buffered.trim();
|
let buffered = buffered.trim();
|
||||||
if !input_uncertain && !buffered.is_empty() {
|
if !input_uncertain && !buffered.is_empty() {
|
||||||
return buffered.to_string();
|
return buffered.to_string();
|
||||||
}
|
}
|
||||||
merge_command_text(rendered, buffered)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn merge_command_text(rendered: Option<&str>, buffered: &str) -> String {
|
|
||||||
let rendered = rendered.unwrap_or_default().trim();
|
|
||||||
let buffered = buffered.trim();
|
|
||||||
if rendered.is_empty() {
|
if rendered.is_empty() {
|
||||||
return buffered.to_string();
|
return buffered.to_string();
|
||||||
}
|
}
|
||||||
if buffered.is_empty() {
|
if buffered.is_empty() {
|
||||||
return rendered.to_string();
|
return rendered.to_string();
|
||||||
}
|
}
|
||||||
if rendered.starts_with(buffered) || rendered.ends_with(buffered) {
|
// Completion extends the current token; a new argument after exact raw input is stale content.
|
||||||
return rendered.to_string();
|
if rendered
|
||||||
}
|
.strip_prefix(buffered)
|
||||||
if buffered.starts_with(rendered) || buffered.ends_with(rendered) {
|
.and_then(|suffix| suffix.chars().next())
|
||||||
|
.is_some_and(char::is_whitespace)
|
||||||
|
{
|
||||||
return buffered.to_string();
|
return buffered.to_string();
|
||||||
}
|
}
|
||||||
|
rendered.to_string()
|
||||||
let overlap = buffered
|
|
||||||
.char_indices()
|
|
||||||
.map(|(index, _)| index)
|
|
||||||
.chain(std::iter::once(buffered.len()))
|
|
||||||
.filter(|index| *index > 0 && rendered.ends_with(&buffered[..*index]))
|
|
||||||
.max()
|
|
||||||
.unwrap_or(0);
|
|
||||||
if overlap > 0 {
|
|
||||||
format!("{rendered}{}", &buffered[overlap..])
|
|
||||||
} else {
|
|
||||||
rendered.to_string()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@@ -1581,6 +1582,58 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rejects_whitespace_delimited_screen_suffix_when_input_is_uncertain() {
|
||||||
|
let command = "sh /site/jimureport/jimureport-restart.sh";
|
||||||
|
let rendered = format!("{command} /sijiji-r");
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
command_history_text(Some(rendered.as_str()), command, true),
|
||||||
|
command
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn does_not_append_tab_completion_keystroke_fragments() {
|
||||||
|
let command = "sh /site/jimureport/jimureport-restart.sh";
|
||||||
|
let buffered = "sh /sijiji-r";
|
||||||
|
|
||||||
|
assert_eq!(command_history_text(Some(command), buffered, true), command);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn falls_back_to_raw_input_when_uncertain_screen_text_is_unavailable() {
|
||||||
|
let command = "sh /site/jimureport/jimureport-restart.sh";
|
||||||
|
|
||||||
|
assert_eq!(command_history_text(None, command, true), command);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn keeps_screen_completion_that_extends_the_current_argument() {
|
||||||
|
let buffered = "sh /site/jimu";
|
||||||
|
let rendered = "sh /site/jimureport/jimureport-restart.sh";
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
command_history_text(Some(rendered), buffered, true),
|
||||||
|
rendered
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn excludes_hidden_screen_residue_from_rendered_command() {
|
||||||
|
let command = "sh /site/jimureport/jimureport-restart.sh";
|
||||||
|
let rendered = format!("$ {command} /sijiji-r");
|
||||||
|
let mut snapshot = snapshot(&[rendered.as_str()], rendered.len());
|
||||||
|
for cell in snapshot.cells.iter_mut().skip(2 + command.chars().count()) {
|
||||||
|
cell.cell.flags.insert(Flags::HIDDEN);
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
terminal_command_text(&snapshot, (0, 2), None),
|
||||||
|
Some(command.to_string())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn truncates_rendered_command_at_the_submission_cursor() {
|
fn truncates_rendered_command_at_the_submission_cursor() {
|
||||||
let command = "sh /site/vocano/vocano-restart.sh";
|
let command = "sh /site/vocano/vocano-restart.sh";
|
||||||
@@ -1592,4 +1645,26 @@ mod tests {
|
|||||||
Some(command.to_string())
|
Some(command.to_string())
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn keeps_command_coordinates_stable_when_wrapping_scrolls_the_viewport() {
|
||||||
|
let first_row = "$ sh /site/jimureport/jimureport-";
|
||||||
|
let second_row = "restart.sh /sijiji-r";
|
||||||
|
let mut snapshot = snapshot(&[first_row, second_row], first_row.len());
|
||||||
|
snapshot.history_size = 3;
|
||||||
|
snapshot.display_offset = 2;
|
||||||
|
snapshot
|
||||||
|
.cells
|
||||||
|
.iter_mut()
|
||||||
|
.find(|cell| cell.row == 0 && cell.col == first_row.len() as i32 - 1)
|
||||||
|
.unwrap()
|
||||||
|
.cell
|
||||||
|
.flags
|
||||||
|
.insert(Flags::WRAPLINE);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
terminal_command_text(&snapshot, (1, 2), Some((2, "restart.sh".len()))),
|
||||||
|
Some("sh /site/jimureport/jimureport-restart.sh".to_string())
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+85
-39
@@ -37,7 +37,7 @@ pub enum TabKind {
|
|||||||
Serial,
|
Serial,
|
||||||
}
|
}
|
||||||
|
|
||||||
const TERMINAL_ACTIVITY_GRACE: Duration = Duration::from_millis(750);
|
const TERMINAL_ACTIVITY_GRACE: Duration = Duration::from_secs(2);
|
||||||
const CLICK_CURSOR_PREDICTION_TTL: Duration = Duration::from_millis(750);
|
const CLICK_CURSOR_PREDICTION_TTL: Duration = Duration::from_millis(750);
|
||||||
const MAX_OSC_PAYLOAD_BYTES: usize = 4096;
|
const MAX_OSC_PAYLOAD_BYTES: usize = 4096;
|
||||||
const MAX_NOTIFICATION_TEXT_BYTES: usize = 8192;
|
const MAX_NOTIFICATION_TEXT_BYTES: usize = 8192;
|
||||||
@@ -242,27 +242,39 @@ struct OscTerminalParser {
|
|||||||
pending_osc99: HashMap<String, PendingOsc99Notification>,
|
pending_osc99: HashMap<String, PendingOsc99Notification>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct OscTerminalScan {
|
||||||
|
events: Vec<(usize, OscTerminalEvent)>,
|
||||||
|
has_terminal_output: bool,
|
||||||
|
}
|
||||||
|
|
||||||
impl OscTerminalParser {
|
impl OscTerminalParser {
|
||||||
/// Scans decoded terminal output without consuming it from the terminal emulator.
|
/// Scans decoded terminal output without consuming it from the terminal emulator.
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
fn advance(&mut self, bytes: &[u8]) -> Vec<OscTerminalEvent> {
|
fn advance(&mut self, bytes: &[u8]) -> Vec<OscTerminalEvent> {
|
||||||
self.advance_with_offsets(bytes)
|
self.advance_with_offsets(bytes)
|
||||||
|
.events
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|(_, event)| event)
|
.map(|(_, event)| event)
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn advance_with_offsets(&mut self, bytes: &[u8]) -> Vec<(usize, OscTerminalEvent)> {
|
fn advance_with_offsets(&mut self, bytes: &[u8]) -> OscTerminalScan {
|
||||||
let mut events = Vec::new();
|
let mut events = Vec::new();
|
||||||
|
let mut has_terminal_output = false;
|
||||||
|
|
||||||
for (index, &byte) in bytes.iter().enumerate() {
|
for (index, &byte) in bytes.iter().enumerate() {
|
||||||
match self.state {
|
match self.state {
|
||||||
OscTerminalState::Ground => {
|
OscTerminalState::Ground => {
|
||||||
if byte == 0x1b {
|
if byte == 0x1b {
|
||||||
self.state = OscTerminalState::Escape;
|
self.state = OscTerminalState::Escape;
|
||||||
|
} else {
|
||||||
|
has_terminal_output = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
OscTerminalState::Escape => {
|
OscTerminalState::Escape => {
|
||||||
|
if byte != b']' && byte != 0x1b {
|
||||||
|
has_terminal_output = true;
|
||||||
|
}
|
||||||
self.state = match byte {
|
self.state = match byte {
|
||||||
b']' => {
|
b']' => {
|
||||||
self.command.clear();
|
self.command.clear();
|
||||||
@@ -333,7 +345,10 @@ impl OscTerminalParser {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
events
|
OscTerminalScan {
|
||||||
|
events,
|
||||||
|
has_terminal_output,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn push_payload_byte(&mut self, byte: u8) {
|
fn push_payload_byte(&mut self, byte: u8) {
|
||||||
@@ -869,8 +884,6 @@ pub struct TerminalTab {
|
|||||||
output_decoder: StreamingDecoder,
|
output_decoder: StreamingDecoder,
|
||||||
osc_terminal_parser: OscTerminalParser,
|
osc_terminal_parser: OscTerminalParser,
|
||||||
output_activity_until: Option<Instant>,
|
output_activity_until: Option<Instant>,
|
||||||
command_running: bool,
|
|
||||||
shell_integration_available: bool,
|
|
||||||
processor: Processor,
|
processor: Processor,
|
||||||
term: Term<TerminalListener>,
|
term: Term<TerminalListener>,
|
||||||
pub cols: u16,
|
pub cols: u16,
|
||||||
@@ -887,7 +900,7 @@ pub struct TerminalTab {
|
|||||||
type HighlightCache = std::cell::RefCell<
|
type HighlightCache = std::cell::RefCell<
|
||||||
Option<(
|
Option<(
|
||||||
Vec<RenderCell>,
|
Vec<RenderCell>,
|
||||||
std::collections::HashMap<(i32, i32), gpui::Hsla>,
|
std::collections::HashMap<(i32, i32), self::highlight::HighlightStyle>,
|
||||||
)>,
|
)>,
|
||||||
>;
|
>;
|
||||||
|
|
||||||
@@ -940,7 +953,7 @@ pub struct RenderSnapshot {
|
|||||||
pub history_size: usize,
|
pub history_size: usize,
|
||||||
pub rows: usize,
|
pub rows: usize,
|
||||||
pub cols: usize,
|
pub cols: usize,
|
||||||
pub highlights: std::collections::HashMap<(i32, i32), gpui::Hsla>,
|
pub highlights: std::collections::HashMap<(i32, i32), self::highlight::HighlightStyle>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy)]
|
#[derive(Clone, Copy)]
|
||||||
@@ -1283,7 +1296,7 @@ mod terminal_tab_backend_tests {
|
|||||||
);
|
);
|
||||||
|
|
||||||
tab.note_click_cursor_move_at(predicted, now);
|
tab.note_click_cursor_move_at(predicted, now);
|
||||||
tab.record_terminal_input(b"x");
|
tab.prepare_for_terminal_input();
|
||||||
assert_eq!(tab.cursor_state_for_click_at(now), actual);
|
assert_eq!(tab.cursor_state_for_click_at(now), actual);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1542,6 +1555,60 @@ mod osc_terminal_tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn distinguishes_terminal_output_from_osc_signaling() {
|
||||||
|
let mut parser = OscTerminalParser::default();
|
||||||
|
|
||||||
|
let shell_marker = parser.advance_with_offsets(b"\x1b]133;C\x07");
|
||||||
|
assert!(!shell_marker.has_terminal_output);
|
||||||
|
assert_eq!(
|
||||||
|
shell_marker.events,
|
||||||
|
vec![(b"\x1b]133;C\x07".len(), OscTerminalEvent::CommandStarted)]
|
||||||
|
);
|
||||||
|
|
||||||
|
let output = parser.advance_with_offsets(b"server ready\r\n");
|
||||||
|
assert!(output.has_terminal_output);
|
||||||
|
assert!(output.events.is_empty());
|
||||||
|
|
||||||
|
let mixed = parser.advance_with_offsets(b"built\r\n\x1b]133;D;0\x07");
|
||||||
|
assert!(mixed.has_terminal_output);
|
||||||
|
assert_eq!(mixed.events.len(), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn excludes_osc_signaling_split_across_output_chunks() {
|
||||||
|
let mut parser = OscTerminalParser::default();
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
!parser
|
||||||
|
.advance_with_offsets(b"\x1b]133;")
|
||||||
|
.has_terminal_output
|
||||||
|
);
|
||||||
|
let completed = parser.advance_with_offsets(b"C\x07");
|
||||||
|
assert!(!completed.has_terminal_output);
|
||||||
|
assert_eq!(completed.events.len(), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn shell_integration_markers_do_not_drive_tab_output_activity() {
|
||||||
|
let (events_tx, _events_rx) = mpsc::channel();
|
||||||
|
let mut tab = TerminalTab::new_local(
|
||||||
|
"tab-1".into(),
|
||||||
|
"Local".into(),
|
||||||
|
BackendTx::Pending,
|
||||||
|
GuardedBackendEventSender::new(events_tx),
|
||||||
|
);
|
||||||
|
|
||||||
|
tab.feed(b"\x1b]133;C\x07");
|
||||||
|
assert!(!tab.has_recent_output());
|
||||||
|
|
||||||
|
tab.feed(b"server ready\r\n");
|
||||||
|
assert!(tab.has_recent_output());
|
||||||
|
|
||||||
|
tab.feed(b"\x1b]133;D;0\x07");
|
||||||
|
assert!(tab.has_recent_output());
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn parses_prompt_click_modes_and_preserves_event_offsets() {
|
fn parses_prompt_click_modes_and_preserves_event_offsets() {
|
||||||
let mut parser = OscTerminalParser::default();
|
let mut parser = OscTerminalParser::default();
|
||||||
@@ -1549,6 +1616,7 @@ mod osc_terminal_tests {
|
|||||||
|
|
||||||
let (event_end, event) = parser
|
let (event_end, event) = parser
|
||||||
.advance_with_offsets(bytes)
|
.advance_with_offsets(bytes)
|
||||||
|
.events
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.next()
|
.next()
|
||||||
.expect("prompt marker event");
|
.expect("prompt marker event");
|
||||||
@@ -1807,8 +1875,6 @@ impl TerminalTab {
|
|||||||
output_decoder: StreamingDecoder::new(TextEncoding::Utf8),
|
output_decoder: StreamingDecoder::new(TextEncoding::Utf8),
|
||||||
osc_terminal_parser: OscTerminalParser::default(),
|
osc_terminal_parser: OscTerminalParser::default(),
|
||||||
output_activity_until: None,
|
output_activity_until: None,
|
||||||
command_running: false,
|
|
||||||
shell_integration_available: false,
|
|
||||||
processor: Processor::new(),
|
processor: Processor::new(),
|
||||||
term: new_term(100, 30, shared_backend.clone(), id, events.clone()),
|
term: new_term(100, 30, shared_backend.clone(), id, events.clone()),
|
||||||
cols: 100,
|
cols: 100,
|
||||||
@@ -1825,12 +1891,13 @@ impl TerminalTab {
|
|||||||
|
|
||||||
pub fn feed(&mut self, bytes: &[u8]) -> Vec<TerminalNotification> {
|
pub fn feed(&mut self, bytes: &[u8]) -> Vec<TerminalNotification> {
|
||||||
let decoded = self.output_decoder.decode(bytes);
|
let decoded = self.output_decoder.decode(bytes);
|
||||||
if !decoded.is_empty() {
|
let scan = self.osc_terminal_parser.advance_with_offsets(&decoded);
|
||||||
|
if scan.has_terminal_output {
|
||||||
self.output_activity_until = Some(Instant::now() + TERMINAL_ACTIVITY_GRACE);
|
self.output_activity_until = Some(Instant::now() + TERMINAL_ACTIVITY_GRACE);
|
||||||
}
|
}
|
||||||
let mut notifications = Vec::new();
|
let mut notifications = Vec::new();
|
||||||
let mut processed_until = 0;
|
let mut processed_until = 0;
|
||||||
for (event_end, event) in self.osc_terminal_parser.advance_with_offsets(&decoded) {
|
for (event_end, event) in scan.events {
|
||||||
self.processor
|
self.processor
|
||||||
.advance(&mut self.term, &decoded[processed_until..event_end]);
|
.advance(&mut self.term, &decoded[processed_until..event_end]);
|
||||||
self.handle_osc_terminal_event(event, &mut notifications);
|
self.handle_osc_terminal_event(event, &mut notifications);
|
||||||
@@ -1850,8 +1917,6 @@ impl TerminalTab {
|
|||||||
) {
|
) {
|
||||||
match event {
|
match event {
|
||||||
OscTerminalEvent::Notification(notification) => {
|
OscTerminalEvent::Notification(notification) => {
|
||||||
self.command_running = false;
|
|
||||||
self.output_activity_until = None;
|
|
||||||
notifications.push(notification);
|
notifications.push(notification);
|
||||||
}
|
}
|
||||||
OscTerminalEvent::ProtocolReply(reply) => {
|
OscTerminalEvent::ProtocolReply(reply) => {
|
||||||
@@ -1862,9 +1927,6 @@ impl TerminalTab {
|
|||||||
secondary,
|
secondary,
|
||||||
} => {
|
} => {
|
||||||
self.clear_click_cursor_prediction();
|
self.clear_click_cursor_prediction();
|
||||||
self.shell_integration_available = true;
|
|
||||||
self.command_running = false;
|
|
||||||
self.output_activity_until = None;
|
|
||||||
let prompt_start = self.buffer_cursor_position().unwrap_or((0, 0));
|
let prompt_start = self.buffer_cursor_position().unwrap_or((0, 0));
|
||||||
if secondary {
|
if secondary {
|
||||||
if let Some(prompt_input) = self.prompt_input.as_mut() {
|
if let Some(prompt_input) = self.prompt_input.as_mut() {
|
||||||
@@ -1907,15 +1969,10 @@ impl TerminalTab {
|
|||||||
}
|
}
|
||||||
OscTerminalEvent::CommandStarted => {
|
OscTerminalEvent::CommandStarted => {
|
||||||
self.clear_click_cursor_prediction();
|
self.clear_click_cursor_prediction();
|
||||||
self.shell_integration_available = true;
|
|
||||||
self.command_running = true;
|
|
||||||
self.prompt_input = None;
|
self.prompt_input = None;
|
||||||
}
|
}
|
||||||
OscTerminalEvent::CommandFinished => {
|
OscTerminalEvent::CommandFinished => {
|
||||||
self.clear_click_cursor_prediction();
|
self.clear_click_cursor_prediction();
|
||||||
self.shell_integration_available = true;
|
|
||||||
self.command_running = false;
|
|
||||||
self.output_activity_until = None;
|
|
||||||
self.prompt_input = None;
|
self.prompt_input = None;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1944,9 +2001,8 @@ impl TerminalTab {
|
|||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn is_command_active(&self) -> bool {
|
pub(crate) fn has_recent_output(&self) -> bool {
|
||||||
(self.command_running && !self.is_alternate_screen_active())
|
self.output_activity_until.is_some()
|
||||||
|| self.output_activity_until.is_some()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn expire_output_activity(&mut self, now: Instant) -> bool {
|
pub(crate) fn expire_output_activity(&mut self, now: Instant) -> bool {
|
||||||
@@ -1961,22 +2017,13 @@ impl TerminalTab {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn record_terminal_input(&mut self, bytes: &[u8]) {
|
pub(crate) fn prepare_for_terminal_input(&mut self) {
|
||||||
self.clear_click_cursor_prediction();
|
self.clear_click_cursor_prediction();
|
||||||
if self.shell_integration_available
|
|
||||||
&& !self.is_alternate_screen_active()
|
|
||||||
&& bytes.iter().any(|byte| matches!(byte, b'\r' | b'\n'))
|
|
||||||
{
|
|
||||||
self.command_running = true;
|
|
||||||
self.output_activity_until = None;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn clear_command_activity(&mut self) -> bool {
|
pub(crate) fn clear_terminal_activity(&mut self) -> bool {
|
||||||
let changed = self.command_running || self.output_activity_until.is_some();
|
let changed = self.output_activity_until.is_some();
|
||||||
self.command_running = false;
|
|
||||||
self.output_activity_until = None;
|
self.output_activity_until = None;
|
||||||
self.shell_integration_available = false;
|
|
||||||
self.prompt_input = None;
|
self.prompt_input = None;
|
||||||
self.clear_click_cursor_prediction();
|
self.clear_click_cursor_prediction();
|
||||||
self.osc_terminal_parser = OscTerminalParser::default();
|
self.osc_terminal_parser = OscTerminalParser::default();
|
||||||
@@ -2005,7 +2052,6 @@ impl TerminalTab {
|
|||||||
self.output_decoder = StreamingDecoder::new(encoding);
|
self.output_decoder = StreamingDecoder::new(encoding);
|
||||||
self.osc_terminal_parser = OscTerminalParser::default();
|
self.osc_terminal_parser = OscTerminalParser::default();
|
||||||
self.output_activity_until = None;
|
self.output_activity_until = None;
|
||||||
self.command_running = false;
|
|
||||||
self.prompt_input = None;
|
self.prompt_input = None;
|
||||||
self.clear_click_cursor_prediction();
|
self.clear_click_cursor_prediction();
|
||||||
if let Some(session) = self.session.as_mut() {
|
if let Some(session) = self.session.as_mut() {
|
||||||
@@ -2136,7 +2182,7 @@ impl TerminalTab {
|
|||||||
let _ = self.cursor_state_for_click_at(now);
|
let _ = self.cursor_state_for_click_at(now);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn buffer_cursor_position(&self) -> Option<(usize, usize)> {
|
pub(crate) fn buffer_cursor_position(&self) -> Option<(usize, usize)> {
|
||||||
let grid = self.term.grid();
|
let grid = self.term.grid();
|
||||||
let row = usize::try_from(grid.cursor.point.line.0).ok()?;
|
let row = usize::try_from(grid.cursor.point.line.0).ok()?;
|
||||||
Some((
|
Some((
|
||||||
|
|||||||
Reference in New Issue
Block a user