//! Data model shared by storage, the session manager and the UI. Field names serialise exactly as //! the Go structs did, so database JSON columns and `.link`/`.nexus` exports round-trip unchanged. use chrono::{DateTime, FixedOffset, Local}; use serde::{Deserialize, Deserializer, Serialize}; use std::collections::BTreeMap; pub const DEFAULT_SSH_PORT: u16 = 22; pub const DEFAULT_TELNET_PORT: u16 = 23; pub const DEFAULT_RAW_PORT: u16 = 23; pub const DEFAULT_GITHUB_PORT: u16 = 443; pub const DEFAULT_VNC_PORT: u16 = 5900; pub const DEFAULT_IRC_PORT: u16 = 6667; pub const DEFAULT_IRC_TLS_PORT: u16 = 6697; pub const DEFAULT_CONNECT_TIMEOUT: i64 = 30; pub const DEFAULT_SCROLLBACK: i64 = 10000; pub const DEFAULT_FONT_SIZE: i64 = 14; pub const DEFAULT_FONT_FAMILY: &str = "JetBrains Mono, Menlo, Monaco, Consolas, monospace"; pub const DEFAULT_CURSOR_STYLE: &str = "block"; pub const DEFAULT_TERMINAL_TYPE: &str = "xterm-256color"; pub const DEFAULT_KEEPALIVE_INTERVAL: i64 = 30; pub const DEFAULT_KEEPALIVE_COUNT_MAX: i64 = 3; pub type Time = DateTime; pub fn now() -> Time { Local::now().fixed_offset() } /// Go's zero `time.Time`, which is what a missing timestamp decodes to. pub fn zero_time() -> Time { DateTime::parse_from_rfc3339("0001-01-01T00:00:00Z").unwrap() } /// Go writes nil slices and maps as `null`; treat that as empty. fn nd<'de, D: Deserializer<'de>, T: Default + Deserialize<'de>>(d: D) -> Result { Ok(Option::::deserialize(d)?.unwrap_or_default()) } #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, Default)] #[serde(rename_all = "lowercase")] pub enum Protocol { #[default] Ssh, Telnet, Raw, Github, Vnc, Irc, Serial, Local, } impl Protocol { pub const SAVED: [Protocol; 7] = [Protocol::Ssh, Protocol::Telnet, Protocol::Raw, Protocol::Github, Protocol::Vnc, Protocol::Irc, Protocol::Serial]; pub fn id(self) -> &'static str { match self { Protocol::Ssh => "ssh", Protocol::Telnet => "telnet", Protocol::Raw => "raw", Protocol::Github => "github", Protocol::Vnc => "vnc", Protocol::Irc => "irc", Protocol::Serial => "serial", Protocol::Local => "local", } } pub fn from_id(s: &str) -> Option { [Protocol::Local].into_iter().chain(Self::SAVED).find(|p| p.id() == s) } pub fn label(self) -> &'static str { match self { Protocol::Ssh => "SSH", Protocol::Telnet => "Telnet", Protocol::Raw => "RAW TCP", Protocol::Github => "GitHub", Protocol::Vnc => "VNC", Protocol::Irc => "IRC", Protocol::Serial => "Serial", Protocol::Local => "Local", } } /// Protocols whose sessions render in a terminal view. pub fn is_terminal(self) -> bool { matches!(self, Protocol::Ssh | Protocol::Telnet | Protocol::Serial | Protocol::Local) } } #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, Default)] pub enum HostKeyPolicy { #[serde(rename = "strict")] Strict, #[default] #[serde(rename = "ask")] Ask, #[serde(rename = "accept-new")] AcceptNew, #[serde(rename = "insecure")] Insecure, } impl HostKeyPolicy { pub const ALL: [HostKeyPolicy; 4] = [Self::Strict, Self::Ask, Self::AcceptNew, Self::Insecure]; pub fn label(self) -> &'static str { match self { Self::Strict => "Strict (known hosts only)", Self::Ask => "Ask on new or changed keys", Self::AcceptNew => "Accept new, reject changed", Self::Insecure => "Insecure (accept anything)", } } } /// How a credential is obtained. Unknown strings (and the legacy "prompt") are kept as /// ask-every-time, matching Go's normaliseSecretMode. #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Default)] pub enum SecretMode { #[default] #[serde(rename = "saved")] Saved, #[serde(rename = "session-only")] SessionOnly, #[serde(rename = "ask-every-time")] AskEveryTime, } impl<'de> Deserialize<'de> for SecretMode { fn deserialize>(d: D) -> Result { Ok(match Option::::deserialize(d)?.as_deref().unwrap_or("") { "saved" | "" => SecretMode::Saved, "session-only" => SecretMode::SessionOnly, _ => SecretMode::AskEveryTime, }) } } impl SecretMode { pub const ALL: [SecretMode; 3] = [Self::Saved, Self::SessionOnly, Self::AskEveryTime]; pub fn label(self) -> &'static str { match self { Self::Saved => "Save securely", Self::SessionOnly => "Remember for this session", Self::AskEveryTime => "Ask every time", } } } #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, Default)] #[serde(rename_all = "lowercase")] pub enum LineEnding { Lf, Cr, #[default] Crlf, None, } impl LineEnding { pub const ALL: [LineEnding; 4] = [Self::Crlf, Self::Lf, Self::Cr, Self::None]; pub fn bytes(self) -> &'static [u8] { match self { Self::Lf => b"\n", Self::Cr => b"\r", Self::Crlf => b"\r\n", Self::None => b"", } } pub fn label(self) -> &'static str { match self { Self::Lf => "LF (\\n)", Self::Cr => "CR (\\r)", Self::Crlf => "CRLF (\\r\\n)", Self::None => "None", } } } #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct Profile { #[serde(default)] pub id: String, #[serde(default)] pub display_name: String, #[serde(default)] pub description: String, #[serde(default)] pub protocol: Protocol, #[serde(default)] pub host: String, #[serde(default)] pub port: i64, #[serde(default)] pub username: String, #[serde(default, deserialize_with = "nd")] pub tags: Vec, #[serde(default)] pub folder: String, #[serde(default)] pub environment: String, #[serde(default)] pub notes: String, #[serde(default)] pub favourite: bool, #[serde(default)] pub last_connected: Option