mirror of
https://github.com/l0ng-ai/tty7.git
synced 2026-09-22 08:02:24 +00:00
fix(ui): SSH auth-sheet polish + softer primary buttons
- Remember toggle: real Checkbox instead of a full-width ghost button whose selected-state fill read as a grey bar across the whole card - auth sheet: tighten to the shared sheet padding (p_4 / gap_3), width 420 - dead-SSH pane: replace the top-left chip + reconnect notice (which overlaid the daemon's red failure line printed at top-left) with a single bottom-centered 'Disconnected — ⌘⇧R · Reconnect' bar, clear of the output - drop the connecting/authenticating top-left SSH chip entirely (the tab status dot carries the phase; the buffer shows connect progress) - soften primary buttons app-wide: fill from foreground nudged ~20% toward the background (a dark charcoal, not pure black) via the primary / button_primary token family Pairs with l0ng-ai/gpui-component@9484cf9 (checkbox: instant check, no fade), picked up by the Cargo.lock bump.
This commit is contained in:
Generated
+3
-3
@@ -2922,7 +2922,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "gpui-component"
|
||||
version = "0.5.2"
|
||||
source = "git+https://github.com/l0ng-ai/gpui-component?branch=tty7#25079a7b2519b1da1f7aced160725a852643b50a"
|
||||
source = "git+https://github.com/l0ng-ai/gpui-component?branch=tty7#9484cf99c89ac7bbce7af063c329925a6576409b"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"anyhow",
|
||||
@@ -2972,7 +2972,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "gpui-component-assets"
|
||||
version = "0.5.1"
|
||||
source = "git+https://github.com/l0ng-ai/gpui-component?branch=tty7#25079a7b2519b1da1f7aced160725a852643b50a"
|
||||
source = "git+https://github.com/l0ng-ai/gpui-component?branch=tty7#9484cf99c89ac7bbce7af063c329925a6576409b"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"gpui",
|
||||
@@ -2986,7 +2986,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "gpui-component-macros"
|
||||
version = "0.5.1"
|
||||
source = "git+https://github.com/l0ng-ai/gpui-component?branch=tty7#25079a7b2519b1da1f7aced160725a852643b50a"
|
||||
source = "git+https://github.com/l0ng-ai/gpui-component?branch=tty7#9484cf99c89ac7bbce7af063c329925a6576409b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
||||
+46
-68
@@ -10,31 +10,24 @@ use gpui_component::button::{Button, ButtonVariants as _};
|
||||
use gpui_component::input::Input;
|
||||
use gpui_component::{ActiveTheme as _, IconName, Sizable as _, h_flex, v_flex};
|
||||
|
||||
use crate::daemon::protocol::{
|
||||
ForwardStatus, ManagedForward, RemoteContext, SshForwardKind, SshPhase,
|
||||
};
|
||||
use crate::daemon::protocol::{ForwardStatus, ManagedForward, RemoteContext, SshForwardKind};
|
||||
use crate::terminal::view::TerminalView;
|
||||
use crate::ui::app::Tty7App;
|
||||
|
||||
impl Tty7App {
|
||||
/// The in-pane native-SSH status strip (PRD FR-E1): a subtle ` SSH ` chip
|
||||
/// coloured by the connection phase, with the hostname, pinned top-left of the
|
||||
/// terminal body. A dead pane also shows the "connection lost — ⌘⇧R to
|
||||
/// reconnect" notice (FR-E4). Returns `None` for a non-native pane.
|
||||
/// The in-pane native-SSH notice (PRD FR-E4): a dead pane shows a
|
||||
/// bottom-centered "Disconnected — ⌘⇧R to reconnect" bar. Live/connecting
|
||||
/// panes show nothing here — the tab status dot already carries the phase and
|
||||
/// the daemon prints connect progress/failures into the buffer. Returns
|
||||
/// `None` for a non-native or still-alive pane.
|
||||
pub(crate) fn render_ssh_status_strip(
|
||||
&self,
|
||||
leaf: &Entity<TerminalView>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Option<AnyElement> {
|
||||
let view = leaf.read(cx);
|
||||
let phase = view.ssh_phase()?;
|
||||
let disconnected = view.ssh_disconnected();
|
||||
// Once connected, the tab status dot already carries the connection state and
|
||||
// the top-right tunnel/SFTP icons signal "this is an SSH pane" — an in-pane
|
||||
// chip here would just float over the shell output. Only surface the strip
|
||||
// while still connecting (blank pane, no overlap) or after a drop (the
|
||||
// actionable reconnect notice).
|
||||
if matches!(phase, SshPhase::Connected) && !disconnected {
|
||||
view.ssh_phase()?;
|
||||
if !view.ssh_disconnected() {
|
||||
return None;
|
||||
}
|
||||
let host = view
|
||||
@@ -45,69 +38,54 @@ impl Tty7App {
|
||||
.unwrap_or_default();
|
||||
|
||||
let theme = cx.theme();
|
||||
// Phase → accent. Connecting/authenticating are cautionary (yellow),
|
||||
// connected reads calm (accent), failed/disconnected are red.
|
||||
let (color, label) = if disconnected {
|
||||
(theme.danger, "SSH ✕")
|
||||
} else {
|
||||
match &phase {
|
||||
SshPhase::Connecting => (theme.warning, "SSH …"),
|
||||
SshPhase::Authenticating => (theme.warning, "SSH ⚿"),
|
||||
SshPhase::Connected => (theme.accent, "SSH"),
|
||||
SshPhase::Failed { .. } => (theme.danger, "SSH ✕"),
|
||||
}
|
||||
};
|
||||
|
||||
let chip = h_flex()
|
||||
// The failure reason is already printed into the terminal buffer
|
||||
// (top-left, in red) by the daemon, so a top-left overlay would sit right
|
||||
// on top of it. Dock the (actionable) reconnect notice at the
|
||||
// bottom-center — clear of the output, a familiar "connection lost,
|
||||
// reconnect" spot.
|
||||
let bar = h_flex()
|
||||
.occlude()
|
||||
.items_center()
|
||||
.gap_1p5()
|
||||
.px_2()
|
||||
.py_0p5()
|
||||
.rounded_md()
|
||||
.bg(color.opacity(0.15))
|
||||
.gap_2()
|
||||
.px_3()
|
||||
.py_1p5()
|
||||
.rounded_lg()
|
||||
.bg(theme.popover)
|
||||
.border_1()
|
||||
.border_color(color.opacity(0.5))
|
||||
.border_color(theme.danger.opacity(0.4))
|
||||
.shadow_md()
|
||||
.text_xs()
|
||||
.text_color(color)
|
||||
.child(div().font_weight(FontWeight::SEMIBOLD).child(label))
|
||||
.when(!host.is_empty(), |d| {
|
||||
d.child(div().text_color(theme.muted_foreground).child(host))
|
||||
});
|
||||
|
||||
let mut col = div().flex().flex_col().items_start().gap_1().child(chip);
|
||||
|
||||
if disconnected {
|
||||
col = col.child(
|
||||
h_flex()
|
||||
.items_center()
|
||||
.gap_2()
|
||||
.px_2()
|
||||
.py_1()
|
||||
.rounded_md()
|
||||
.bg(theme.danger.opacity(0.12))
|
||||
.border_1()
|
||||
.border_color(theme.danger.opacity(0.4))
|
||||
.text_xs()
|
||||
.text_color(theme.muted_foreground)
|
||||
.child(
|
||||
div()
|
||||
.font_weight(FontWeight::MEDIUM)
|
||||
.text_color(theme.foreground)
|
||||
.child("Connection lost — press ⌘⇧R to reconnect")
|
||||
.child(
|
||||
Button::new("ssh-reconnect")
|
||||
.label("Reconnect")
|
||||
.primary()
|
||||
.small()
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
this.restart_ssh_session(window, cx)
|
||||
})),
|
||||
.child(if host.is_empty() {
|
||||
"Disconnected".to_string()
|
||||
} else {
|
||||
format!("Disconnected from {host}")
|
||||
}),
|
||||
)
|
||||
.child(div().child("· ⌘⇧R"))
|
||||
.child(
|
||||
Button::new("ssh-reconnect")
|
||||
.label("Reconnect")
|
||||
.primary()
|
||||
.small()
|
||||
.on_click(
|
||||
cx.listener(|this, _, window, cx| this.restart_ssh_session(window, cx)),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Some(
|
||||
div()
|
||||
.absolute()
|
||||
.top_2()
|
||||
.left_4()
|
||||
.child(col)
|
||||
.left_0()
|
||||
.right_0()
|
||||
.bottom_4()
|
||||
.flex()
|
||||
.justify_center()
|
||||
.child(bar)
|
||||
.into_any_element(),
|
||||
)
|
||||
}
|
||||
|
||||
+1
-1
@@ -181,7 +181,7 @@ impl Theme {
|
||||
}
|
||||
|
||||
/// Blend `a` toward `b` by `t` (0.0 = all `a`, 1.0 = all `b`), per channel.
|
||||
fn mix(a: u32, b: u32, t: f32) -> u32 {
|
||||
pub(crate) fn mix(a: u32, b: u32, t: f32) -> u32 {
|
||||
let (ar, ag, ab) = (a >> 16 & 0xff, a >> 8 & 0xff, a & 0xff);
|
||||
let (br, bg, bb) = (b >> 16 & 0xff, b >> 8 & 0xff, b & 0xff);
|
||||
let ch = |x: u32, y: u32| (x as f32 + (y as f32 - x as f32) * t).round() as u32;
|
||||
|
||||
+15
-14
@@ -21,8 +21,9 @@ use gpui::{
|
||||
Subscription, Window, div, prelude::*, px,
|
||||
};
|
||||
use gpui_component::button::{Button, ButtonVariants as _};
|
||||
use gpui_component::checkbox::Checkbox;
|
||||
use gpui_component::input::{Input, InputEvent, InputState};
|
||||
use gpui_component::{ActiveTheme as _, Selectable as _, Sizable as _, h_flex, v_flex};
|
||||
use gpui_component::{ActiveTheme as _, Sizable as _, h_flex, v_flex};
|
||||
|
||||
use crate::core::keychain::{CredentialStore as _, OsCredentialStore};
|
||||
use crate::daemon::protocol::{AuthPromptKind, AuthResponse, SshPhase};
|
||||
@@ -668,9 +669,9 @@ impl Tty7App {
|
||||
.occlude()
|
||||
.track_focus(&self.ssh_prompt.focus_handle)
|
||||
.key_context("SshPrompt")
|
||||
.w(px(460.))
|
||||
.gap_2()
|
||||
.p_3()
|
||||
.w(px(420.))
|
||||
.gap_3()
|
||||
.p_4()
|
||||
.bg(cx.theme().popover)
|
||||
.border_1()
|
||||
.rounded_lg()
|
||||
@@ -835,16 +836,16 @@ impl Tty7App {
|
||||
}
|
||||
|
||||
fn render_ssh_remember(&self, cx: &mut Context<Self>) -> AnyElement {
|
||||
Button::new("ssh-remember")
|
||||
.label(if self.ssh_prompt.remember {
|
||||
"☑ Remember (keychain)"
|
||||
} else {
|
||||
"☐ Remember (keychain)"
|
||||
})
|
||||
.small()
|
||||
.ghost()
|
||||
.selected(self.ssh_prompt.remember)
|
||||
.on_click(cx.listener(|this, _, _w, cx| this.toggle_ssh_remember(cx)))
|
||||
// A real checkbox, left-aligned in its own row. The old ghost Button
|
||||
// stretched to the card's full width, so its selected-state fill read as
|
||||
// a full-width grey bar rather than a checkbox.
|
||||
h_flex()
|
||||
.child(
|
||||
Checkbox::new("ssh-remember")
|
||||
.label("Remember (keychain)")
|
||||
.checked(self.ssh_prompt.remember)
|
||||
.on_click(cx.listener(|this, _, _w, cx| this.toggle_ssh_remember(cx))),
|
||||
)
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
|
||||
@@ -136,6 +136,28 @@ pub(crate) fn apply_theme(mut window: Option<&mut Window>, cx: &mut App) {
|
||||
t.tokens.accent = Hsla::from(accent_fill).into();
|
||||
t.tokens.accent_foreground = accent_text.into();
|
||||
|
||||
// Primary buttons (Connect, Reconnect, Trust, Save…) fill from
|
||||
// `tokens.button_primary`; the raw default is the foreground — a pure
|
||||
// near-black in a light theme, which reads harsh. Nudge it toward the
|
||||
// background so it lands on a softer dark charcoal (and, symmetrically, a
|
||||
// slightly dimmed near-white in dark themes). We set the whole primary token
|
||||
// family (both the plain `primary*` fields — used for the border and outline
|
||||
// text — and the `button_primary*` tokens the fill actually reads) so every
|
||||
// primary button shifts together, hover/pressed included. The stock
|
||||
// `button_primary_foreground` stays legible on top either way.
|
||||
let primary_base: Hsla = rgb(presets::mix(m.foreground, m.background, 0.20)).into();
|
||||
let primary_hover: Hsla = rgb(presets::mix(m.foreground, m.background, 0.30)).into();
|
||||
let primary_active: Hsla = rgb(presets::mix(m.foreground, m.background, 0.10)).into();
|
||||
t.primary = primary_base;
|
||||
t.primary_hover = primary_hover;
|
||||
t.primary_active = primary_active;
|
||||
t.tokens.primary = primary_base.into();
|
||||
t.tokens.primary_hover = primary_hover.into();
|
||||
t.tokens.primary_active = primary_active.into();
|
||||
t.tokens.button_primary = primary_base.into();
|
||||
t.tokens.button_primary_hover = primary_hover.into();
|
||||
t.tokens.button_primary_active = primary_active.into();
|
||||
|
||||
t.caret = rgb(m.caret).into();
|
||||
t.selection = rgb(m.selection).into(); // text selection highlight
|
||||
|
||||
|
||||
Reference in New Issue
Block a user