1use serde::{Deserialize, Serialize};
18use std::collections::HashMap;
19
20#[derive(Debug, Serialize, Deserialize, Default, Clone)]
22pub struct Config {
23 #[serde(default)]
24 pub defaults: Defaults,
25
26 #[serde(default)]
27 pub clusters: HashMap<String, Cluster>,
28
29 #[serde(default)]
30 pub interactive: InteractiveConfig,
31}
32
33#[derive(Debug, Serialize, Deserialize, Clone)]
43#[serde(untagged)]
44pub enum JumpHostConfig {
45 SshConfigHostRef {
48 ssh_config_host: String,
50 },
51 Detailed {
53 host: String,
54 #[serde(default)]
55 user: Option<String>,
56 #[serde(default)]
57 port: Option<u16>,
58 #[serde(default)]
59 ssh_key: Option<String>,
60 },
61 Simple(String),
64}
65
66#[derive(Debug, Serialize, Deserialize, Default, Clone)]
68pub struct Defaults {
69 pub user: Option<String>,
70 pub port: Option<u16>,
71 pub ssh_key: Option<String>,
72 pub parallel: Option<usize>,
73 pub timeout: Option<u64>,
74 pub jump_host: Option<JumpHostConfig>,
78 pub server_alive_interval: Option<u64>,
82 pub server_alive_count_max: Option<usize>,
85}
86
87#[derive(Debug, Serialize, Deserialize, Default, Clone)]
89pub struct InteractiveConfig {
90 #[serde(default = "default_interactive_mode")]
91 pub default_mode: InteractiveMode,
92
93 #[serde(default = "default_prompt_format")]
94 pub prompt_format: String,
95
96 #[serde(default)]
97 pub history_file: Option<String>,
98
99 #[serde(default)]
100 pub colors: HashMap<String, String>,
101
102 #[serde(default)]
103 pub keybindings: KeyBindings,
104
105 #[serde(default)]
106 pub broadcast_prefix: Option<String>,
107
108 #[serde(default)]
109 pub node_switch_prefix: Option<String>,
110
111 #[serde(default)]
112 pub show_timestamps: bool,
113
114 #[serde(default)]
115 pub work_dir: Option<String>,
116}
117
118#[derive(Debug, Serialize, Deserialize, Clone)]
120#[serde(rename_all = "snake_case")]
121#[derive(Default)]
122pub enum InteractiveMode {
123 #[default]
124 SingleNode,
125 Multiplex,
126}
127
128#[derive(Debug, Serialize, Deserialize, Default, Clone)]
130pub struct KeyBindings {
131 #[serde(default = "default_switch_node")]
132 pub switch_node: String,
133
134 #[serde(default = "default_broadcast_toggle")]
135 pub broadcast_toggle: String,
136
137 #[serde(default = "default_quit")]
138 pub quit: String,
139
140 #[serde(default)]
141 pub clear_screen: Option<String>,
142}
143
144#[derive(Debug, Serialize, Deserialize, Clone)]
146pub struct Cluster {
147 pub nodes: Vec<NodeConfig>,
148
149 #[serde(flatten)]
150 pub defaults: ClusterDefaults,
151
152 #[serde(default)]
153 pub interactive: Option<InteractiveConfig>,
154}
155
156#[derive(Debug, Serialize, Deserialize, Default, Clone)]
158pub struct ClusterDefaults {
159 pub user: Option<String>,
160 pub port: Option<u16>,
161 pub ssh_key: Option<String>,
162 pub parallel: Option<usize>,
163 pub timeout: Option<u64>,
164 pub jump_host: Option<JumpHostConfig>,
168 pub server_alive_interval: Option<u64>,
172 pub server_alive_count_max: Option<usize>,
175}
176
177#[derive(Debug, Serialize, Deserialize, Clone)]
179#[serde(untagged)]
180pub enum NodeConfig {
181 Simple(String),
182 Detailed {
183 host: String,
184 #[serde(default)]
185 port: Option<u16>,
186 #[serde(default)]
187 user: Option<String>,
188 #[serde(default)]
192 jump_host: Option<JumpHostConfig>,
193 },
194}
195
196#[derive(Debug, Default)]
198pub struct InteractiveConfigUpdate {
199 pub default_mode: Option<InteractiveMode>,
200 pub prompt_format: Option<String>,
201 pub history_file: Option<String>,
202 pub work_dir: Option<String>,
203 pub show_timestamps: Option<bool>,
204 pub colors: Option<HashMap<String, String>>,
205}
206
207pub(super) fn default_interactive_mode() -> InteractiveMode {
209 InteractiveMode::SingleNode
210}
211
212pub(super) fn default_prompt_format() -> String {
213 "[{node}:{user}@{host}:{pwd}]$ ".to_string()
214}
215
216pub(super) fn default_switch_node() -> String {
217 "Ctrl+N".to_string()
218}
219
220pub(super) fn default_broadcast_toggle() -> String {
221 "Ctrl+B".to_string()
222}
223
224pub(super) fn default_quit() -> String {
225 "Ctrl+Q".to_string()
226}
227
228impl JumpHostConfig {
229 pub fn to_connection_string(&self) -> String {
235 match self {
236 JumpHostConfig::Simple(s) => s.clone(),
237 JumpHostConfig::Detailed {
238 host,
239 user,
240 port,
241 ssh_key: _,
242 } => {
243 let mut result = String::new();
244 if let Some(u) = user {
245 result.push_str(u);
246 result.push('@');
247 }
248 result.push_str(host);
249 if let Some(p) = port {
250 result.push(':');
251 result.push_str(&p.to_string());
252 }
253 result
254 }
255 JumpHostConfig::SshConfigHostRef { ssh_config_host } => {
256 format!("@{}", ssh_config_host)
257 }
258 }
259 }
260
261 pub fn ssh_key(&self) -> Option<&str> {
263 match self {
264 JumpHostConfig::Simple(_) => None,
265 JumpHostConfig::Detailed { ssh_key, .. } => ssh_key.as_deref(),
266 JumpHostConfig::SshConfigHostRef { .. } => None,
267 }
268 }
269
270 pub fn is_ssh_config_ref(&self) -> bool {
272 match self {
273 JumpHostConfig::Simple(s) => s.starts_with('@'),
274 JumpHostConfig::SshConfigHostRef { .. } => true,
275 JumpHostConfig::Detailed { .. } => false,
276 }
277 }
278
279 pub fn ssh_config_host(&self) -> Option<&str> {
286 match self {
287 JumpHostConfig::Simple(s) if s.starts_with('@') => Some(&s[1..]),
288 JumpHostConfig::SshConfigHostRef { ssh_config_host } => Some(ssh_config_host),
289 _ => None,
290 }
291 }
292}