use ksni::{Tray, MenuItem, menu::*}; use crate::config; fn write_status(state: &str) { if let Some(config_dir) = dirs::config_dir() { let _ = std::fs::write(config_dir.join("vietc").join("status"), state); } } fn read_status() -> String { let path = dirs::config_dir() .map(|d| d.join("vietc").join("status")) .unwrap_or_else(|| std::path::PathBuf::from("/tmp/vietc-status")); std::fs::read_to_string(&path) .map(|s| s.trim().to_string()) .unwrap_or_else(|_| { let cfg = config::Config::load(); if cfg.start_enabled { "vn".into() } else { "en".into() } }) } fn current_im() -> String { config::Config::load().input_method } fn ensure_icons() { let Some(config_dir) = dirs::config_dir() else { return }; let icons_dir = config_dir.join("vietc").join("icons"); let _ = std::fs::create_dir_all(&icons_dir); let vn_path = icons_dir.join("vietc-vn.svg"); let en_path = icons_dir.join("vietc-en.svg"); if !vn_path.exists() { let _ = std::fs::write(&vn_path, r##" VN "##); } if !en_path.exists() { let _ = std::fs::write(&en_path, r##" EN "##); } } struct VietTray { mode: String, im: String, autostart: bool, } impl Tray for VietTray { fn id(&self) -> String { "io.github.vietc.Tray".into() } fn title(&self) -> String { "Viet+".into() } fn icon_name(&self) -> String { if self.mode == "vn" { "vietc-vn".into() } else { "vietc-en".into() } } fn icon_theme_path(&self) -> String { dirs::config_dir() .map(|d| d.join("vietc").join("icons").to_string_lossy().into_owned()) .unwrap_or_default() } fn activate(&mut self, _x: i32, _y: i32) { let next = if self.mode == "vn" { "en" } else { "vn" }; write_status(&next); let mut cfg = config::Config::load(); cfg.start_enabled = next == "vn"; let _ = cfg.save(); self.mode = next.to_string(); } fn menu(&self) -> Vec> { let is_vn = self.mode == "vn"; let im_index = if self.im == "telex" { 0 } else { 1 }; vec![ CheckmarkItem { label: "Vietnamese Mode".into(), checked: is_vn, activate: Box::new(|this: &mut VietTray| { let next = if this.mode == "vn" { "en" } else { "vn" }; write_status(&next); let mut cfg = config::Config::load(); cfg.start_enabled = next == "vn"; let _ = cfg.save(); this.mode = next.to_string(); }), ..Default::default() }.into(), MenuItem::Separator, SubMenu { label: "Input Method".into(), submenu: vec![ RadioGroup { selected: im_index, select: Box::new(|this: &mut VietTray, idx: usize| { let im = if idx == 0 { "telex" } else { "vni" }; let mut cfg = config::Config::load(); cfg.input_method = im.into(); let _ = cfg.save(); this.im = im.into(); }), options: vec![ RadioItem { label: "Telex".into(), ..Default::default() }, RadioItem { label: "VNI".into(), ..Default::default() }, ], }.into(), ], ..Default::default() }.into(), MenuItem::Separator, CheckmarkItem { label: "Start with System".into(), checked: self.autostart, activate: Box::new(|this: &mut VietTray| { if this.autostart { config::uninstall_autostart(); } else { config::install_autostart(); } }), ..Default::default() }.into(), MenuItem::Separator, StandardItem { label: "Quit".into(), activate: Box::new(|_| { let _ = std::process::Command::new("pkill") .arg("-x").arg("vietc").status(); std::process::exit(0); }), ..Default::default() }.into(), ] } } pub fn run() { ensure_icons(); let tray = VietTray { mode: read_status(), im: current_im(), autostart: config::is_autostart_installed(), }; let service = ksni::TrayService::new(tray); let handle = service.handle(); service.spawn(); // Poll for changes std::thread::spawn(move || { loop { std::thread::sleep(std::time::Duration::from_millis(500)); let mode = read_status(); let im = current_im(); let autostart = config::is_autostart_installed(); let _ = handle.update(move |t| { t.mode = mode; t.im = im; t.autostart = autostart; }); } }); loop { std::thread::park(); } }