From b22beb544a11f5d92fe08c8a525b19ccc9ed28ac Mon Sep 17 00:00:00 2001 From: xinhua pu Date: Thu, 15 Oct 2026 14:37:15 +0800 Subject: [PATCH 1/5] feat: add group-colored terminal wave accents --- docs/group-terminal-waves.jpg | Bin 0 -> 27470 bytes src-tauri/src/bin/rssh/commands/group.rs | 22 +++++- src-tauri/src/db/group.rs | 62 +++++++++++++++-- src-tauri/src/db/schema.rs | 26 +++++++- src-tauri/src/models.rs | 3 + src/lib/components/EnvironmentWave.svelte | 63 ++++++++++++++++++ src/lib/components/GroupManager.svelte | 35 ++++++++++ src/lib/components/ProfileEditor.svelte | 2 + src/lib/components/SerialProfileEditor.svelte | 2 + src/lib/components/TelnetProfileEditor.svelte | 2 + src/lib/components/TerminalPane.svelte | 46 ++++++++++++- src/lib/i18n/locales/en.ts | 3 + src/lib/i18n/locales/zh.ts | 3 + src/lib/stores/app.svelte.ts | 1 + src/lib/terminal/environment-wave.test.ts | 28 ++++++++ src/lib/terminal/environment-wave.ts | 7 ++ 16 files changed, 292 insertions(+), 13 deletions(-) create mode 100644 docs/group-terminal-waves.jpg create mode 100644 src/lib/components/EnvironmentWave.svelte create mode 100644 src/lib/terminal/environment-wave.test.ts create mode 100644 src/lib/terminal/environment-wave.ts diff --git a/docs/group-terminal-waves.jpg b/docs/group-terminal-waves.jpg new file mode 100644 index 0000000000000000000000000000000000000000..f70da4fbebf2b5b6f6e356123a891cbd04bd7bd1 GIT binary patch literal 27470 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 AppResult<()> { return Ok(()); } - println!("{:<24} {:<9} ORDER", "NAME", "COLOR"); + println!("{:<24} {:<9} ORDER WAVES", "NAME", "COLOR"); println!("{}", "-".repeat(42)); for group in groups { - println!("{:<24} {:<9} {}", group.name, group.color, group.sort_order); + println!( + "{:<24} {:<9} {} {}", + group.name, + group.color, + group.sort_order, + if group.terminal_background.is_some() { + "on" + } else { + "off" + } + ); } Ok(()) } @@ -34,11 +44,13 @@ pub fn cmd_add_group(conn: &CliCtx) -> AppResult<()> { let name = prompt("Name: "); let color = prompt_default("Color (#RRGGBB)", "#4A6CF7"); let sort_order = prompt_default("Sort order", "0"); + let terminal_background = wave_prompt(&color, false); let group = Group { id: uuid::Uuid::new_v4().to_string(), name, color, sort_order: parse_sort_order(&sort_order)?, + terminal_background, }; rssh_lib::db::group::insert(conn, &group)?; println!("Group '{}' created.", group.name); @@ -56,6 +68,8 @@ pub fn cmd_edit_group(conn: &CliCtx, name: &str) -> AppResult<()> { updated.color = prompt_default("Color (#RRGGBB)", ¤t.color); let sort_order = prompt_default("Sort order", ¤t.sort_order.to_string()); updated.sort_order = parse_sort_order(&sort_order)?; + updated.terminal_background = + wave_prompt(&updated.color, current.terminal_background.is_some()); rssh_lib::db::group::update(conn, &updated)?; println!("Group '{}' updated.", updated.name); @@ -77,3 +91,7 @@ pub fn cmd_rm_group(conn: &CliCtx, name: &str) -> AppResult<()> { println!("Deleted."); Ok(()) } + +fn wave_prompt(color: &str, enabled: bool) -> Option { + confirm("Show group-color terminal waves", enabled).then(|| color.to_owned()) +} diff --git a/src-tauri/src/db/group.rs b/src-tauri/src/db/group.rs index 07e21aec..12665289 100644 --- a/src-tauri/src/db/group.rs +++ b/src-tauri/src/db/group.rs @@ -73,7 +73,7 @@ fn reconcile_sync_filter_for_group_delete(conn: &rusqlite::Connection, id: &str) pub fn list(db: &Db) -> AppResult> { let conn = db.lock()?; let mut stmt = conn.prepare( - "SELECT id, name, color, sort_order FROM groups ORDER BY sort_order ASC, name ASC", + "SELECT id, name, color, sort_order, terminal_background FROM groups ORDER BY sort_order ASC, name ASC", )?; let rows = stmt.query_map([], |row| { Ok(Group { @@ -81,6 +81,7 @@ pub fn list(db: &Db) -> AppResult> { name: row.get(1)?, color: row.get(2)?, sort_order: row.get(3)?, + terminal_background: row.get(4)?, }) })?; Ok(rows.collect::, _>>()?) @@ -89,7 +90,7 @@ pub fn list(db: &Db) -> AppResult> { pub fn get(db: &Db, id: &str) -> AppResult { let conn = db.lock()?; conn.query_row( - "SELECT id, name, color, sort_order FROM groups WHERE id = ?1", + "SELECT id, name, color, sort_order, terminal_background FROM groups WHERE id = ?1", params![id], |row| { Ok(Group { @@ -97,6 +98,7 @@ pub fn get(db: &Db, id: &str) -> AppResult { name: row.get(1)?, color: row.get(2)?, sort_order: row.get(3)?, + terminal_background: row.get(4)?, }) }, ) @@ -106,10 +108,15 @@ pub fn get(db: &Db, id: &str) -> AppResult { pub fn insert_tx(conn: &rusqlite::Connection, g: &Group) -> AppResult<()> { validate_name(&g.name)?; let color = normalize_group_color(&g.color)?; + let background = g + .terminal_background + .as_deref() + .map(normalize_group_color) + .transpose()?; conn.execute( - "INSERT INTO groups (id, name, color, sort_order) VALUES (?1, ?2, ?3, ?4) \ - ON CONFLICT(id) DO UPDATE SET name=excluded.name, color=excluded.color, sort_order=excluded.sort_order", - params![g.id, g.name, color, g.sort_order], + "INSERT INTO groups (id, name, color, sort_order, terminal_background) VALUES (?1, ?2, ?3, ?4, ?5) \ + ON CONFLICT(id) DO UPDATE SET name=excluded.name, color=excluded.color, sort_order=excluded.sort_order, terminal_background=excluded.terminal_background", + params![g.id, g.name, color, g.sort_order, background], )?; Ok(()) } @@ -122,10 +129,15 @@ pub fn insert(db: &Db, g: &Group) -> AppResult<()> { pub fn update(db: &Db, g: &Group) -> AppResult<()> { validate_name(&g.name)?; let color = normalize_group_color(&g.color)?; + let background = g + .terminal_background + .as_deref() + .map(normalize_group_color) + .transpose()?; let conn = db.lock()?; conn.execute( - "UPDATE groups SET name=?1, color=?2, sort_order=?3 WHERE id=?4", - params![g.name, color, g.sort_order, g.id], + "UPDATE groups SET name=?1, color=?2, sort_order=?3, terminal_background=?5 WHERE id=?4", + params![g.name, color, g.sort_order, g.id, background], )?; Ok(()) } @@ -183,6 +195,7 @@ mod tests { name: name.into(), color: "#FF0000".into(), sort_order: 0, + terminal_background: None, } } @@ -202,6 +215,41 @@ mod tests { } } + #[test] + fn terminal_background_survives_database_and_serialization_roundtrips() { + let db = Db::open_in_memory().unwrap(); + let mut group = mk_group("prod", "production"); + group.terminal_background = Some("#3b1518".into()); + insert(&db, &group).unwrap(); + let stored = get(&db, "prod").unwrap(); + assert_eq!(stored.terminal_background.as_deref(), Some("#3B1518")); + let payload = serde_json::to_value(&stored).unwrap(); + let restored: Group = serde_json::from_value(payload).unwrap(); + clear_all(&db).unwrap(); + insert(&db, &restored).unwrap(); + assert_eq!( + list(&db).unwrap()[0].terminal_background, + stored.terminal_background + ); + group.terminal_background = None; + update(&db, &group).unwrap(); + assert!(get(&db, "prod").unwrap().terminal_background.is_none()); + } + + #[test] + fn legacy_groups_use_global_theme_and_invalid_backgrounds_are_rejected() { + let db = Db::open_in_memory().unwrap(); + let mut group: Group = serde_json::from_value(serde_json::json!({ + "id": "old", "name": "legacy", "color": "#FF0000", "sort_order": 0 + })) + .unwrap(); + assert!(group.terminal_background.is_none()); + insert(&db, &group).unwrap(); + group.terminal_background = Some("#fff; color:red".into()); + assert!(update(&db, &group).is_err()); + assert!(get(&db, "old").unwrap().terminal_background.is_none()); + } + #[test] fn insert_then_get_roundtrip() { let db = Db::open_in_memory().unwrap(); diff --git a/src-tauri/src/db/schema.rs b/src-tauri/src/db/schema.rs index d0bad7d3..d89cae3b 100644 --- a/src-tauri/src/db/schema.rs +++ b/src-tauri/src/db/schema.rs @@ -2,7 +2,7 @@ use rusqlite::{params, Connection}; use crate::error::AppResult; -const SCHEMA_VERSION: u32 = 30; +const SCHEMA_VERSION: u32 = 31; fn column_exists(conn: &Connection, table: &str, col: &str) -> AppResult { let mut stmt = conn.prepare("SELECT 1 FROM pragma_table_info(?1) WHERE name = ?2")?; @@ -667,6 +667,13 @@ pub fn migrate(conn: &Connection) -> AppResult<()> { )?; } + if version < 31 + && table_exists(conn, "groups")? + && !column_exists(conn, "groups", "terminal_background")? + { + conn.execute_batch("ALTER TABLE groups ADD COLUMN terminal_background TEXT DEFAULT NULL;")?; + } + if version < SCHEMA_VERSION { conn.pragma_update(None, "user_version", SCHEMA_VERSION)?; } @@ -676,6 +683,23 @@ pub fn migrate(conn: &Connection) -> AppResult<()> { #[cfg(test)] mod tests { + #[test] + fn migration_31_preserves_groups_and_defaults_background_to_global() { + let conn = rusqlite::Connection::open_in_memory().unwrap(); + conn.execute_batch("CREATE TABLE groups (id TEXT PRIMARY KEY, name TEXT NOT NULL, color TEXT NOT NULL, sort_order INTEGER NOT NULL); + INSERT INTO groups VALUES ('prod', 'Production', '#FF0000', 1); + PRAGMA user_version = 30;").unwrap(); + super::migrate(&conn).unwrap(); + super::migrate(&conn).unwrap(); + let group: (String, Option) = conn + .query_row( + "SELECT name, terminal_background FROM groups WHERE id = 'prod'", + [], + |row| Ok((row.get(0)?, row.get(1)?)), + ) + .unwrap(); + assert_eq!(group, ("Production".into(), None)); + } use super::*; #[test] diff --git a/src-tauri/src/models.rs b/src-tauri/src/models.rs index a5b22e1d..fbf10392 100644 --- a/src-tauri/src/models.rs +++ b/src-tauri/src/models.rs @@ -491,6 +491,9 @@ pub struct Group { pub name: String, pub color: String, pub sort_order: i32, + /// Non-null enables terminal bottom waves; the rendered hue follows `color`. + #[serde(default)] + pub terminal_background: Option, } // --- Highlight --- diff --git a/src/lib/components/EnvironmentWave.svelte b/src/lib/components/EnvironmentWave.svelte new file mode 100644 index 00000000..8500e515 --- /dev/null +++ b/src/lib/components/EnvironmentWave.svelte @@ -0,0 +1,63 @@ + + +
+ +
{name}
+
+ + diff --git a/src/lib/components/GroupManager.svelte b/src/lib/components/GroupManager.svelte index 6f401268..86a52e0b 100644 --- a/src/lib/components/GroupManager.svelte +++ b/src/lib/components/GroupManager.svelte @@ -6,6 +6,9 @@ import { toast } from "../stores/toast.svelte.ts"; import { t, errMsg } from "../i18n/index.svelte.ts"; import AppIcon from "./AppIcon.svelte"; + import { invalidateConfiguration } from "../stores/sync.svelte.ts"; + import EnvironmentWave from "./EnvironmentWave.svelte"; + import * as theme from "../themes/store.svelte.ts"; let groups = $state([]); let adding = $state(false); @@ -15,6 +18,8 @@ let formName = $state(""); let formColor = $state("#4A6CF7"); let formOrder = $state(0); + let formBackgroundEnabled = $state(false); + let previewTheme = $derived(theme.currentTermTheme()); let deleting = $state(null); @@ -30,6 +35,7 @@ formName = ""; formColor = "#4A6CF7"; formOrder = 0; + formBackgroundEnabled = false; } function startEdit(g: Group) { @@ -38,6 +44,7 @@ formName = g.name; formColor = g.color; formOrder = g.sort_order; + formBackgroundEnabled = !!g.terminal_background; } function cancelForm() { @@ -52,9 +59,11 @@ name: formName.trim(), color: formColor, sort_order: formOrder, + terminal_background: formBackgroundEnabled ? formColor : null, }; try { await invoke("create_group", { group }); + invalidateConfiguration(); adding = false; await refresh(); } catch (e: any) { toast.error(`${t("toast.error.add")}: ${errMsg(e)}`); } @@ -67,9 +76,11 @@ name: formName.trim(), color: formColor, sort_order: formOrder, + terminal_background: formBackgroundEnabled ? formColor : null, }; try { await invoke("update_group", { group }); + invalidateConfiguration(); editId = null; await refresh(); } catch (e: any) { toast.error(`${t("toast.error.save")}: ${errMsg(e)}`); } @@ -79,6 +90,7 @@ deleting = id; try { await invoke("delete_group", { id }); + invalidateConfiguration(); if (editId === id) editId = null; await refresh(); } catch (e: any) { toast.error(`${t("toast.error.delete")}: ${errMsg(e)}`); } @@ -86,6 +98,20 @@ } +{#snippet backgroundFields()} + +

{t("group.terminal_background_hint")}

+ {#if formBackgroundEnabled} +
+
{formName || t("group.name_placeholder")} ~ # {t("group.preview")}
+ +
+ {/if} +{/snippet} +
@@ -97,6 +123,7 @@ + {@render backgroundFields()}
@@ -113,6 +140,7 @@ + {@render backgroundFields()}
@@ -127,6 +155,9 @@
{g.name}
{t("group.order", { n: g.sort_order })}
+ {#if g.terminal_background} +
{t("group.terminal_background")}: {g.color}
+ {/if}
@@ -160,6 +191,10 @@ diff --git a/src/lib/components/GroupManager.svelte b/src/lib/components/GroupManager.svelte index 86a52e0b..9b77d079 100644 --- a/src/lib/components/GroupManager.svelte +++ b/src/lib/components/GroupManager.svelte @@ -6,7 +6,7 @@ import { toast } from "../stores/toast.svelte.ts"; import { t, errMsg } from "../i18n/index.svelte.ts"; import AppIcon from "./AppIcon.svelte"; - import { invalidateConfiguration } from "../stores/sync.svelte.ts"; + import { notifyEnvironmentChanged } from "../terminal/environment-events.ts"; import EnvironmentWave from "./EnvironmentWave.svelte"; import * as theme from "../themes/store.svelte.ts"; @@ -63,7 +63,7 @@ }; try { await invoke("create_group", { group }); - invalidateConfiguration(); + await notifyEnvironmentChanged("group", group.id); adding = false; await refresh(); } catch (e: any) { toast.error(`${t("toast.error.add")}: ${errMsg(e)}`); } @@ -80,7 +80,7 @@ }; try { await invoke("update_group", { group }); - invalidateConfiguration(); + await notifyEnvironmentChanged("group", group.id); editId = null; await refresh(); } catch (e: any) { toast.error(`${t("toast.error.save")}: ${errMsg(e)}`); } @@ -90,7 +90,7 @@ deleting = id; try { await invoke("delete_group", { id }); - invalidateConfiguration(); + await notifyEnvironmentChanged("group", id); if (editId === id) editId = null; await refresh(); } catch (e: any) { toast.error(`${t("toast.error.delete")}: ${errMsg(e)}`); } diff --git a/src/lib/components/ProfileEditor.svelte b/src/lib/components/ProfileEditor.svelte index 9c5fabb1..28527cfe 100644 --- a/src/lib/components/ProfileEditor.svelte +++ b/src/lib/components/ProfileEditor.svelte @@ -1,5 +1,5 @@ -
- - {#if showName} -
{name}
- {/if} + diff --git a/src/lib/components/GroupManager.svelte b/src/lib/components/GroupManager.svelte index 9b77d079..d87012ad 100644 --- a/src/lib/components/GroupManager.svelte +++ b/src/lib/components/GroupManager.svelte @@ -107,7 +107,7 @@ {#if formBackgroundEnabled}
{formName || t("group.name_placeholder")} ~ # {t("group.preview")}
- +
{/if} {/snippet} @@ -193,8 +193,13 @@ .page { padding: 24px; } .background-toggle { display: flex; align-items: center; gap: 8px; } .background-hint { color: var(--text-sub); font-size: 12px; margin: 0; } - .terminal-preview { border-radius: 4px; overflow: hidden; font-family: monospace; } - .preview-content { padding: 12px 12px 20px; overflow-wrap: anywhere; } + .terminal-preview { + position: relative; + overflow: hidden; + border-radius: 4px; + font-family: monospace; + } + .preview-content { position: relative; z-index: 1; padding: 12px 12px 20px; overflow-wrap: anywhere; } .toolbar { display: flex; justify-content: flex-end; margin-bottom: 16px; } .item-row { display: flex; justify-content: space-between; align-items: center; gap: 12px; margin-bottom: 16px; } .item-info { display: flex; align-items: center; gap: 10px; min-width: 0; } diff --git a/src/lib/components/ImportExportScreen.svelte b/src/lib/components/ImportExportScreen.svelte index aa0eaa61..e130e835 100644 --- a/src/lib/components/ImportExportScreen.svelte +++ b/src/lib/components/ImportExportScreen.svelte @@ -1,4 +1,6 @@ -