1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
use crate::ShutdownReason;
use crate::linux::core::DaemonCore;
use crate::linux::core::types::DaemonStatus;
use crate::linux::manager::DaemonManager;
use ksni::MenuItem;
use ksni::TrayMethods;
use ksni::menu::StandardItem;
use std::sync::Arc;
use sysinfo::Pid;
use sysinfo::System;
use tokio::sync::Mutex;
use tokio::sync::mpsc;
use tokio_util::sync::CancellationToken;
use wayclip_core::models::error::WayclipError;
use wayclip_core::settings::tray::TraySettings;
/// Make this logo compile-time dependant
static TRAY_LOGO_PNG: &[u8] = include_bytes!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/branding/pngs/wayclip-256x256.png"
));
// courrently this is only for linux, didnt find a tray lib for windows, but i bet its gonna be rly
// different anyway
#[derive(Clone)]
pub struct TrayStats {
pub status: String,
pub cpu: String,
pub ram: String,
// not really possible since depends highly on gpu
// vram: String,
// gpu: String,
}
#[derive(Clone)]
pub struct WayclipTray {
pub daemon: Arc<Mutex<DaemonCore>>,
// we will update this using our handler
pub stats: Option<TrayStats>,
pub config: TraySettings,
pub cancel_token: CancellationToken,
pub shutdown_sender: mpsc::Sender<ShutdownReason>,
}
impl WayclipTray {
pub async fn run_tray(
daemon: Arc<Mutex<DaemonCore>>,
config: TraySettings,
cancel_token: CancellationToken,
shutdown_sender: mpsc::Sender<ShutdownReason>,
) -> Result<(), WayclipError> {
if !config.enabled {
return Ok(());
}
// create tray
let tray = Self {
daemon: daemon.clone(),
stats: None,
config,
cancel_token: cancel_token.clone(),
shutdown_sender,
};
let poll = tray.config.show_stats || tray.config.show_status;
// spawn handler, so we can also then edit it on the fly
let handle = tray
.spawn()
.await
.map_err(|e| WayclipError::Tray(e.to_string().into()))?;
log::info!("Tray registered successfully");
if poll {
// collect system info
let mut sys = System::new_all();
let pid = Pid::from(std::process::id() as usize);
loop {
tokio::select! {
_ = cancel_token.cancelled() => {
log::debug!("Tray polling loop shutting down");
break;
}
_ = tokio::time::sleep(std::time::Duration::from_secs(5)) => {
// inf loop, update info on a specific pid
sys.refresh_processes(sysinfo::ProcessesToUpdate::Some(&[pid]), true);
let (status, cpu, ram) = {
let core = daemon.lock().await;
// get status
let status = format!(
"{:?}",
core.get_status().await.unwrap_or(DaemonStatus::Inactive)
);
// get cpu & mem for a process
let mut cpu = 0.0;
let mut mem = 0.0;
if let Some(proc) = sys.process(pid) {
// yes, its between 0-100. meaning across all cores
cpu = proc.cpu_usage() / sys.cpus().len() as f32;
// 1000, not 1024 since MB not MiB
mem = proc.memory() as f64 / 1000.0 / 1000.0;
}
(status, format!("{:.1}%", cpu), format!("{:.1} MB", mem))
};
// and then use that data to actually update tray info
handle
.update(move |t: &mut WayclipTray| {
let stats = TrayStats { status, cpu, ram };
t.stats = Some(stats);
})
.await;
}
}
}
} else {
cancel_token.cancelled().await;
}
Ok(())
}
pub fn get_png(&self) -> Vec<u8> {
TRAY_LOGO_PNG.to_vec()
}
}
impl ksni::Tray for WayclipTray {
fn id(&self) -> String {
// hardcoded
"org.wayclip.Tray".into()
}
fn icon_name(&self) -> String {
String::from("wayclip")
}
fn title(&self) -> String {
String::from("Wayclip")
}
// all of our menu actions
fn menu(&self) -> Vec<ksni::MenuItem<Self>> {
let daemon_for_save = self.daemon.clone();
vec![
// main title
StandardItem {
label: "Wayclip".into(),
icon_data: self.get_png(),
enabled: false,
visible: self.config.show_logo,
..Default::default()
}
.into(),
MenuItem::Separator,
// standart actions
StandardItem {
label: "Save Clip".into(),
activate: Box::new(move |_| {
// yes i agree ugly ahh code, but whatever
let core = daemon_for_save.clone();
tokio::spawn(async move {
if let Err(e) = DaemonCore::save_clip(core, None).await {
log::error!("Tray failed to trigger save: {e}");
}
});
}),
visible: self.config.show_save_clip,
..Default::default()
}
.into(),
StandardItem {
label: "Restart Daemon".into(),
activate: Box::new(|_| {
tokio::spawn(async move {
if let Ok(mgr) = DaemonManager::new().await {
let _ = mgr.restart_daemon().await;
}
});
}),
visible: self.config.show_restart,
..Default::default()
}
.into(),
StandardItem {
label: "Exit Tray & Daemon".into(),
activate: Box::new(|this: &mut Self| {
let token = this.cancel_token.clone();
let sender = this.shutdown_sender.clone();
tokio::spawn(async move {
let _ = sender.send(ShutdownReason::TrayExit).await;
token.cancel();
});
}),
visible: self.config.show_exit,
..Default::default()
}
.into(),
// all of statistics
MenuItem::Separator,
StandardItem {
label: format!(
"Status: {}",
self.stats
.as_ref()
.map(|s| s.status.clone())
.unwrap_or_default()
),
visible: self.stats.is_some() && self.config.show_status,
enabled: false,
..Default::default()
}
.into(),
StandardItem {
label: format!(
"CPU: {} • RAM: {}",
self.stats
.as_ref()
.map(|s| s.cpu.clone())
.unwrap_or_default(),
// could go for the combined look, but turns out too fat
self.stats
.as_ref()
.map(|s| s.ram.clone())
.unwrap_or_default(),
),
visible: self.stats.is_some() && self.config.show_stats,
enabled: false,
..Default::default()
}
.into(),
// StandardItem {
// label: format!(
// "RAM: {}",
// self.stats
// .as_ref()
// .map(|s| s.ram.clone())
// .unwrap_or_default()
// ),
// visible: self.stats.is_some(),
// enabled: false,
// ..Default::default()
// }
// .into(),
]
}
}