diff --git a/src/tailscale/dns.rs b/src/tailscale/dns.rs new file mode 100644 index 0000000..d48a5b3 --- /dev/null +++ b/src/tailscale/dns.rs @@ -0,0 +1,9 @@ +use crate::tailscale::utils::{sanitize_hostname, trim_suffix, Machine, PeerKind}; + +pub fn dns_or_quote_hostname(m: &mut Machine, dns_suffix: &str) { + let base_name = trim_suffix(&m.dns_name, dns_suffix); + m.display_name = match base_name { + n if n.is_empty() => PeerKind::DNSName(sanitize_hostname(m.hostname.as_str())), + base => PeerKind::HostName(base), + } +} diff --git a/src/tailscale/mod.rs b/src/tailscale/mod.rs index 0966800..afa0f10 100644 --- a/src/tailscale/mod.rs +++ b/src/tailscale/mod.rs @@ -1,2 +1,4 @@ +pub mod dns; +pub mod peer; pub mod status; pub mod utils; diff --git a/src/tailscale/peer.rs b/src/tailscale/peer.rs new file mode 100644 index 0000000..c2f5e48 --- /dev/null +++ b/src/tailscale/peer.rs @@ -0,0 +1,47 @@ +use crate::clipboard::{copy_to_clipboard, get_from_clipboard}; +use log::{error, info}; +use notify_rust::Notification; + +pub fn check_peer_ip(peer_ip: &str) { + if peer_ip.is_empty() { + error!("No peer IP.") + } else { + info!("Peer IP: {}", peer_ip); + } +} + +pub fn copy_peer_ip(peer_ip: &str, notif_title: &str) { + check_peer_ip(peer_ip); + + match copy_to_clipboard(peer_ip) { + Ok(_) => { + // Get IP from clipboard to verify + match get_from_clipboard() { + Ok(clip_ip) => { + // log success + info!("Copied IP address {} to the Clipboard", clip_ip); + + // send a notification through dbus + let body = format!("Copied IP address {} to the Clipboard", clip_ip); + let _result = Notification::new() + .summary(notif_title) + .body(&body) + .icon("info") + .show(); + } + + Err(e) => { + let message = "Failed to get IP from clipboard"; + error!("{}: {}", message, e); + + let _result = Notification::new() + .summary(notif_title) + .body(&message) + .icon("error") + .show(); + } + } + } + Err(e) => error!("Failed to copy IP to clipboard: {}", e), + } +} diff --git a/src/tailscale/status.rs b/src/tailscale/status.rs index 9b23e67..8437ac6 100644 --- a/src/tailscale/status.rs +++ b/src/tailscale/status.rs @@ -1,10 +1,29 @@ -use crate::tailscale::utils; +use crate::tailscale::dns; +use crate::tailscale::utils::{Machine, User}; use crate::tray::Context; +use serde::{Deserialize, Serialize}; +use std::{collections::HashMap, process::Command}; use which::which; +#[derive(Serialize, Deserialize, Debug)] +pub struct Status { + // TODO: mutex + #[serde(skip)] + pub tailscale_up: bool, + #[serde(rename(deserialize = "BackendState"))] + backend_state: String, + #[serde(rename(deserialize = "Self"))] + pub this_machine: Machine, + #[serde(rename(deserialize = "MagicDNSSuffix"))] + magic_dnssuffix: String, + #[serde(rename(deserialize = "Peer"))] + pub peers: HashMap, + #[serde(rename(deserialize = "User"))] + user: HashMap, +} + pub fn get_current_status() -> Context { - let status: utils::Status = utils::get_status().unwrap(); - + let status: Status = get_status().unwrap(); Context { ip: status.this_machine.ips[0].clone(), @@ -12,3 +31,28 @@ pub fn get_current_status() -> Context { status, } } + +pub fn get_status_json() -> String { + let output = Command::new("tailscale") + .arg("status") + .arg("--json") + .output() + .expect("Fetch tailscale status fail."); + String::from_utf8(output.stdout).expect("Unable to convert status output string.") +} + +pub fn get_status() -> Result { + let mut st: Status = serde_json::from_str(get_status_json().as_str())?; + let dnssuffix = st.magic_dnssuffix.to_owned(); + st.tailscale_up = match st.backend_state.as_str() { + "Running" => true, + "Stopped" => false, + _ => false, + }; + + dns::dns_or_quote_hostname(&mut st.this_machine, &dnssuffix); + st.peers + .values_mut() + .for_each(|m: &mut Machine| dns::dns_or_quote_hostname(m, &dnssuffix)); + Ok(st) +} diff --git a/src/tailscale/utils.rs b/src/tailscale/utils.rs index 6cb619a..2268b70 100644 --- a/src/tailscale/utils.rs +++ b/src/tailscale/utils.rs @@ -2,7 +2,6 @@ use serde::{Deserialize, Serialize}; use std::{ collections::HashMap, fmt::{Display, Formatter}, - process::Command, }; #[derive(Debug)] @@ -10,11 +9,13 @@ pub enum PeerKind { DNSName(String), HostName(String), } + impl Default for PeerKind { fn default() -> Self { PeerKind::HostName("default".to_owned()) } } + impl Display for PeerKind { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match &self { @@ -29,14 +30,15 @@ pub struct Machine { #[serde(skip)] pub display_name: PeerKind, #[serde(rename(deserialize = "DNSName"))] - dns_name: String, + pub dns_name: String, #[serde(rename(deserialize = "HostName"))] - hostname: String, + pub hostname: String, #[serde(rename(deserialize = "TailscaleIPs"))] pub ips: Vec, } + #[derive(Serialize, Deserialize, Debug)] -struct User { +pub struct User { #[serde(rename(deserialize = "ID"))] id: u64, #[serde(rename(deserialize = "LoginName"))] @@ -48,56 +50,8 @@ struct User { #[serde(rename(deserialize = "Roles"))] roles: Vec, } -#[derive(Serialize, Deserialize, Debug)] - -pub struct Status { - // TODO: mutex - #[serde(skip)] - pub tailscale_up: bool, - #[serde(rename(deserialize = "BackendState"))] - backend_state: String, - #[serde(rename(deserialize = "Self"))] - pub this_machine: Machine, - #[serde(rename(deserialize = "MagicDNSSuffix"))] - magic_dnssuffix: String, - #[serde(rename(deserialize = "Peer"))] - pub peers: HashMap, - #[serde(rename(deserialize = "User"))] - user: HashMap, -} -pub fn get_status() -> Result { - let mut st: Status = serde_json::from_str(get_json().as_str())?; - let dnssuffix = st.magic_dnssuffix.to_owned(); - st.tailscale_up = match st.backend_state.as_str() { - "Running" => true, - "Stopped" => false, - _ => false, - }; - dns_or_quote_hostname(&mut st.this_machine, &dnssuffix); - st.peers - .values_mut() - .for_each(|m: &mut Machine| dns_or_quote_hostname(m, &dnssuffix)); - Ok(st) -} - -fn get_json() -> String { - let output = Command::new("tailscale") - .arg("status") - .arg("--json") - .output() - .expect("Fetch tailscale status fail."); - String::from_utf8(output.stdout).expect("Unable to convert status output string.") -} - -fn dns_or_quote_hostname(m: &mut Machine, dns_suffix: &str) { - let base_name = trim_suffix(&m.dns_name, dns_suffix); - m.display_name = match base_name { - n if n.is_empty() => PeerKind::DNSName(sanitize_hostname(m.hostname.as_str())), - base => PeerKind::HostName(base), - } -} -fn has_suffix(name: &str, suffix: &str) -> bool { +pub fn has_suffix(name: &str, suffix: &str) -> bool { let name = name.trim_end_matches('.'); let mut suffix = suffix.trim_end_matches('.'); suffix = suffix.trim_start_matches('.'); @@ -105,7 +59,7 @@ fn has_suffix(name: &str, suffix: &str) -> bool { name_base.len() < name.len() && name_base.ends_with('.') } -fn trim_suffix(name: &str, suffix: &str) -> String { +pub fn trim_suffix(name: &str, suffix: &str) -> String { let mut new_name = name; if has_suffix(name, suffix) { new_name = new_name.trim_end_matches('.'); @@ -115,7 +69,7 @@ fn trim_suffix(name: &str, suffix: &str) -> String { new_name.trim_end_matches('.').to_string() } -fn sanitize_hostname(hostname: &str) -> String { +pub fn sanitize_hostname(hostname: &str) -> String { const MAX_LABEL_LEN: usize = 63; let mut sb = "".to_string(); let hostname = hostname diff --git a/src/tray.rs b/src/tray.rs index fdd94d2..1a9e94c 100644 --- a/src/tray.rs +++ b/src/tray.rs @@ -1,15 +1,16 @@ -use crate::clipboard::{copy_to_clipboard, get_from_clipboard}; use crate::pkexec::{get_pkexec_path, pkexec_found}; use crate::svg::utils::ResvgRenderer; +use crate::tailscale::peer::copy_peer_ip; +use crate::tailscale::status::{get_current_status, Status}; use crate::tailscale::utils::PeerKind; -use crate::tailscale::utils::{get_status, Status}; use ksni::{ self, menu::{StandardItem, SubMenu}, Icon, MenuItem, ToolTip, Tray, }; -use log::{error, info}; + +use log::info; use notify_rust::Notification; use std::{ path::PathBuf, @@ -31,7 +32,7 @@ pub struct SysTray { impl SysTray { pub fn new() -> Self { SysTray { - ctx: Self::get_current_status(), + ctx: get_current_status(), } } @@ -39,21 +40,8 @@ impl SysTray { self.ctx.status.tailscale_up } - fn get_current_status() -> Context { - let status: Status = get_status().unwrap(); - let pkexec_path = get_pkexec_path(); - let ctx = Context { - ip: status.this_machine.ips[0].clone(), - pkexec: pkexec_path.clone(), - status, - }; - - assert_eq!(ctx.pkexec, pkexec_path); - ctx - } - fn update_status(&mut self) { - self.ctx = Self::get_current_status(); + self.ctx = get_current_status(); } fn do_service_link(&mut self, verb: &str) { @@ -90,50 +78,6 @@ impl SysTray { self.update_status(); } } - - fn check_peer_ip(peer_ip: &str) { - if peer_ip.is_empty() { - error!("No peer IP.") - } else { - info!("Peer IP: {}", peer_ip); - } - } - - fn copy_peer_ip(peer_ip: &str, notif_title: &str) { - Self::check_peer_ip(peer_ip); - - match copy_to_clipboard(peer_ip) { - Ok(_) => { - // Get IP from clipboard to verify - match get_from_clipboard() { - Ok(clip_ip) => { - // log success - info!("Copied IP address {} to the Clipboard", clip_ip); - - // send a notification through dbus - let body = format!("Copied IP address {} to the Clipboard", clip_ip); - let _result = Notification::new() - .summary(notif_title) - .body(&body) - .icon("info") - .show(); - } - - Err(e) => { - let message = "Failed to get IP from clipboard"; - error!("{}: {}", message, e); - - let _result = Notification::new() - .summary(notif_title) - .body(&message) - .icon("error") - .show(); - } - } - } - Err(e) => error!("Failed to copy IP to clipboard: {}", e), - } - } } impl Tray for SysTray { @@ -188,7 +132,7 @@ impl Tray for SysTray { let peer_title = title.to_owned(); let menu = MenuItem::Standard(StandardItem { label: format!("{}\t({})", title, ip), - activate: Box::new(move |_: &mut Self| Self::copy_peer_ip(&peer_ip, &peer_title)), + activate: Box::new(move |_: &mut Self| copy_peer_ip(&peer_ip, &peer_title)), ..Default::default() }); sub.push(menu); @@ -218,7 +162,7 @@ impl Tray for SysTray { "This device: {} ({})", self.ctx.status.this_machine.display_name, self.ctx.ip ), - activate: Box::new(move |_| Self::copy_peer_ip(&my_ip, "This device")), + activate: Box::new(move |_| copy_peer_ip(&my_ip, "This device")), ..Default::default() } .into(),