Skip to content

Commit e51f0e1

Browse files
committed
feat: dimmed overlay backgrounds and subtle form dividers (v2.22.0)
1 parent 996f7ec commit e51f0e1

10 files changed

Lines changed: 65 additions & 12 deletions

File tree

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1+
## 2.22.0
2+
3+
- Dimmed background behind overlays for better visual depth and focus
4+
- Form divider lines rendered dim to establish clear visual hierarchy (border > labels > dividers)
5+
- Three-tier color support: truecolor (dark grey fg), ANSI 16 (DarkGray), NO_COLOR (DIM modifier only)
6+
- Consistent dimming across open animation, steady-state and close animation
7+
18
## 2.21.0
29

310
- Host patterns support

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "purple-ssh"
3-
version = "2.21.0"
3+
version = "2.22.0"
44
edition = "2024"
55
description = "Search your SSH config instantly, connect to any host with one keystroke and sync servers from 16 cloud providers. Visual file transfer, container management over SSH, password management, command snippets and MCP server for AI agent integration. Edits ~/.ssh/config with round-trip fidelity."
66
license = "MIT"

demo.gif

1.66 MB
Loading

demo.webm

177 KB
Binary file not shown.

llms.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -345,7 +345,7 @@ A: Yes. purple uses the Bitwarden CLI (bw) for Bitwarden password sources. If yo
345345

346346
## Status
347347

348-
- Current version: 2.21.0 (March 2026)
348+
- Current version: 2.22.0 (March 2026)
349349
- Release cadence: approximately bi-weekly
350350
- Test suite: 5000+ tests (unit, integration, property-based and HTTP mocking)
351351
- CI: fmt, clippy, test on macOS and Linux, cargo-deny, MSRV 1.86 check

site/page.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@
4141
"url": "https://getpurple.sh",
4242
"downloadUrl": "https://getpurple.sh",
4343
"installUrl": "https://github.com/erickochen/purple/releases",
44-
"softwareVersion": "2.21.0",
44+
"softwareVersion": "2.22.0",
4545
"datePublished": "2024-10-01",
4646
"dateModified": "2026-03-26",
4747
"softwareRequirements": "macOS or Linux",
@@ -1004,7 +1004,7 @@ <h2>FAQ</h2>
10041004
</main>
10051005

10061006
<footer>
1007-
<a href="https://github.com/erickochen/purple" rel="noopener">GitHub</a> · <a href="https://crates.io/crates/purple-ssh" rel="noopener">crates.io</a> · MIT License · v2.21.0
1007+
<a href="https://github.com/erickochen/purple" rel="noopener">GitHub</a> · <a href="https://crates.io/crates/purple-ssh" rel="noopener">crates.io</a> · MIT License · v2.22.0
10081008
</footer>
10091009

10101010
<script>

site/worker.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,7 @@ const LANDING_PAGE = `<!DOCTYPE html>
177177
"url": "https://getpurple.sh",
178178
"downloadUrl": "https://getpurple.sh",
179179
"installUrl": "https://github.com/erickochen/purple/releases",
180-
"softwareVersion": "2.21.0",
180+
"softwareVersion": "2.22.0",
181181
"datePublished": "2024-10-01",
182182
"dateModified": "2026-03-26",
183183
"softwareRequirements": "macOS or Linux",
@@ -1140,7 +1140,7 @@ footer a:hover { color: var(--accent); }
11401140
</main>
11411141
11421142
<footer>
1143-
<a href="https://github.com/erickochen/purple" rel="noopener">GitHub</a> · <a href="https://crates.io/crates/purple-ssh" rel="noopener">crates.io</a> · MIT License · v2.21.0
1143+
<a href="https://github.com/erickochen/purple" rel="noopener">GitHub</a> · <a href="https://crates.io/crates/purple-ssh" rel="noopener">crates.io</a> · MIT License · v2.22.0
11441144
</footer>
11451145
11461146
<script>
@@ -1502,7 +1502,7 @@ A: Yes. purple uses the Bitwarden CLI (bw) for Bitwarden password sources. If yo
15021502
15031503
## Status
15041504
1505-
- Current version: 2.21.0 (March 2026)
1505+
- Current version: 2.22.0 (March 2026)
15061506
- Release cadence: approximately bi-weekly
15071507
- Test suite: 5000+ tests (unit, integration, property-based and HTTP mocking)
15081508
- CI: fmt, clippy, test on macOS and Linux, cargo-deny, MSRV 1.86 check

src/ui/mod.rs

Lines changed: 45 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ mod tunnel_list;
2020

2121
use ratatui::Frame;
2222
use ratatui::layout::{Constraint, Layout, Rect};
23-
use ratatui::style::Style;
23+
use ratatui::style::{Modifier, Style};
2424
use ratatui::text::{Line, Span};
2525
use ratatui::widgets::Paragraph;
2626
use unicode_width::UnicodeWidthStr;
@@ -204,7 +204,11 @@ pub fn render(frame: &mut Frame, app: &mut App) {
204204
fn render_overlay(frame: &mut Frame, app: &mut App, f: impl FnOnce(&mut Frame, &mut App)) {
205205
let status = app.status.take();
206206

207-
// Save host list buffer before overlay renders (needed for open animation clip restore)
207+
dim_background(frame);
208+
209+
// Save dimmed host list buffer before overlay renders (needed for open
210+
// animation clip restore). Captured after dim so the background outside the
211+
// growing clip stays consistently dimmed during the animation.
208212
let progress = app.overlay_anim_progress();
209213
let animating_open = progress.is_some();
210214
let pre_overlay = if animating_open {
@@ -233,6 +237,33 @@ fn render_overlay(frame: &mut Frame, app: &mut App, f: impl FnOnce(&mut Frame, &
233237
app.status = status;
234238
}
235239

240+
/// Dim all cells in the frame buffer so the host list behind an overlay appears muted.
241+
/// On truecolor/ANSI-16 terminals the foreground is replaced with dark grey for a
242+
/// stronger effect. Cells that already have a coloured background (badges, selected
243+
/// row) only receive the DIM modifier so their text stays readable.
244+
fn dim_background(frame: &mut Frame) {
245+
use ratatui::style::Color;
246+
247+
let dim_only = Style::default().add_modifier(Modifier::DIM);
248+
let style = match theme::color_mode() {
249+
2 => Style::default()
250+
.fg(Color::Rgb(70, 70, 70))
251+
.add_modifier(Modifier::DIM),
252+
1 => Style::default()
253+
.fg(Color::DarkGray)
254+
.add_modifier(Modifier::DIM),
255+
_ => dim_only,
256+
};
257+
let area = frame.area();
258+
let buf = frame.buffer_mut();
259+
for y in area.y..area.y + area.height {
260+
for x in area.x..area.x + area.width {
261+
let has_bg = buf[(x, y)].bg != Color::Reset;
262+
buf[(x, y)].set_style(if has_bg { dim_only } else { style });
263+
}
264+
}
265+
}
266+
236267
/// Render the close animation: paint saved overlay buffer with shrinking scale clip.
237268
fn render_overlay_close(frame: &mut Frame, app: &mut App) {
238269
// Only run when a close animation is active
@@ -248,6 +279,10 @@ fn render_overlay_close(frame: &mut Frame, app: &mut App) {
248279

249280
if let Some(ref saved) = app.overlay_buffer {
250281
if progress > 0.0 {
282+
// Dim the host list so the background stays consistently muted
283+
// while the overlay shrinks away.
284+
dim_background(frame);
285+
251286
let area = frame.area();
252287
let (left, right, top, bottom) = scale_clip_rect(area, progress);
253288

@@ -380,6 +415,9 @@ pub(crate) fn truncate(s: &str, max_cols: usize) -> String {
380415
}
381416

382417
/// Render a horizontal divider: ├─ Label ───────┤
418+
/// The `├` and `┤` connectors use the border style so they blend with the outer
419+
/// border. The horizontal `─` fill is rendered DIM to keep dividers visually
420+
/// subordinate to the border.
383421
pub(crate) fn render_divider(
384422
frame: &mut Frame,
385423
block_area: Rect,
@@ -388,13 +426,16 @@ pub(crate) fn render_divider(
388426
label_style: Style,
389427
border_style: Style,
390428
) {
429+
let dim = theme::muted();
391430
let width = block_area.width as usize;
392431
let label_w = label.width();
393432
let fill = width.saturating_sub(3 + label_w);
394433
let line = Line::from(vec![
395-
Span::styled("├─", border_style),
434+
Span::styled("├", border_style),
435+
Span::styled("─", dim),
396436
Span::styled(label.to_string(), label_style),
397-
Span::styled(format!("{}┤", "─".repeat(fill)), border_style),
437+
Span::styled("─".repeat(fill), dim),
438+
Span::styled("┤", border_style),
398439
]);
399440
frame.render_widget(
400441
Paragraph::new(line),

src/ui/theme.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,11 @@ pub fn init() {
1717
}
1818
}
1919

20+
/// Current color mode: 0 = NO_COLOR, 1 = ANSI 16, 2 = truecolor.
21+
pub fn color_mode() -> u8 {
22+
COLOR_MODE.load(Ordering::Acquire)
23+
}
24+
2025
/// Brand badge: purple background with white text. The single splash of color.
2126
/// Truecolor: #9333EA purple bg. ANSI 16: Magenta bg. NO_COLOR: REVERSED.
2227
/// Removes DIM so border_style doesn't leak through ratatui's Style::patch().

0 commit comments

Comments
 (0)