Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 18 additions & 2 deletions src-tauri/src/bin/rssh/commands/group.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,20 @@ pub fn cmd_list_groups(conn: &CliCtx) -> 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_waves_enabled {
"on"
} else {
"off"
}
);
}
Ok(())
}
Expand All @@ -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_waves_enabled = confirm("Show group-color terminal waves", false);
let group = Group {
id: uuid::Uuid::new_v4().to_string(),
name,
color,
sort_order: parse_sort_order(&sort_order)?,
terminal_waves_enabled,
};
rssh_lib::db::group::insert(conn, &group)?;
println!("Group '{}' created.", group.name);
Expand All @@ -56,6 +68,10 @@ pub fn cmd_edit_group(conn: &CliCtx, name: &str) -> AppResult<()> {
updated.color = prompt_default("Color (#RRGGBB)", &current.color);
let sort_order = prompt_default("Sort order", &current.sort_order.to_string());
updated.sort_order = parse_sort_order(&sort_order)?;
updated.terminal_waves_enabled = confirm(
"Show group-color terminal waves",
current.terminal_waves_enabled,
);

rssh_lib::db::group::update(conn, &updated)?;
println!("Group '{}' updated.", updated.name);
Expand Down
35 changes: 28 additions & 7 deletions src-tauri/src/db/group.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,14 +73,15 @@ fn reconcile_sync_filter_for_group_delete(conn: &rusqlite::Connection, id: &str)
pub fn list(db: &Db) -> AppResult<Vec<Group>> {
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_waves_enabled FROM groups ORDER BY sort_order ASC, name ASC",
)?;
let rows = stmt.query_map([], |row| {
Ok(Group {
id: row.get(0)?,
name: row.get(1)?,
color: row.get(2)?,
sort_order: row.get(3)?,
terminal_waves_enabled: row.get(4)?,
})
})?;
Ok(rows.collect::<Result<Vec<_>, _>>()?)
Expand All @@ -89,14 +90,15 @@ pub fn list(db: &Db) -> AppResult<Vec<Group>> {
pub fn get(db: &Db, id: &str) -> AppResult<Group> {
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_waves_enabled FROM groups WHERE id = ?1",
params![id],
|row| {
Ok(Group {
id: row.get(0)?,
name: row.get(1)?,
color: row.get(2)?,
sort_order: row.get(3)?,
terminal_waves_enabled: row.get(4)?,
})
},
)
Expand All @@ -107,9 +109,9 @@ pub fn insert_tx(conn: &rusqlite::Connection, g: &Group) -> AppResult<()> {
validate_name(&g.name)?;
let color = normalize_group_color(&g.color)?;
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_waves_enabled) VALUES (?1, ?2, ?3, ?4, ?5) \
ON CONFLICT(id) DO UPDATE SET name=excluded.name, color=excluded.color, sort_order=excluded.sort_order, terminal_waves_enabled=excluded.terminal_waves_enabled",
params![g.id, g.name, color, g.sort_order, g.terminal_waves_enabled],
)?;
Ok(())
}
Expand All @@ -124,8 +126,8 @@ pub fn update(db: &Db, g: &Group) -> AppResult<()> {
let color = normalize_group_color(&g.color)?;
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_waves_enabled=?4 WHERE id=?5",
params![g.name, color, g.sort_order, g.terminal_waves_enabled, g.id],
)?;
Ok(())
}
Expand Down Expand Up @@ -183,6 +185,7 @@ mod tests {
name: name.into(),
color: "#FF0000".into(),
sort_order: 0,
terminal_waves_enabled: false,
}
}

Expand All @@ -202,6 +205,24 @@ mod tests {
}
}

#[test]
fn group_waves_survive_insert_update_and_upsert() {
let db = Db::open_in_memory().unwrap();
let mut group = mk_group("prod", "production");
group.terminal_waves_enabled = true;
insert(&db, &group).unwrap();
assert!(get(&db, "prod").unwrap().terminal_waves_enabled);
assert!(list(&db).unwrap()[0].terminal_waves_enabled);

group.terminal_waves_enabled = false;
update(&db, &group).unwrap();
assert!(!get(&db, "prod").unwrap().terminal_waves_enabled);

group.terminal_waves_enabled = true;
insert(&db, &group).unwrap();
assert!(get(&db, "prod").unwrap().terminal_waves_enabled);
}

#[test]
fn insert_then_get_roundtrip() {
let db = Db::open_in_memory().unwrap();
Expand Down
162 changes: 161 additions & 1 deletion src-tauri/src/db/schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use rusqlite::{params, Connection};

use crate::error::AppResult;

const SCHEMA_VERSION: u32 = 30;
const SCHEMA_VERSION: u32 = 32;

fn column_exists(conn: &Connection, table: &str, col: &str) -> AppResult<bool> {
let mut stmt = conn.prepare("SELECT 1 FROM pragma_table_info(?1) WHERE name = ?2")?;
Expand Down Expand Up @@ -667,6 +667,31 @@ 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 < 32 && table_exists(conn, "groups")? {
// Some development v31 databases already contain the boolean flag.
let needs_wave_flag = !column_exists(conn, "groups", "terminal_waves_enabled")?;
if needs_wave_flag {
conn.execute_batch(
"ALTER TABLE groups ADD COLUMN terminal_waves_enabled INTEGER NOT NULL DEFAULT 0;",
)?;
}
if column_exists(conn, "groups", "terminal_background")? {
if needs_wave_flag {
conn.execute_batch(
"UPDATE groups SET terminal_waves_enabled = (terminal_background IS NOT NULL);",
)?;
}
conn.execute_batch("ALTER TABLE groups DROP COLUMN terminal_background;")?;
}
}

if version < SCHEMA_VERSION {
conn.pragma_update(None, "user_version", SCHEMA_VERSION)?;
}
Expand All @@ -676,6 +701,141 @@ pub fn migrate(conn: &Connection) -> AppResult<()> {

#[cfg(test)]
mod tests {
#[test]
fn migration_32_preserves_groups_and_defaults_group_waves_to_disabled() {
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, bool) = conn
.query_row(
"SELECT name, terminal_waves_enabled FROM groups WHERE id = 'prod'",
[],
|row| Ok((row.get(0)?, row.get(1)?)),
)
.unwrap();
assert_eq!(group, ("Production".into(), false));
assert!(conn
.execute("UPDATE groups SET terminal_waves_enabled = NULL", [])
.is_err());
let version: u32 = conn
.pragma_query_value(None, "user_version", |row| row.get(0))
.unwrap();
assert_eq!(version, 32);
assert!(!super::column_exists(&conn, "groups", "terminal_background").unwrap());
}

#[test]
fn migration_32_converts_legacy_wave_colors_to_flags() {
let conn = Connection::open_in_memory().unwrap();
conn.execute_batch(
"PRAGMA foreign_keys = ON;
CREATE TABLE groups (
id TEXT PRIMARY KEY, name TEXT NOT NULL, color TEXT NOT NULL,
sort_order INTEGER NOT NULL, terminal_background TEXT DEFAULT NULL
);
INSERT INTO groups VALUES
('off', 'Disabled', '#112233', 1, NULL),
('on', 'Enabled', '#445566', 2, '#AABBCC');
CREATE TABLE profiles (id TEXT PRIMARY KEY, group_id TEXT REFERENCES groups(id));
INSERT INTO profiles VALUES ('ssh', 'on');
PRAGMA user_version = 31;",
)
.unwrap();

migrate(&conn).unwrap();
migrate(&conn).unwrap();

let groups: Vec<(String, String, String, i32, bool)> = conn
.prepare("SELECT id, name, color, sort_order, terminal_waves_enabled FROM groups ORDER BY id")
.unwrap()
.query_map([], |row| Ok((row.get(0)?, row.get(1)?, row.get(2)?, row.get(3)?, row.get(4)?)))
.unwrap()
.collect::<Result<_, _>>()
.unwrap();
assert_eq!(
groups,
vec![
("off".into(), "Disabled".into(), "#112233".into(), 1, false),
("on".into(), "Enabled".into(), "#445566".into(), 2, true),
]
);
let profile_group: String = conn
.query_row(
"SELECT group_id FROM profiles WHERE id = 'ssh'",
[],
|row| row.get(0),
)
.unwrap();
assert_eq!(profile_group, "on");
assert!(!column_exists(&conn, "groups", "terminal_background").unwrap());
assert!(conn
.execute("UPDATE groups SET terminal_waves_enabled = NULL", [])
.is_err());
conn.execute(
"INSERT INTO groups (id, name, color, sort_order) VALUES ('new', 'New', '#778899', 3)",
[],
)
.unwrap();
let enabled: bool = conn
.query_row(
"SELECT terminal_waves_enabled FROM groups WHERE id = 'new'",
[],
|row| row.get(0),
)
.unwrap();
assert!(!enabled);
let version: u32 = conn
.pragma_query_value(None, "user_version", |row| row.get(0))
.unwrap();
assert_eq!(version, 32);
}

#[test]
fn migration_32_preserves_existing_wave_flags() {
// Also cover a database where both columns are present: the flag wins.
for legacy_column in [false, true] {
let conn = 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,
terminal_waves_enabled INTEGER NOT NULL DEFAULT 0
);
INSERT INTO groups VALUES
('off', 'Disabled', '#112233', 1, 0),
('on', 'Enabled', '#445566', 2, 1);
PRAGMA user_version = 31;",
)
.unwrap();
if legacy_column {
conn.execute_batch(
"ALTER TABLE groups ADD COLUMN terminal_background TEXT DEFAULT NULL;
UPDATE groups SET terminal_background = '#AABBCC' WHERE id = 'off';",
)
.unwrap();
}

migrate(&conn).unwrap();
migrate(&conn).unwrap();

let flags: Vec<bool> = conn
.prepare("SELECT terminal_waves_enabled FROM groups ORDER BY id")
.unwrap()
.query_map([], |row| row.get(0))
.unwrap()
.collect::<Result<_, _>>()
.unwrap();
assert_eq!(flags, vec![false, true]);
assert!(!column_exists(&conn, "groups", "terminal_background").unwrap());
let version: u32 = conn
.pragma_query_value(None, "user_version", |row| row.get(0))
.unwrap();
assert_eq!(version, 32);
}
}
use super::*;

#[test]
Expand Down
3 changes: 3 additions & 0 deletions src-tauri/src/models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -491,6 +491,9 @@ pub struct Group {
pub name: String,
pub color: String,
pub sort_order: i32,
/// Enables terminal waves in the group's color.
#[serde(default)]
pub terminal_waves_enabled: bool,
}

// --- Highlight ---
Expand Down
42 changes: 42 additions & 0 deletions src-tauri/src/sync/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -870,6 +870,48 @@ mod tests {
v
}

#[test]
fn group_waves_import_defaults_and_boolean_exports() {
let (db, ss, dir) = fixture();
let legacy = json!({
"id": "legacy", "name": "Legacy", "color": "#FF0000", "sort_order": 0
});
let enabled = json!({
"id": "enabled", "name": "Enabled", "color": "#00FF00", "sort_order": 1,
"terminal_waves_enabled": true
});
let disabled = json!({
"id": "disabled", "name": "Disabled", "color": "#0000FF", "sort_order": 2,
"terminal_waves_enabled": false
});
merge_import(
&db,
&ss,
dir.path(),
&json!({
"groups": [legacy.clone(), enabled.clone(), disabled.clone()]
}),
)
.unwrap();
let mut expected_legacy = legacy;
expected_legacy["terminal_waves_enabled"] = json!(false);
let expected = json!([expected_legacy, enabled, disabled]);

for mode in [
ExportMode::LocalBackup,
ExportMode::RemotePush(read_sync_prefs(&db).unwrap()),
] {
let exported = build_payload(&db, &ss, dir.path(), &mode).unwrap();
assert_eq!(exported["groups"], expected);
let (restored, restored_ss, restored_dir) = fixture();
merge_import(&restored, &restored_ss, restored_dir.path(), &exported).unwrap();
assert_eq!(
serde_json::to_value(group::list(&restored).unwrap()).unwrap(),
expected
);
}
}

#[test]
fn merge_keeps_local_only_rows() {
let (db, ss, dir) = fixture();
Expand Down
Loading
Loading