use std::sync::{Arc, Condvar, Mutex};
use std::time::Duration;
use crate::error::{Error, Result, Unsupported};
use crate::geometry::{Edge, LogicalPoint, LogicalRect, LogicalSize};
use crate::menu::{Icon, Menu, MenuId};
use crate::platform::{Appearance, Platform};
use crate::theme::MenuOptions;
use crate::{Tray, TrayCommand};
mod sni;
mod x11;
use ksni::blocking::{Handle, TrayMethods};
use sni::MuriSni;
#[derive(Default)]
#[non_exhaustive]
pub struct LinuxAnchor;
impl LinuxAnchor {
pub fn new() -> Self {
LinuxAnchor
}
fn anchor_rect(&self) -> Result<LogicalRect> {
Err(Error::Unsupported(Unsupported::TrayAnchor))
}
fn supports_tray_anchor(&self) -> bool {
false
}
}
impl std::fmt::Debug for LinuxAnchor {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("LinuxAnchor")
}
}
#[derive(Default)]
pub struct LinuxPlatform {
anchor: LinuxAnchor,
service: Option<Handle<MuriSni>>,
}
impl std::fmt::Debug for LinuxPlatform {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("LinuxPlatform")
.field("anchor", &self.anchor)
.field("installed", &self.service.is_some())
.finish()
}
}
impl LinuxPlatform {
pub fn new() -> Self {
LinuxPlatform {
anchor: LinuxAnchor::new(),
service: None,
}
}
}
impl Platform for LinuxPlatform {
fn install_tray(&mut self, icon: &Icon, tooltip: Option<&str>) -> Result<()> {
let mut tray = Tray::new(icon.clone());
if let Some(tip) = tooltip {
tray = tray.tooltip(tip);
}
let handle = MuriSni {
tray,
visible: true,
}
.spawn()
.map_err(|e| Error::Platform(format!("SNI/StatusNotifierItem registration failed: {e}")))?;
self.service = Some(handle);
Ok(())
}
fn tray_anchor_rect(&self) -> Result<LogicalRect> {
self.anchor.anchor_rect()
}
fn supports_tray_anchor(&self) -> bool {
self.anchor.supports_tray_anchor()
}
fn appearance(&self) -> Appearance {
system_appearance()
}
fn work_area(&self) -> LogicalRect {
LogicalRect::new(LogicalPoint::new(0.0, 0.0), LogicalSize::new(1440.0, 900.0))
}
fn run_tray(self, tray: Tray) -> Result<()> {
run_sni_loop(tray)
}
fn spawn_tray(self, tray: Tray) -> Result<()> {
std::thread::Builder::new()
.name("muri-tray".to_owned())
.spawn(move || {
if let Err(e) = run_sni_loop(tray) {
eprintln!("muri: tray thread exited with error: {e}");
}
})
.map(|_| ())
.map_err(|e| Error::Platform(format!("failed to spawn muri tray thread: {e}")))
}
fn open_popup_session(
&mut self,
menu: Menu,
options: MenuOptions,
on_click: &(dyn Fn(&MenuId) + '_),
anchor: LogicalRect,
edge: Edge,
) -> Result<()> {
if x11::is_available() {
let dark = self.appearance().is_dark();
x11::open_popup_session(menu, options, on_click, anchor, edge, dark)
} else {
Err(x11::wayland_unsupported())
}
}
}
fn run_sni_loop(tray: Tray) -> Result<()> {
let commands = Arc::clone(&tray.commands);
let waker_slot = Arc::clone(&tray.waker);
let handle = MuriSni {
tray,
visible: true,
}
.spawn()
.map_err(|e| Error::Platform(format!("SNI/StatusNotifierItem registration failed: {e}")))?;
let wake: Arc<(Mutex<bool>, Condvar)> = Arc::new((Mutex::new(false), Condvar::new()));
{
let wake = Arc::clone(&wake);
if let Ok(mut slot) = waker_slot.lock() {
*slot = Some(Box::new(move || {
let (lock, cvar) = &*wake;
if let Ok(mut flag) = lock.lock() {
*flag = true;
}
cvar.notify_all();
}));
}
}
loop {
let pending: Vec<TrayCommand> = commands
.lock()
.map(|mut q| std::mem::take(&mut *q))
.unwrap_or_default();
for command in pending {
apply_command(&handle, command);
}
if handle.is_closed() {
break;
}
let (lock, cvar) = &*wake;
let Ok(mut flag) = lock.lock() else { break };
while !*flag {
let (guard, timeout) = cvar
.wait_timeout(flag, Duration::from_millis(500))
.unwrap_or_else(|e| e.into_inner());
flag = guard;
if timeout.timed_out() {
break;
}
}
*flag = false;
}
Ok(())
}
fn apply_command(handle: &Handle<MuriSni>, command: TrayCommand) {
match command {
TrayCommand::SetMenu(menu) => {
handle.update(move |s| s.tray.menu = menu);
}
TrayCommand::SetIcon(icon) => {
handle.update(move |s| s.tray.icon = icon);
}
TrayCommand::SetTooltip(tooltip) => {
handle.update(move |s| s.tray.tooltip = tooltip);
}
TrayCommand::SetVisible(visible) => {
handle.update(move |s| s.visible = visible);
}
TrayCommand::Open | TrayCommand::Close => {}
}
}
fn system_appearance() -> Appearance {
portal_color_scheme_is_dark()
.map(Appearance::from_is_dark)
.unwrap_or(Appearance::Light)
}
fn portal_color_scheme_is_dark() -> Option<bool> {
use zbus::blocking::Connection;
use zbus::zvariant::Value;
fn scheme_is_dark(value: &Value) -> Option<bool> {
match value {
Value::U32(n) => Some(*n == 1),
Value::Value(inner) => scheme_is_dark(inner),
_ => None,
}
}
let connection = Connection::session().ok()?;
let reply = connection
.call_method(
Some("org.freedesktop.portal.Desktop"),
"/org/freedesktop/portal/desktop",
Some("org.freedesktop.portal.Settings"),
"ReadOne",
&("org.freedesktop.appearance", "color-scheme"),
)
.ok()?;
let body = reply.body();
let value: Value = body.deserialize().ok()?;
scheme_is_dark(&value)
}