use std::collections::{HashMap, HashSet};
use std::fs;
use std::path::PathBuf;
use serde::{Deserialize, Serialize};
use crate::config::TunnelConfig;
#[derive(Debug, Clone)]
pub struct EffectiveTunnel {
pub config: TunnelConfig,
pub yaml_index: Option<usize>,
pub user_idx: usize,
pub is_overridden: bool,
}
pub fn effective_tunnels_for(
yaml_tunnels: &[TunnelConfig],
server_key: &str,
overrides: &[TunnelOverride],
) -> Vec<EffectiveTunnel> {
let server_overrides: Vec<&TunnelOverride> = overrides
.iter()
.filter(|o| o.server_key == server_key)
.collect();
let mut result = Vec::new();
for (i, yaml_cfg) in yaml_tunnels.iter().enumerate() {
let override_entry = server_overrides.iter().find(|o| o.yaml_index == Some(i));
match override_entry {
Some(o) if o.hidden => {
}
Some(o) => {
result.push(EffectiveTunnel {
config: o.config.clone(),
yaml_index: Some(i),
user_idx: 0,
is_overridden: true,
});
}
None => {
result.push(EffectiveTunnel {
config: yaml_cfg.clone(),
yaml_index: Some(i),
user_idx: 0,
is_overridden: false,
});
}
}
}
let user_tunnels: Vec<&TunnelOverride> = server_overrides
.iter()
.filter(|o| o.yaml_index.is_none() && !o.hidden)
.copied()
.collect();
for (user_idx, o) in user_tunnels.iter().enumerate() {
result.push(EffectiveTunnel {
config: o.config.clone(),
yaml_index: None,
user_idx,
is_overridden: true,
});
}
result
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TunnelOverride {
pub server_key: String,
pub yaml_index: Option<usize>,
pub config: TunnelConfig,
pub hidden: bool,
}
#[derive(Debug, Default, Serialize, Deserialize)]
pub struct AppState {
pub expanded_items: HashSet<String>,
#[serde(default)]
pub last_seen: HashMap<String, u64>,
#[serde(default)]
pub favorites: HashSet<String>,
#[serde(default)]
pub sort_by_recent: bool,
#[serde(default)]
pub tunnel_overrides: Vec<TunnelOverride>,
#[serde(default)]
pub command_history: Vec<String>,
}
fn state_path() -> PathBuf {
let raw = shellexpand::tilde("~/.susshi_state.json");
PathBuf::from(raw.as_ref())
}
pub fn load_state() -> AppState {
let path = state_path();
let Ok(content) = fs::read_to_string(&path) else {
return AppState::default();
};
serde_json::from_str(&content).unwrap_or_default()
}
pub fn save_state(state: &AppState) {
let path = state_path();
if let Ok(json) = serde_json::to_string_pretty(state) {
let _ = fs::write(path, json);
}
}
#[cfg(test)]
mod tests {
use super::*;
fn t(local: u16, remote: u16, label: &str) -> TunnelConfig {
TunnelConfig {
local_port: local,
remote_host: "127.0.0.1".into(),
remote_port: remote,
label: label.into(),
}
}
const KEY: &str = "Group:G:Server:S";
#[test]
fn no_overrides_returns_yaml() {
let yaml = vec![t(5432, 5432, "pg"), t(6379, 6379, "redis")];
let result = effective_tunnels_for(&yaml, KEY, &[]);
assert_eq!(result.len(), 2);
assert_eq!(result[0].config.label, "pg");
assert_eq!(result[0].yaml_index, Some(0));
assert!(!result[0].is_overridden);
assert_eq!(result[1].config.label, "redis");
assert_eq!(result[1].yaml_index, Some(1));
assert!(!result[1].is_overridden);
}
#[test]
fn override_replaces_yaml_tunnel() {
let yaml = vec![t(5432, 5432, "pg")];
let overrides = vec![TunnelOverride {
server_key: KEY.into(),
yaml_index: Some(0),
config: t(15432, 5432, "pg-edited"),
hidden: false,
}];
let result = effective_tunnels_for(&yaml, KEY, &overrides);
assert_eq!(result.len(), 1);
assert_eq!(result[0].config.label, "pg-edited");
assert_eq!(result[0].config.local_port, 15432);
assert!(result[0].is_overridden);
}
#[test]
fn hidden_override_removes_yaml_tunnel() {
let yaml = vec![t(5432, 5432, "pg"), t(6379, 6379, "redis")];
let overrides = vec![TunnelOverride {
server_key: KEY.into(),
yaml_index: Some(0),
config: t(5432, 5432, "pg"),
hidden: true,
}];
let result = effective_tunnels_for(&yaml, KEY, &overrides);
assert_eq!(result.len(), 1);
assert_eq!(result[0].config.label, "redis");
assert_eq!(result[0].yaml_index, Some(1));
}
#[test]
fn user_tunnel_appended_after_yaml() {
let yaml = vec![t(5432, 5432, "pg")];
let overrides = vec![TunnelOverride {
server_key: KEY.into(),
yaml_index: None,
config: t(8080, 8080, "web"),
hidden: false,
}];
let result = effective_tunnels_for(&yaml, KEY, &overrides);
assert_eq!(result.len(), 2);
assert_eq!(result[0].yaml_index, Some(0));
assert_eq!(result[1].yaml_index, None);
assert_eq!(result[1].config.label, "web");
assert_eq!(result[1].user_idx, 0);
}
#[test]
fn hidden_user_tunnel_not_shown() {
let yaml = vec![];
let overrides = vec![TunnelOverride {
server_key: KEY.into(),
yaml_index: None,
config: t(8080, 8080, "web"),
hidden: true,
}];
let result = effective_tunnels_for(&yaml, KEY, &overrides);
assert!(result.is_empty());
}
#[test]
fn overrides_for_other_server_ignored() {
let yaml = vec![t(5432, 5432, "pg")];
let overrides = vec![TunnelOverride {
server_key: "Group:Other:Server:X".into(),
yaml_index: Some(0),
config: t(9999, 9999, "wrong"),
hidden: false,
}];
let result = effective_tunnels_for(&yaml, KEY, &overrides);
assert_eq!(result.len(), 1);
assert_eq!(result[0].config.label, "pg");
assert!(!result[0].is_overridden);
}
#[test]
fn multiple_user_tunnels_indexed_correctly() {
let yaml = vec![];
let overrides = vec![
TunnelOverride {
server_key: KEY.into(),
yaml_index: None,
config: t(8080, 8080, "web"),
hidden: false,
},
TunnelOverride {
server_key: KEY.into(),
yaml_index: None,
config: t(9090, 9090, "metrics"),
hidden: false,
},
];
let result = effective_tunnels_for(&yaml, KEY, &overrides);
assert_eq!(result.len(), 2);
assert_eq!(result[0].user_idx, 0);
assert_eq!(result[1].user_idx, 1);
}
}