1use std::fmt;
11use std::str::FromStr;
12
13use serde::{Deserialize, Deserializer, Serialize};
14
15use crate::RmuxError;
16
17#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)]
25#[serde(transparent)]
26pub struct SessionName(String);
27
28impl SessionName {
29 pub fn new(value: impl Into<String>) -> Result<Self, RmuxError> {
31 let value = value.into();
32
33 if value.is_empty() {
34 return Err(RmuxError::EmptySessionName);
35 }
36
37 Ok(Self(sanitize_session_name(value.as_bytes())))
38 }
39
40 #[must_use]
42 pub fn as_str(&self) -> &str {
43 &self.0
44 }
45
46 #[must_use]
48 pub fn into_inner(self) -> String {
49 self.0
50 }
51}
52
53fn sanitize_session_name(input: &[u8]) -> String {
54 let mut sanitized = String::with_capacity(input.len());
55 for &byte in input {
56 let rewritten = match byte {
57 b':' | b'.' => b'_',
58 other => other,
59 };
60 push_session_name_byte(rewritten, &mut sanitized);
61 }
62 sanitized
63}
64
65fn push_session_name_byte(byte: u8, output: &mut String) {
66 if (0x20..=0x7e).contains(&byte) && byte != b'\\' {
67 output.push(char::from(byte));
68 return;
69 }
70
71 match byte {
72 b'\0' => output.push_str("\\000"),
73 b'\x07' => output.push_str("\\a"),
74 b'\x08' => output.push_str("\\b"),
75 b'\t' => output.push_str("\\t"),
76 b'\n' => output.push_str("\\n"),
77 b'\x0b' => output.push_str("\\v"),
78 b'\x0c' => output.push_str("\\f"),
79 b'\r' => output.push_str("\\r"),
80 b'\\' => output.push_str("\\\\"),
81 _ => {
82 output.push('\\');
83 output.push(char::from(b'0' + ((byte >> 6) & 0x7)));
84 output.push(char::from(b'0' + ((byte >> 3) & 0x7)));
85 output.push(char::from(b'0' + (byte & 0x7)));
86 }
87 }
88}
89
90impl AsRef<str> for SessionName {
91 fn as_ref(&self) -> &str {
92 self.as_str()
93 }
94}
95
96impl fmt::Display for SessionName {
97 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
98 formatter.write_str(self.as_str())
99 }
100}
101
102impl FromStr for SessionName {
103 type Err = RmuxError;
104
105 fn from_str(value: &str) -> Result<Self, Self::Err> {
106 Self::new(value)
107 }
108}
109
110impl TryFrom<&str> for SessionName {
111 type Error = RmuxError;
112
113 fn try_from(value: &str) -> Result<Self, Self::Error> {
114 Self::new(value)
115 }
116}
117
118impl TryFrom<String> for SessionName {
119 type Error = RmuxError;
120
121 fn try_from(value: String) -> Result<Self, Self::Error> {
122 Self::new(value)
123 }
124}
125
126impl<'de> Deserialize<'de> for SessionName {
127 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
128 where
129 D: Deserializer<'de>,
130 {
131 let value = String::deserialize(deserializer)?;
132 Self::new(value).map_err(serde::de::Error::custom)
133 }
134}
135
136#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
142#[serde(transparent)]
143pub struct SessionId(u32);
144
145impl SessionId {
146 #[must_use]
148 pub const fn new(value: u32) -> Self {
149 Self(value)
150 }
151
152 #[must_use]
154 pub const fn as_u32(self) -> u32 {
155 self.0
156 }
157}
158
159impl fmt::Display for SessionId {
160 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
161 write!(formatter, "${}", self.0)
162 }
163}
164
165impl From<SessionId> for u32 {
166 fn from(value: SessionId) -> Self {
167 value.0
168 }
169}
170
171impl From<u32> for SessionId {
172 fn from(value: u32) -> Self {
173 Self(value)
174 }
175}
176
177#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
183#[serde(transparent)]
184pub struct WindowId(u32);
185
186impl WindowId {
187 #[must_use]
189 pub const fn new(value: u32) -> Self {
190 Self(value)
191 }
192
193 #[must_use]
195 pub const fn as_u32(self) -> u32 {
196 self.0
197 }
198}
199
200impl fmt::Display for WindowId {
201 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
202 write!(formatter, "@{}", self.0)
203 }
204}
205
206impl From<WindowId> for u32 {
207 fn from(value: WindowId) -> Self {
208 value.0
209 }
210}
211
212impl From<u32> for WindowId {
213 fn from(value: u32) -> Self {
214 Self(value)
215 }
216}
217
218#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
224#[serde(transparent)]
225pub struct PaneId(u32);
226
227impl PaneId {
228 #[must_use]
230 pub const fn new(value: u32) -> Self {
231 Self(value)
232 }
233
234 #[must_use]
236 pub const fn as_u32(self) -> u32 {
237 self.0
238 }
239}
240
241impl fmt::Display for PaneId {
242 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
243 write!(formatter, "%{}", self.0)
244 }
245}
246
247impl From<PaneId> for u32 {
248 fn from(value: PaneId) -> Self {
249 value.0
250 }
251}
252
253impl From<u32> for PaneId {
254 fn from(value: u32) -> Self {
255 Self(value)
256 }
257}
258
259#[cfg(test)]
260mod tests {
261 use super::{PaneId, SessionId, SessionName, WindowId};
262 use crate::RmuxError;
263
264 #[test]
265 fn session_name_rejects_empty_values() {
266 assert_eq!(SessionName::new(""), Err(RmuxError::EmptySessionName));
267 }
268
269 #[test]
270 fn session_name_rewrites_colon_and_dot() {
271 assert_eq!(
272 SessionName::new("alpha:beta.gamma")
273 .expect("rewritten")
274 .as_str(),
275 "alpha_beta_gamma"
276 );
277 }
278
279 #[test]
280 fn session_name_round_trips_through_serde() {
281 let payload = bincode::serialize("alpha.beta").expect("string encodes");
282 assert_eq!(
283 bincode::deserialize::<SessionName>(&payload).expect("rewritten on the wire"),
284 SessionName::new("alpha_beta").expect("valid")
285 );
286 }
287
288 #[test]
289 fn session_name_serde_rejects_empty_payloads_truthfully() {
290 let payload = bincode::serialize("").expect("empty string encodes");
291 assert!(
292 bincode::deserialize::<SessionName>(&payload).is_err(),
293 "empty session names must fail deserialization rather than silently \
294 producing an empty inner value"
295 );
296 }
297
298 #[test]
299 fn session_name_serialize_round_trips_after_rewriting() {
300 let original = SessionName::new("alpha.beta").expect("rewrites dots");
301 let bytes = bincode::serialize(&original).expect("session name encodes");
302 let restored: SessionName =
303 bincode::deserialize(&bytes).expect("session name decodes idempotently");
304 assert_eq!(restored, original);
305 assert_eq!(restored.as_str(), "alpha_beta");
306 }
307
308 #[test]
309 fn session_name_from_str_and_try_from_match_constructor() {
310 let from_str: SessionName = "alpha:beta".parse().expect("FromStr rewrites");
311 let try_from_ref: SessionName =
312 SessionName::try_from("alpha:beta").expect("TryFrom<&str> rewrites");
313 let try_from_owned: SessionName =
314 SessionName::try_from(String::from("alpha:beta")).expect("TryFrom<String> rewrites");
315 assert_eq!(from_str, try_from_ref);
316 assert_eq!(from_str, try_from_owned);
317 assert_eq!(from_str.as_str(), "alpha_beta");
318 }
319
320 #[test]
321 fn session_name_into_inner_returns_sanitized_string() {
322 let owned = SessionName::new("alpha:beta")
323 .expect("rewrites colons")
324 .into_inner();
325 assert_eq!(owned, "alpha_beta");
326 }
327
328 #[test]
329 fn session_id_displays_with_dollar_prefix() {
330 assert_eq!(SessionId::new(7).to_string(), "$7");
331 assert_eq!(SessionId::new(7).as_u32(), 7);
332 }
333
334 #[test]
335 fn window_id_displays_with_at_prefix() {
336 assert_eq!(WindowId::new(9).to_string(), "@9");
337 assert_eq!(WindowId::new(9).as_u32(), 9);
338 }
339
340 #[test]
341 fn window_id_zero_and_max_render_as_at_prefixed_decimal() {
342 assert_eq!(WindowId::new(0).to_string(), "@0");
343 assert_eq!(
344 WindowId::new(u32::MAX).to_string(),
345 format!("@{}", u32::MAX)
346 );
347 }
348
349 #[test]
350 fn pane_id_displays_with_percent_prefix() {
351 assert_eq!(PaneId::new(3).to_string(), "%3");
352 assert_eq!(PaneId::new(3).as_u32(), 3);
353 }
354
355 #[test]
356 fn pane_id_zero_and_max_render_as_percent_prefixed_decimal() {
357 assert_eq!(PaneId::new(0).to_string(), "%0");
358 assert_eq!(PaneId::new(u32::MAX).to_string(), format!("%{}", u32::MAX));
359 }
360
361 #[test]
362 fn session_id_zero_and_max_render_as_dollar_prefixed_decimal() {
363 assert_eq!(SessionId::new(0).to_string(), "$0");
364 assert_eq!(
365 SessionId::new(u32::MAX).to_string(),
366 format!("${}", u32::MAX)
367 );
368 }
369
370 #[test]
371 fn identity_newtypes_round_trip_through_u32_conversions() {
372 for value in [0_u32, 1, 17, u32::MAX] {
373 assert_eq!(u32::from(SessionId::from(value)), value);
374 assert_eq!(u32::from(WindowId::from(value)), value);
375 assert_eq!(u32::from(PaneId::from(value)), value);
376 assert_eq!(SessionId::from(value).as_u32(), value);
377 assert_eq!(WindowId::from(value).as_u32(), value);
378 assert_eq!(PaneId::from(value).as_u32(), value);
379 }
380 }
381
382 #[test]
383 fn identity_newtypes_are_serde_transparent() {
384 assert_eq!(
385 bincode::serialize(&PaneId::new(11)).expect("encodes"),
386 bincode::serialize(&11_u32).expect("encodes")
387 );
388 assert_eq!(
389 bincode::serialize(&WindowId::new(11)).expect("encodes"),
390 bincode::serialize(&11_u32).expect("encodes")
391 );
392 assert_eq!(
393 bincode::serialize(&SessionId::new(11)).expect("encodes"),
394 bincode::serialize(&11_u32).expect("encodes")
395 );
396 }
397
398 #[test]
399 fn identity_id_newtypes_decode_back_through_serde() {
400 for value in [0_u32, 7, 257, u32::MAX] {
401 let session_bytes =
402 bincode::serialize(&SessionId::new(value)).expect("session id encodes");
403 let window_bytes =
404 bincode::serialize(&WindowId::new(value)).expect("window id encodes");
405 let pane_bytes = bincode::serialize(&PaneId::new(value)).expect("pane id encodes");
406
407 assert_eq!(
408 bincode::deserialize::<SessionId>(&session_bytes).expect("session id decodes"),
409 SessionId::new(value),
410 );
411 assert_eq!(
412 bincode::deserialize::<WindowId>(&window_bytes).expect("window id decodes"),
413 WindowId::new(value),
414 );
415 assert_eq!(
416 bincode::deserialize::<PaneId>(&pane_bytes).expect("pane id decodes"),
417 PaneId::new(value),
418 );
419 }
420 }
421
422 #[test]
423 fn identity_id_newtypes_total_order_matches_inner_u32() {
424 let mut ids = [PaneId::new(3), PaneId::new(0), PaneId::new(1)];
425 ids.sort();
426 assert_eq!(ids, [PaneId::new(0), PaneId::new(1), PaneId::new(3)]);
427 }
428
429 #[test]
430 fn session_name_already_sanitized_round_trips_through_serde() {
431 let original = SessionName::new("alpha-beta_gamma").expect("printable name");
432 let bytes = bincode::serialize(&original).expect("session name encodes");
433 let restored: SessionName =
434 bincode::deserialize(&bytes).expect("session name decodes idempotently");
435 assert_eq!(restored, original);
436 }
437}