- 1
//! vak-tray: the app bundle's actual entry point (`Info.plist` - 2
//! `CFBundleExecutable`), so double-clicking Vak.app or clicking it - 3
//! in Spotlight runs this binary, not `vak-desktop`. - 4
//! - 5
//! The menu bar uses the generated monochrome Vakyartha template, shared with - 6
//! vak-desktop. AppKit supplies the colour for light, dark and selected - 7
//! menu-bar states. Service status stays in the tooltip and menu. - 8
//! - 9
//! It also opens the chat window and the admin console. `vak-desktop` — - 10
//! the actual product surface — is a sibling binary this process spawns; - 11
//! nothing about it runs inside the tray. Without that spawn, opening - 12
//! the installed app showed only a menu-bar dot and nothing a - 13
//! first-time user would recognize as "the app is now open." - 14
//! `vak-desktop` guards itself with `tauri_plugin_single_instance`, so - 15
//! spawning it when a window is already open just refocuses that window - 16
//! rather than duplicating it — both the automatic launch below and the - 17
//! always-present "Open Vakyartha" menu item rely on that guarantee. - 18
//! "Open Admin Console" opens a pre-authenticated link built from the - 19
//! gateway token `self install` pins into the canonical secret scope - 20
//! (`ensure_gateway_token`, crates/vak/src/install/mod.rs) — see - 21
//! `open_admin_console` for why that has to be a query param and not a - 22
//! URL fragment. - 23
- 24
// GUI bootstrap: every setup call here is infallible in practice, and a - 25
// controller that cannot start should be loud about it. - 26
#![allow(clippy::expect_used)] - 27
- 28
use std::sync::Arc; - 29
use std::sync::atomic::{AtomicBool, Ordering}; - 30
use std::time::Duration; - 31
- 32
use tray_icon::menu::{CheckMenuItem, Menu, MenuEvent, MenuItem, PredefinedMenuItem}; - 33
use tray_icon::{Icon, TrayIcon, TrayIconBuilder}; - 34
use winit::application::ApplicationHandler; - 35
use winit::event::WindowEvent; - 36
use winit::event_loop::{ActiveEventLoop, EventLoop}; - 37
use winit::platform::macos::{ActivationPolicy, EventLoopBuilderExtMacOS as _}; - 38
- 39
#[derive(Debug, Clone)] - 40
enum TrayEvent { - 41
Refresh([vak_ops::State; 2]), - 42
Command(u32), - 43
} - 44
- 45
const SVC_COUNT: usize = 2; - 46
const GATEWAY: usize = 0; - 47
const BRIDGES: usize = 1; - 48
- 49
fn service(idx: usize) -> vak_ops::Service { - 50
if idx == GATEWAY { - 51
vak_ops::Service::Gateway - 52
} else { - 53
vak_ops::Service::Bridges - 54
} - 55
} - 56
- 57
/// 36x36 RGBA dot: the fallback glyph on the rare path where the - 58
/// embedded brand icon fails to decode. No longer the everyday icon — - 59
/// see `brand_icon` for why the tray now shows the same mark as the - 60
/// desktop app. Sized to match `brand_icon`'s output so a decode failure - 61
/// swaps the glyph, not also the icon's apparent size in the menu bar. - 62
#[allow(clippy::expect_used)] // infallible: fixed non-zero dimensions - 63
fn icon_dot(rgb: [u8; 3]) -> Icon { - 64
const S: usize = 36; - 65
let mut rgba = Vec::with_capacity(S * S * 4); - 66
let c = (S as f32 - 1.0) / 2.0; - 67
for y in 0..S { - 68
for x in 0..S { - 69
let d = ((x as f32 - c).powi(2) + (y as f32 - c).powi(2)).sqrt(); - 70
let alpha = if d <= 13.0 { - 71
255u8 - 72
} else if d <= 15.0 { - 73
140 - 74
} else { - 75
0 - 76
}; - 77
rgba.extend_from_slice(&[rgb[0], rgb[1], rgb[2], alpha]); - 78
} - 79
} - 80
// A fixed-size buffer can only fail on zero width/height — neither - 81
// applies here. - 82
Icon::from_rgba(rgba, S as u32, S as u32).expect("static icon") - 83
} - 84
- 85
/// A dedicated 2x alpha mask for AppKit's 18pt status item. - 86
const BRAND_ICON_PNG: &[u8] = include_bytes!(concat!( - 87
env!("CARGO_MANIFEST_DIR"), - 88
"/../vak-desktop/icons/tray-template.png" - 89
)); - 90
- 91
fn brand_icon() -> Option<Icon> { - 92
let (rgba, w, h) = decode_template(BRAND_ICON_PNG)?; - 93
Icon::from_rgba(rgba, w, h).ok() - 94
} - 95
- 96
fn decode_template(png_bytes: &[u8]) -> Option<(Vec<u8>, u32, u32)> { - 97
let decoder = png::Decoder::new(png_bytes); - 98
let mut reader = decoder.read_info().ok()?; - 99
let mut pixels = vec![0; reader.output_buffer_size()]; - 100
let info = reader.next_frame(&mut pixels).ok()?; - 101
if info.bit_depth != png::BitDepth::Eight - 102
|| info.color_type != png::ColorType::Rgba - 103
|| (info.width, info.height) != (36, 36) - 104
{ - 105
return None; - 106
} - 107
pixels.truncate(info.buffer_size()); - 108
Some((pixels, info.width, info.height)) - 109
} - 110
- 111
/// `brand_icon`, falling back to the plain grey dot if the embedded PNG - 112
/// ever fails to decode -- the tray must still have some icon rather - 113
/// than none. - 114
fn startup_icon() -> Icon { - 115
brand_icon().unwrap_or_else(|| icon_dot([158, 158, 158])) - 116
} - 117
- 118
struct Ui { - 119
tray: TrayIcon, - 120
watchdog: Arc<AtomicBool>, - 121
states: [vak_ops::State; 2], - 122
/// (states, watchdog_on) as of the last menu/icon rebuild. Replacing - 123
/// a status item's NSMenu is only safe when it is not currently being - 124
/// tracked (open) by the user -- there is no "menu will open" hook in - 125
/// this version of tray-icon to defer the rebuild until then, so the - 126
/// next-best guard is to never replace it on a bare timer tick when - 127
/// nothing changed. Refreshing every 3s unconditionally meant any - 128
/// refresh landing while the user had the menu open could tear it - 129
/// down mid-track, which read as "the menu opens then disappears." - 130
/// A steady healthy system rebuilds roughly never instead of 1200 - 131
/// times an hour. - 132
last_rendered: Option<([vak_ops::State; 2], bool)>, - 133
} - 134
- 135
impl ApplicationHandler<TrayEvent> for Ui { - 136
fn resumed(&mut self, _loop: &ActiveEventLoop) {} - 137
- 138
fn about_to_wait(&mut self, _loop: &ActiveEventLoop) {} - 139
- 140
fn user_event(&mut self, loop_handle: &ActiveEventLoop, event: TrayEvent) { - 141
match event { - 142
TrayEvent::Refresh(states) => { - 143
let _ = loop_handle; - 144
self.states = states; - 145
let watchdog_on = self.watchdog.load(Ordering::SeqCst); - 146
let snapshot = (states, watchdog_on); - 147
if self.last_rendered == Some(snapshot) { - 148
return; - 149
} - 150
self.last_rendered = Some(snapshot); - 151
self.tray - 152
.set_menu(Some(Box::new(build_menu(&states, watchdog_on)))); - 153
// The icon itself is the fixed brand mark now (see - 154
// brand_icon); status moves to the tooltip instead of an - 155
// icon color swap. - 156
let _ = self.tray.set_tooltip(Some(status_tooltip(&states))); - 157
} - 158
TrayEvent::Command(id) => self.handle_command(id), - 159
} - 160
} - 161
- 162
fn window_event(&mut self, _: &ActiveEventLoop, _: winit::window::WindowId, _: WindowEvent) {} - 163
} - 164
- 165
impl Ui { - 166
fn handle_command(&mut self, id: u32) { - 167
// Commands are encoded as (slot << 8) | action; see build_menu. - 168
let slot = (id >> 8) as usize; - 169
let action = id & 0xff; - 170
let cfg = ops_config(); - 171
match action { - 172
ACT_START => { - 173
let _ = vak_ops::start(service(slot), &cfg); - 174
} - 175
ACT_STOP => { - 176
let _ = vak_ops::stop(service(slot), &cfg); - 177
} - 178
ACT_RESTART => { - 179
vak_ops::restart(service(slot), &cfg); - 180
} - 181
ACT_INSTALL => { - 182
if let Err(e) = vak_ops::install(service(slot), &cfg) { - 183
notify("Vakyartha", &e); - 184
} - 185
} - 186
ACT_UNINSTALL => { - 187
let _ = vak_ops::uninstall(service(slot), &cfg); - 188
} - 189
ACT_LOG => vak_ops::open_log(service(slot)), - 190
ACT_WATCHDOG_TOGGLE => { - 191
let newval = !self.watchdog.load(Ordering::SeqCst); - 192
self.watchdog.store(newval, Ordering::SeqCst); - 193
persist_watchdog(newval); - 194
} - 195
ACT_QUIT => std::process::exit(0), - 196
ACT_OPEN_DESKTOP => open_desktop(), - 197
ACT_OPEN_ADMIN => open_admin_console(), - 198
ACT_OPEN_OPERATIONS => open_operations_center(), - 199
_ => {} - 200
} - 201
} - 202
} - 203
- 204
const ACT_START: u32 = 1; - 205
const ACT_STOP: u32 = 2; - 206
const ACT_RESTART: u32 = 3; - 207
const ACT_INSTALL: u32 = 4; - 208
const ACT_UNINSTALL: u32 = 5; - 209
const ACT_LOG: u32 = 6; - 210
const ACT_WATCHDOG_TOGGLE: u32 = 7; - 211
const ACT_QUIT: u32 = 8; - 212
const ACT_OPEN_DESKTOP: u32 = 9; - 213
const ACT_OPEN_ADMIN: u32 = 10; - 214
const ACT_OPEN_OPERATIONS: u32 = 11; - 215
- 216
fn persist_watchdog(on: bool) { - 217
// home() is already the canonical data home; a further ".vak" - 218
// segment here would nest a bogus nested legacy-named directory - 219
// inside it rather than writing there directly. - 220
let path = home().join("tray.json"); - 221
if let Some(parent) = path.parent() { - 222
std::fs::create_dir_all(parent).ok(); - 223
} - 224
std::fs::write(path, serde_json::json!({ "auto_restart": on }).to_string()).ok(); - 225
} - 226
- 227
fn load_watchdog() -> bool { - 228
std::fs::read_to_string(home().join("tray.json")) - 229
.ok() - 230
.and_then(|s| serde_json::from_str::<serde_json::Value>(&s).ok()) - 231
.and_then(|v| v["auto_restart"].as_bool()) - 232
.unwrap_or(true) - 233
} - 234
- 235
fn home() -> std::path::PathBuf { - 236
// Canonical data home (doc 32) — never hand-roll HOME/.vak. - 237
vak_config::paths::data_home() - 238
} - 239
- 240
fn notify(title: &str, body: &str) { - 241
let _ = notify_rust::Notification::new() - 242
.summary(title) - 243
.body(body) - 244
.show(); - 245
} - 246
- 247
/// The chat window, as a sibling binary next to this one — same - 248
/// directory in an installed bundle (`Contents/MacOS/`), same - 249
/// `target/{debug,release}/` in a dev build. `None` in a dev tree where - 250
/// only the tray was built; the caller decides whether that is worth - 251
/// telling the user about. - 252
fn desktop_binary() -> Option<std::path::PathBuf> { - 253
let sibling = std::env::current_exe().ok()?.parent()?.join("vak-desktop"); - 254
sibling.exists().then_some(sibling) - 255
} - 256
- 257
/// Open the chat window. Safe to call whether or not one is already - 258
/// open: `vak-desktop` enforces single-instance itself and refocuses the - 259
/// existing window instead of duplicating it, so this never needs to - 260
/// track that state here. Spawned detached — the tray outlives the - 261
/// window and must not wait on it or inherit its lifetime. - 262
/// Resolve the managed gateway port from the same user-level environment as - 263
/// the server. The tray is launched directly by the session manager, so it - 264
/// never passes through the CLI's secret-loading path first and has to - 265
/// source the canonical user secret scope itself. - 266
/// - 267
/// That scope is named by `vak_config::user_env_path()`, beside the - 268
/// Shared config layer — not one keyed off `data_home()`, which is where - 269
/// this used to look and where nothing has been written since the - 270
/// canonical layout landed. Reading the wrong scope meant the tray found - 271
/// no `VAK_GATEWAY_TOKEN` and could not build an authenticated admin URL. - 272
fn ops_config() -> vak_ops::OpsConfig { - 273
if let Some(env_path) = vak_config::user_env_path() { - 274
vak_config::replace_env_files(&[env_path.as_path()]); - 275
} - 276
vak_ops::OpsConfig::detect() - 277
} - 278
- 279
/// The token written into the canonical secret scope when setup activates - 280
/// the gateway. `None` - 281
/// on an install that predates that pinning step -- the token still - 282
/// exists (freshly minted on every boot), it is just not discoverable - 283
/// from outside the running process, so there is nothing to build a link - 284
/// with. Read fresh on every click rather than cached at tray startup, - 285
/// so a token added by reinstalling after the tray was already running - 286
/// is picked up immediately. - 287
fn pinned_gateway_token() -> Option<String> { - 288
let path = vak_config::user_env_path()?; - 289
let text = std::fs::read_to_string(path).ok()?; - 290
text.lines().find_map(|line| { - 291
let (key, value) = line.split_once('=')?; - 292
(key.trim() == "VAK_GATEWAY_TOKEN") - 293
.then(|| value.trim().to_string()) - 294
.filter(|v| !v.is_empty()) - 295
}) - 296
} - 297
- 298
/// Open the admin console, pre-authenticated. - 299
/// - 300
/// `?token=` (not `#token=`) deliberately: the SPA's own router treats - 301
/// the entire `location.hash` as the route (`#/overview` and so on, - 302
/// vak-admin-ui/src/store.ts), so a `#token=` fragment would collide - 303
/// with it instead of composing. The query string is unrelated to - 304
/// routing and the SPA scrubs it via `history.replaceState` immediately - 305
/// after logging in, so it does not linger in the address bar. This is - 306
/// the same local-loopback pre-authenticated-link pattern Jupyter's own - 307
/// `?token=` uses; the value is the same bearer token already used for - 308
/// every other authenticated request, not a weaker credential minted - 309
/// for this purpose. - 310
fn open_admin_path(route: &str) { - 311
let cfg = ops_config(); - 312
if vak_ops::status(vak_ops::Service::Gateway, &cfg) != vak_ops::State::Running { - 313
notify( - 314
"Vakyartha", - 315
"start the gateway service first (Gateway → Start)", - 316
); - 317
return; - 318
} - 319
let url = match pinned_gateway_token() { - 320
Some(token) => format!("http://127.0.0.1:{}/admin?token={token}{route}", cfg.port), - 321
None => { - 322
// Older install: no pinned token to build a one-click link - 323
// with. The console still works -- open it to the manual - 324
// login form rather than not opening it at all. - 325
notify( - 326
"Vakyartha", - 327
"no pinned token found — reinstall to enable one-click login; opening manual login", - 328
); - 329
format!("http://127.0.0.1:{}/admin{route}", cfg.port) - 330
} - 331
}; - 332
if let Err(e) = std::process::Command::new("open").arg(url).spawn() { - 333
notify("Vakyartha", &format!("could not open admin console: {e}")); - 334
} - 335
} - 336
- 337
fn open_admin_console() { - 338
open_admin_path("#/overview"); - 339
} - 340
- 341
fn open_operations_center() { - 342
open_admin_path("#/operations"); - 343
} - 344
- 345
fn open_desktop() { - 346
let Some(bin) = desktop_binary() else { - 347
notify( - 348
"Vakyartha", - 349
"Vakyartha desktop app not found next to the tray binary", - 350
); - 351
return; - 352
}; - 353
if let Err(e) = std::process::Command::new(bin).spawn() { - 354
notify("Vakyartha", &format!("could not open Vakyartha: {e}")); - 355
} - 356
} - 357
- 358
/// Exclusive ownership of the menu-bar icon, held for the process's - 359
/// lifetime by whichever tray started first. - 360
/// - 361
/// The bundle's `CFBundleExecutable` is this binary, and - 362
/// `com.vak.tray` also runs it as a launchd service with - 363
/// `RunAtLoad`. So the ordinary path -- install, `services-sync`, then - 364
/// open Vakyartha from Finder, Spotlight, or the Dock -- started a - 365
/// *second* tray and put two identical icons in the menu bar, with no - 366
/// guard anywhere against it. - 367
/// - 368
/// `None` means another live tray already owns the menu bar. The caller - 369
/// then does what launching the app actually asked for -- open the chat - 370
/// window -- and exits, instead of duplicating an icon or (worse, once - 371
/// macOS starts merely re-activating the running app rather than - 372
/// spawning a new one) doing nothing visible at all. - 373
/// - 374
/// Same mechanism as `vak_server::surfaces::telegram::InstanceLock`: an O_EXCL - 375
/// marker plus a liveness probe on the recorded pid, so a crashed holder - 376
/// leaves a marker the next launch reclaims rather than a lock that - 377
/// wedges the menu bar until a reboot. Deliberately not flock, which - 378
/// would need `unsafe` -- the workspace denies it. - 379
struct MenuBarLock { - 380
path: std::path::PathBuf, - 381
} - 382
- 383
impl Drop for MenuBarLock { - 384
fn drop(&mut self) { - 385
let _ = std::fs::remove_file(&self.path); - 386
} - 387
} - 388
- 389
fn claim_menu_bar() -> Option<MenuBarLock> { - 390
let dir = home().join("locks"); - 391
std::fs::create_dir_all(&dir).ok()?; - 392
let path = dir.join("tray.lock"); - 393
- 394
if try_claim(&path) { - 395
return Some(MenuBarLock { path }); - 396
} - 397
// Marker present: only yield to a holder that is actually alive. - 398
let holder = std::fs::read_to_string(&path).unwrap_or_default(); - 399
let pid = holder - 400
.split_whitespace() - 401
.find_map(|t| t.parse::<u32>().ok()); - 402
if pid.is_some_and(pid_alive) { - 403
return None; - 404
} - 405
let _ = std::fs::remove_file(&path); - 406
try_claim(&path).then(|| MenuBarLock { path }) - 407
} - 408
- 409
fn try_claim(path: &std::path::Path) -> bool { - 410
if std::fs::OpenOptions::new() - 411
.write(true) - 412
.create_new(true) - 413
.open(path) - 414
.is_err() - 415
{ - 416
return false; - 417
} - 418
std::fs::write(path, format!("pid {}\n", std::process::id())).is_ok() - 419
} - 420
- 421
/// Liveness probe without libc: `kill -0` via a subprocess, matching - 422
/// how `vak_server::surfaces::telegram::InstanceLock` does it. - 423
fn pid_alive(pid: u32) -> bool { - 424
std::process::Command::new("kill") - 425
.arg("-0") - 426
.arg(pid.to_string()) - 427
.stdout(std::process::Stdio::null()) - 428
.stderr(std::process::Stdio::null()) - 429
.status() - 430
.map(|st| st.success()) - 431
.unwrap_or(false) - 432
} - 433
- 434
fn main() { - 435
// Held for the whole process lifetime: dropping it early would - 436
// release the lock and let a later launch add a second icon. - 437
let Some(_menu_bar) = claim_menu_bar() else { - 438
// Another tray owns the menu bar. Launching the app is a request - 439
// to see the app, so honour that and get out of the way. - 440
open_desktop(); - 441
return; - 442
}; - 443
let watchdog = Arc::new(AtomicBool::new(load_watchdog())); - 444
- 445
// Menu-bar-only, set here rather than via the bundle's LSUIElement. - 446
// The bundle now launches vak-desktop (see install::bundle), and a - 447
// bundle-wide LSUIElement would have hidden that app too. Setting the - 448
// policy on our own event loop keeps this process out of the Dock - 449
// without constraining the app the bundle actually launches. - 450
let event_loop: EventLoop<TrayEvent> = EventLoop::with_user_event() - 451
.with_activation_policy(ActivationPolicy::Accessory) - 452
.build() - 453
.expect("event loop"); - 454
- 455
let cfg = ops_config(); - 456
let proxy = event_loop.create_proxy(); - 457
- 458
// Watchdog + poller thread: computes truth every 3 s, pushes a refresh - 459
// to the UI, and restarts crashed services when enabled. - 460
{ - 461
let watchdog = watchdog.clone(); - 462
let proxy = proxy.clone(); - 463
std::thread::spawn(move || { - 464
let mut last_running = [false; SVC_COUNT]; - 465
let mut last_down_notify = std::time::Instant::now(); - 466
loop { - 467
let mut states = [vak_ops::State::Unknown; SVC_COUNT]; - 468
for (i, st) in states.iter_mut().enumerate() { - 469
*st = vak_ops::status(service(i), &cfg); - 470
} - 471
// launchd/systemd owns recovery. The tray only reports a - 472
// transition so a transient probe can never kill a healthy - 473
// process by issuing a competing restart. - 474
for i in 0..SVC_COUNT { - 475
let running = states[i] == vak_ops::State::Running; - 476
if watchdog.load(Ordering::SeqCst) - 477
&& last_running[i] - 478
&& !running - 479
&& last_down_notify.elapsed() > Duration::from_secs(60) - 480
{ - 481
notify( - 482
"Vakyartha watchdog", - 483
&format!( - 484
"{} is down — the service manager will recover it", - 485
service(i).label() - 486
), - 487
); - 488
last_down_notify = std::time::Instant::now(); - 489
} - 490
last_running[i] = running; - 491
} - 492
let _ = proxy.send_event(TrayEvent::Refresh(states)); - 493
std::thread::sleep(Duration::from_secs(3)); - 494
} - 495
}); - 496
} - 497
- 498
// Build the initial tray before entering the loop. - 499
let menu = build_menu(&states_now(), watchdog.load(Ordering::SeqCst)); - 500
let tray = TrayIconBuilder::new() - 501
.with_menu(Box::new(menu)) - 502
.with_tooltip(status_tooltip(&states_now())) - 503
.with_icon(startup_icon()) - 504
.with_icon_as_template(true) - 505
.build() - 506
.expect("tray built"); - 507
- 508
// Deliberately does NOT open the chat window here. This process is a - 509
// background service (com.vak.tray, RunAtLoad), so doing so - 510
// would throw a window in the user's face at every login. Opening - 511
// the app is now the bundle's job -- its CFBundleExecutable is - 512
// vak-desktop -- and "Open Vakyartha" in the menu covers the rest. - 513
- 514
let mut ui = Ui { - 515
tray, - 516
watchdog, - 517
states: [vak_ops::State::Unknown; 2], - 518
last_rendered: None, - 519
}; - 520
- 521
// Menu clicks arrive on a global channel; forward them as user events so - 522
// everything runs on the UI thread. - 523
let proxy2 = event_loop.create_proxy(); - 524
std::thread::spawn(move || { - 525
let rx = MenuEvent::receiver(); - 526
for ev in rx { - 527
if let Ok(raw) = ev.id.0.parse::<u32>() { - 528
let _ = proxy2.send_event(TrayEvent::Command(raw)); - 529
} - 530
} - 531
}); - 532
- 533
event_loop.run_app(&mut ui).expect("event loop ran"); - 534
} - 535
- 536
// ---- menu construction ------------------------------------------------------ - 537
- 538
fn states_now() -> [vak_ops::State; 2] { - 539
let cfg = ops_config(); - 540
[ - 541
vak_ops::status(vak_ops::Service::Gateway, &cfg), - 542
vak_ops::status(vak_ops::Service::Bridges, &cfg), - 543
] - 544
} - 545
- 546
/// "one glance answers is my agent alive" now lives here rather than in - 547
/// the icon's color: a hover away, exactly as reliable, and it says the - 548
/// actual state in words instead of asking the user to remember what - 549
/// amber means. - 550
fn status_tooltip(states: &[vak_ops::State; 2]) -> String { - 551
// State's Display already renders lowercase ("running", "stopped", …). - 552
format!( - 553
"Vakyartha — gateway {}, chat bridges {}", - 554
states[GATEWAY], states[BRIDGES] - 555
) - 556
} - 557
- 558
fn build_menu(states: &[vak_ops::State; 2], watchdog_on: bool) -> Menu { - 559
let menu = Menu::new(); - 560
// Top of the menu, always present: the three actions a user is - 561
// actually looking for. Everything below is service plumbing. - 562
let open = MenuItem::with_id(ACT_OPEN_DESKTOP.to_string(), "Open Vakyartha", true, None); - 563
let _ = menu.append(&open); - 564
let admin = MenuItem::with_id(ACT_OPEN_ADMIN.to_string(), "Open Admin Console", true, None); - 565
let _ = menu.append(&admin); - 566
let operations = MenuItem::with_id( - 567
ACT_OPEN_OPERATIONS.to_string(), - 568
"Open Operations Center", - 569
true, - 570
None, - 571
); - 572
let _ = menu.append(&operations); - 573
let _ = menu.append(&PredefinedMenuItem::separator()); - 574
for (i, st) in states.iter().enumerate() { - 575
let dot = match st { - 576
vak_ops::State::Running => "●", - 577
vak_ops::State::Stopped => "○", - 578
vak_ops::State::NotInstalled => "×", - 579
vak_ops::State::Unknown => "?", - 580
}; - 581
let base = (i as u32) << 8; - 582
let header = MenuItem::new( - 583
format!("{dot} {} — {}", service(i).label(), st), - 584
false, - 585
None, - 586
); - 587
let _ = menu.append(&header); - 588
- 589
match st { - 590
vak_ops::State::NotInstalled => { - 591
let install = MenuItem::with_id( - 592
(base | ACT_INSTALL).to_string(), - 593
"Install service", - 594
true, - 595
None, - 596
); - 597
let _ = menu.append(&install); - 598
} - 599
other => { - 600
let running = *other == vak_ops::State::Running; - 601
let toggle_label = if running { "Stop" } else { "Start" }; - 602
let toggle_action = if running { ACT_STOP } else { ACT_START }; - 603
let tgl = - 604
MenuItem::with_id((base | toggle_action).to_string(), toggle_label, true, None); - 605
let rst = - 606
MenuItem::with_id((base | ACT_RESTART).to_string(), "Restart", running, None); - 607
let _ = menu.append(&tgl); - 608
let _ = menu.append(&rst); - 609
let un = MenuItem::with_id( - 610
(base | ACT_UNINSTALL).to_string(), - 611
"Uninstall service", - 612
true, - 613
None, - 614
); - 615
let _ = menu.append(&un); - 616
} - 617
} - 618
let log = MenuItem::with_id((base | ACT_LOG).to_string(), "Open log", true, None); - 619
let _ = menu.append(&log); - 620
let _ = menu.append(&PredefinedMenuItem::separator()); - 621
} - 622
- 623
let wd = CheckMenuItem::with_id( - 624
ACT_WATCHDOG_TOGGLE.to_string(), - 625
"Watchdog: alert on service failures", - 626
true, - 627
watchdog_on, - 628
None, - 629
); - 630
let _ = menu.append(&wd); - 631
let _ = menu.append(&PredefinedMenuItem::separator()); - 632
let quit = MenuItem::with_id(ACT_QUIT.to_string(), "Quit tray", true, None); - 633
let _ = menu.append(&quit); - 634
menu - 635
} - 636
- 637
#[cfg(test)] - 638
#[allow(clippy::unwrap_used, clippy::expect_used)] - 639
mod brand_icon_tests { - 640
use super::*; - 641
- 642
#[test] - 643
fn tray_template_has_transparent_ground_and_legible_coverage() { - 644
let (rgba, w, h) = decode_template(BRAND_ICON_PNG).expect("valid tray template"); - 645
assert_eq!((w, h), (36, 36)); - 646
let pixels = rgba.as_chunks::<4>().0; - 647
assert_eq!(pixels[0][3], 0, "no opaque square around the mark"); - 648
assert!(pixels.iter().all(|p| p[0..3] == [0, 0, 0])); - 649
let solid = pixels.iter().filter(|p| p[3] > 128).count(); - 650
assert!( - 651
(300..800).contains(&solid), - 652
"recognisable mark, not a tiny dot or filled tile" - 653
); - 654
let xs: Vec<_> = pixels - 655
.iter() - 656
.enumerate() - 657
.filter(|(_, p)| p[3] > 128) - 658
.map(|(i, _)| i % 36) - 659
.collect(); - 660
assert!(xs.iter().max().unwrap() - xs.iter().min().unwrap() >= 30); - 661
} - 662
- 663
#[test] - 664
fn invalid_template_is_refused() { - 665
assert!(decode_template(b"not a PNG").is_none()); - 666
} - 667
} - 668
Indexing the workspace…
Vakyartha documentation is discovering safe artifacts, anchors, and source references.