Download
216 lines 8.7 KiBRustModified 2026-10-14 raw
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]

mod commands;
mod config;
mod events;
mod hlwm;
mod icons;
mod media;
mod readers;
mod resume;
mod stats;
mod tray;
mod weather;
mod xicon;

#[cfg(target_os = "linux")]
mod linux;

use tauri::{Manager, PhysicalPosition, PhysicalSize, WebviewUrl, WebviewWindowBuilder};

/// Fallback bar height (logical px) when the config has none. The window
/// must be sized before the webview loads, so this mirrors the height
/// default in the frontend's bar schema (src/lib/bar-config.js) — keep
/// the two equal.
pub const DEFAULT_BAR_HEIGHT: f64 = 48.0;

/// Extra transparent space below the bar (logical px) where pop-down panels
/// (volume/brightness sliders) can render. Normally click-through via the
/// X11 input shape; made interactive only while a popup is open.
pub const POPUP_SPACE: f64 = 320.0;

/// Window geometry in physical pixels, shared (behind a Mutex) with
/// commands that toggle the interactive input region or resize the bar.
pub struct Geometry {
    pub width_px: u32,
    pub bar_px: u32,
    pub total_px: u32,
    /// Multiplier from frontend CSS px to physical px (zoom * monitor scale).
    pub css_scale: f64,
    /// Webview zoom factor, also applied to auxiliary windows (settings).
    pub zoom: f64,
    /// Whether a compositor is running; without one the window's bounding
    /// shape must be clipped to visible content (transparency shows black).
    pub composited: bool,
    /// Monitor origin (for strut coordinates in root-window space).
    pub origin_x: i32,
}

pub fn bar_height_from(config: &serde_json::Value) -> f64 {
    config
        .get("bar")
        .and_then(|b| b.get("height"))
        .and_then(serde_json::Value::as_f64)
        .filter(|h| (16.0..=256.0).contains(h))
        .unwrap_or(DEFAULT_BAR_HEIGHT)
}

/// Whether to force WebKitGTK's non-composited (software) rendering path.
///
/// Before WebKitGTK 2.54, accelerated compositing rendered black on
/// shaped/ARGB X11 windows (especially without a compositor), so the
/// software path was the only one that worked. On 2.54+ this legacy path
/// itself renders the bar black, so the override is limited to older
/// WebKit; the 2.54+ compositor-less case is handled in `setup` instead
/// (opaque window + shared-memory renderer). Setting
/// WEBKIT_DISABLE_COMPOSITING_MODE yourself (to "1" or "0") always wins.
#[cfg(target_os = "linux")]
fn force_software_compositing() -> bool {
    // SAFETY: plain FFI getters with no preconditions or side effects.
    let (major, minor) = unsafe {
        (
            webkit2gtk::ffi::webkit_get_major_version(),
            webkit2gtk::ffi::webkit_get_minor_version(),
        )
    };
    (major, minor) < (2, 54)
}

fn main() {
    #[cfg(target_os = "linux")]
    if std::env::var_os("WEBKIT_DISABLE_COMPOSITING_MODE").is_none()
        && force_software_compositing()
    {
        std::env::set_var("WEBKIT_DISABLE_COMPOSITING_MODE", "1");
    }

    tauri::Builder::default()
        .setup(|app| {
            // Whether a compositing manager is running. Without one, the
            // window's bounding shape clips everything outside the bar strip
            // (and any open popup), so the window itself can be opaque. That
            // matters since WebKitGTK 2.54: its renderers paint transparent
            // surfaces as black or only partially on compositor-less X11,
            // whereas an opaque window draws fine. The black backdrop looks
            // identical to what the uncomposited X server showed through the
            // alpha before (it ignores alpha and displays premultiplied RGB).
            #[cfg(target_os = "linux")]
            let composited = linux::screen_is_composited();
            #[cfg(not(target_os = "linux"))]
            let composited = true;

            // Without a compositor the window carries an X11 *bounding*
            // shape (see linux::set_region), and WebKitGTK 2.54's GPU
            // (DMA-BUF) renderer paints nothing into a bounding-shaped
            // window — a plain GtkWindow + WebKitWebView with a shape
            // reproduces it. Its shared-memory renderer, the path WebKit
            // uses when GL is unavailable, works, so select it before the
            // first webview is created. Honor an explicit setting from the
            // environment.
            #[cfg(target_os = "linux")]
            if !composited && std::env::var_os("WEBKIT_DISABLE_DMABUF_RENDERER").is_none() {
                std::env::set_var("WEBKIT_DISABLE_DMABUF_RENDERER", "1");
            }

            let builder = WebviewWindowBuilder::new(app, "main", WebviewUrl::default())
                .title("divebar")
                .decorations(false)
                .always_on_top(true)
                .skip_taskbar(true)
                .visible(false);
            let builder = if composited {
                builder.transparent(true)
            } else {
                builder.background_color(tauri::window::Color(0, 0, 0, 255))
            };
            let window = builder.build()?;

            // HiDPI: WebKitGTK only honors the integer GDK window scale, not
            // font DPI, so allow zooming the whole webview. Priority:
            // DIVEBAR_ZOOM env var > Xft.dpi autodetection > 1.0.
            let zoom = std::env::var("DIVEBAR_ZOOM")
                .ok()
                .and_then(|v| v.parse::<f64>().ok())
                .filter(|z| (0.25..=8.0).contains(z));
            #[cfg(target_os = "linux")]
            let zoom = zoom.or_else(|| linux::detect_zoom(&window));
            let zoom = zoom.unwrap_or(1.0);
            if zoom != 1.0 {
                window.set_zoom(zoom)?;
            }

            // Size the bar to span the primary monitor, glued to its top edge.
            let monitor = window
                .primary_monitor()?
                .or_else(|| window.current_monitor().ok().flatten())
                .expect("no monitor found");
            let scale = monitor.scale_factor();
            let bar_height = bar_height_from(&config::load());
            let bar_height_px = (bar_height * zoom * scale).round() as u32;
            let total_height_px = ((bar_height + POPUP_SPACE) * zoom * scale).round() as u32;
            let monitor_width_px = monitor.size().width;
            let origin = *monitor.position();

            // The window includes transparent popup space below the bar, but
            // only the bar strip reserves screen space (strut) and accepts
            // input by default.
            window.set_size(PhysicalSize::new(monitor_width_px, total_height_px))?;
            window.set_position(PhysicalPosition::new(origin.x, origin.y))?;

            // On X11, mark the window as a dock and reserve screen space so
            // tiled/maximized windows don't overlap the bar.
            #[cfg(target_os = "linux")]
            linux::setup_dock_window(&window, bar_height_px, monitor_width_px, origin.x);

            app.manage(std::sync::Mutex::new(Geometry {
                width_px: monitor_width_px,
                bar_px: bar_height_px,
                total_px: total_height_px,
                css_scale: zoom * scale,
                zoom,
                composited,
                origin_x: origin.x,
            }));

            window.show()?;

            // Shape after mapping — some setups drop shapes applied pre-show.
            #[cfg(target_os = "linux")]
            {
                let bar_strip = [(0, 0, monitor_width_px as i32, bar_height_px as i32)];
                linux::set_region(&window, &bar_strip, !composited);
            }

            stats::spawn_stats_emitter(app.handle().clone());
            hlwm::spawn_hlwm_watcher(app.handle().clone());
            tray::spawn_tray_host(app.handle().clone());
            weather::spawn_weather(app.handle().clone());
            media::spawn_media(app.handle().clone());
            resume::spawn_resume_watcher(app.handle().clone());
            Ok(())
        })
        .invoke_handler(tauri::generate_handler![
            commands::hlwm_use_tag,
            commands::set_volume,
            commands::toggle_mute,
            commands::set_brightness,
            commands::display_off,
            commands::set_popup_open,
            commands::get_env,
            commands::get_latest_states,
            commands::media_control,
            commands::tray_activate,
            commands::tray_menu_click,
            commands::get_config,
            commands::get_default_config,
            commands::save_config,
            commands::open_settings,
            commands::close_settings,
            commands::list_fonts,
            commands::launch_wifi_gui,
            commands::lock_screen,
            commands::resolve_location,
        ])
        .run(tauri::generate_context!())
        .expect("error while running divebar");
}