use std::fmt;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Format {
Tsv,
Json,
Portal,
}
impl Format {
pub fn parse(s: &str) -> Result<Self, String> {
match s.trim() {
"tsv" => Ok(Format::Tsv),
"json" => Ok(Format::Json),
"portal" => Ok(Format::Portal),
other => Err(format!("{other:?} is not tsv, json or portal")),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Kind {
Window,
Output,
}
impl fmt::Display for Kind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
Kind::Window => "window",
Kind::Output => "output",
})
}
}
#[derive(Clone)]
pub struct Target {
pub kind: Kind,
pub id: String,
pub con_id: Option<i64>,
pub ft_id: String,
pub app: String,
pub title: String,
}
impl Target {
pub fn window(con_id: i64, ft_id: String, app: String, title: String) -> Self {
Self {
kind: Kind::Window,
id: con_id.to_string(),
con_id: Some(con_id),
ft_id,
app,
title,
}
}
pub fn output(name: String) -> Self {
Self {
kind: Kind::Output,
id: name.clone(),
con_id: None,
ft_id: String::new(),
app: "display".to_string(),
title: name,
}
}
pub fn label(&self) -> String {
if self.app.is_empty() {
self.title.clone()
} else {
format!("{} · {}", self.title, self.app)
}
}
pub fn tsv(&self) -> String {
format!(
"{}\t{}\t{}\t{}\t{}",
self.kind,
self.id,
self.ft_id,
clean(&self.app),
clean(&self.title)
)
}
pub fn json(&self) -> String {
let opt = |v: Option<String>| match v {
Some(s) => format!("\"{}\"", esc(&s)),
None => "null".to_string(),
};
let (con_id, output) = match self.kind {
Kind::Window => (
self.con_id.map(|n| n.to_string()).unwrap_or("null".into()),
"null".to_string(),
),
Kind::Output => ("null".to_string(), opt(Some(self.id.clone()))),
};
let toplevel = match self.ft_id.is_empty() {
true => "null".to_string(),
false => format!("\"{}\"", esc(&self.ft_id)),
};
format!(
"{{\"type\":\"{}\",\"con_id\":{con_id},\"toplevel_id\":{toplevel},\
\"output\":{output},\"app\":\"{}\",\"title\":\"{}\"}}",
self.kind,
esc(&self.app),
esc(&self.title)
)
}
pub fn render(&self, format: Format) -> Option<String> {
match format {
Format::Tsv => Some(self.tsv()),
Format::Json => Some(self.json()),
Format::Portal => self.portal(),
}
}
pub fn portal(&self) -> Option<String> {
match self.kind {
Kind::Output => Some(format!("Monitor: {}", self.id)),
Kind::Window if !self.ft_id.is_empty() => Some(format!("Window: {}", self.ft_id)),
Kind::Window => None,
}
}
}
fn clean(s: &str) -> String {
s.chars()
.map(|c| {
if (c as u32) < 0x20 || c == '\u{7f}' {
' '
} else {
c
}
})
.collect()
}
fn esc(s: &str) -> String {
let mut out = String::with_capacity(s.len());
for c in s.chars() {
match c {
'"' => out.push_str("\\\""),
'\\' => out.push_str("\\\\"),
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
'\t' => out.push_str("\\t"),
c if (c as u32) < 0x20 => out.push_str(&format!("\\u{:04x}", c as u32)),
c => out.push(c),
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
fn win() -> Target {
Target::window(42, "abc123".into(), "kitty".into(), "zsh\tin\na tab".into())
}
#[test]
fn tsv_is_one_line_with_both_identifiers() {
let row = win().tsv();
assert_eq!(row, "window\t42\tabc123\tkitty\tzsh in a tab");
assert_eq!(row.split('\t').count(), 5);
assert!(!row.contains('\n'));
}
#[test]
fn json_keeps_every_key_and_escapes() {
let j = win().json();
assert!(j.contains("\"type\":\"window\""), "{j}");
assert!(j.contains("\"con_id\":42"), "{j}");
assert!(j.contains("\"toplevel_id\":\"abc123\""), "{j}");
assert!(j.contains("\"output\":null"), "{j}");
assert!(j.contains("zsh\\tin\\na tab"), "{j}");
let o = Target::output("DP-1".into()).json();
assert!(o.contains("\"con_id\":null"), "{o}");
assert!(o.contains("\"output\":\"DP-1\""), "{o}");
}
#[test]
fn portal_speaks_xdpw() {
assert_eq!(win().portal().as_deref(), Some("Window: abc123"));
assert_eq!(
Target::output("DP-1".into()).portal().as_deref(),
Some("Monitor: DP-1")
);
let anon = Target::window(7, String::new(), "x".into(), "y".into());
assert_eq!(anon.portal(), None);
}
#[test]
fn displays_say_what_they_are() {
let t = Target::output("DP-1".into());
assert_eq!(t.label(), "DP-1 · display");
assert_eq!(t.con_id, None);
}
}