// waydock — modern, resurssnål dock för Wayfire. // Se doc/architecture.md för helheten. mod config; mod dock; mod hotspots; mod icons; mod menu; mod settings; mod style; mod toplevel; mod wayfire_ipc; use std::cell::{Cell, RefCell}; use std::path::PathBuf; use std::rc::Rc; use std::time::Duration; use gtk4 as gtk; use gtk4::gdk; use gtk4::glib; use gtk4::prelude::*; use log::{error, info, warn, LevelFilter}; const VERSION: &str = env!("CARGO_PKG_VERSION"); struct Cli { config: Option, log: Option, log_file: Option, } fn parse_cli() -> Cli { let mut cli = Cli { config: None, log: None, log_file: None }; let mut args = std::env::args().skip(1); while let Some(arg) = args.next() { match arg.as_str() { "--config" => cli.config = args.next().map(PathBuf::from), "--log" => cli.log = args.next(), "--log-file" => cli.log_file = args.next(), "--version" | "-V" => { println!("waydock {VERSION}"); std::process::exit(0); } "--help" | "-h" => { println!( "waydock {VERSION} — dock för Wayfire\n\n\ FLAGGOR:\n \ --config annan configfil (default ~/.config/waydock/config.json)\n \ --log error|warn|info|debug|trace (även env WAYDOCK_LOG)\n \ --log-file skriv logg till fil\n \ --version visa version" ); std::process::exit(0); } other => { eprintln!("okänd flagga: {other} (se --help)"); std::process::exit(2); } } } cli } fn init_logging(cli: &Cli, cfg: &config::Config) { let level_str = cli .log .clone() .or_else(|| std::env::var("WAYDOCK_LOG").ok()) .unwrap_or_else(|| cfg.log.level.clone()); let level = match level_str.to_lowercase().as_str() { "error" => LevelFilter::Error, "warn" => LevelFilter::Warn, "debug" => LevelFilter::Debug, "trace" => LevelFilter::Trace, _ => LevelFilter::Info, }; let log_cfg = simplelog::ConfigBuilder::new() .set_time_format_rfc3339() .build(); let mut loggers: Vec> = vec![simplelog::TermLogger::new( level, log_cfg.clone(), simplelog::TerminalMode::Stderr, simplelog::ColorChoice::Auto, )]; let file_path = cli.log_file.clone().or_else(|| cfg.log.file.clone()); if let Some(path) = file_path { let expanded = if let Some(rest) = path.strip_prefix("~/") { glib::home_dir().join(rest) } else { PathBuf::from(&path) }; match std::fs::File::create(&expanded) { Ok(f) => loggers.push(simplelog::WriteLogger::new(level, log_cfg, f)), Err(e) => eprintln!("kunde inte öppna loggfil {path}: {e}"), } } let _ = simplelog::CombinedLogger::init(loggers); } /// Delar av configen som kräver att fönstren byggs om (inte bara CSS). fn layout_signature(cfg: &config::Config) -> String { format!( "{:?}|{:?}|{}|{}|{}|{}|{:?}|{}|{}|{}|{:?}", cfg.position, cfg.alignment, cfg.expand, cfg.margin, cfg.margin_start, cfg.margin_end, cfg.layer, cfg.avoid_windows, cfg.appearance.shadow.enabled, cfg.appearance.shadow.blur, serde_json::to_string(&cfg.outputs).unwrap_or_default(), ) + &format!("|{:?}", cfg.autohide) } fn main() -> glib::ExitCode { let cli = parse_cli(); let cfg_path = cli.config.clone().unwrap_or_else(config::default_path); let initial_cfg = config::load_or_create(&cfg_path); init_logging(&cli, &initial_cfg); info!("waydock {VERSION} — config: {}", cfg_path.display()); let app = gtk::Application::new(Some("eu.brassepc.waydock"), Default::default()); let started = Rc::new(Cell::new(false)); let cfg_path2 = cfg_path.clone(); app.connect_activate(move |app| { if started.get() { info!("waydock kör redan — ignorerar andra instansen"); return; } started.set(true); activate(app, cfg_path2.clone()); }); // ge inte GTK våra egna flaggor app.run_with_args::<&str>(&[]) } fn activate(app: >k::Application, cfg_path: PathBuf) { let cfg: config::SharedConfig = Rc::new(RefCell::new(config::load_or_create(&cfg_path))); let ctx = Rc::new(dock::Ctx { cfg: cfg.clone(), cfg_path: cfg_path.clone(), service: RefCell::new(None), docks: RefCell::new(Vec::new()), menu_target: RefCell::new(None), app: app.clone(), on_config_changed: RefCell::new(None), save_generation: Cell::new(0), urgent: RefCell::new(Default::default()), hotspot_windows: RefCell::new(Vec::new()), open_popovers: Cell::new(0), refresh_pending: Cell::new(false), popover_stack: RefCell::new(Vec::new()), group_list: RefCell::new(None), }); // CSS + rebuild/refresh-logik let css_provider: Rc>> = Rc::new(RefCell::new(None)); let last_signature = Rc::new(RefCell::new(String::new())); { let ctx2 = ctx.clone(); let css_provider = css_provider.clone(); let last_signature = last_signature.clone(); let apply: Rc = Rc::new(move || { let cfg = ctx2.cfg.borrow(); let provider = style::apply(&cfg, css_provider.borrow().as_ref()); *css_provider.borrow_mut() = Some(provider); let sig = layout_signature(&cfg); drop(cfg); if *last_signature.borrow() != sig { *last_signature.borrow_mut() = sig; dock::rebuild_all(&ctx2); } else { dock::refresh_items(&ctx2); } hotspots::rebuild(&ctx2); }); *ctx.on_config_changed.borrow_mut() = Some(apply); } // fönsterspårningen { let ctx2 = ctx.clone(); let on_update: Rc = Rc::new(move || dock::refresh_items(&ctx2)); match toplevel::start(on_update) { Ok(service) => *ctx.service.borrow_mut() = Some(service), Err(e) => { error!("{e} — docken visar bara pins"); } } } if wayfire_ipc::available() { subscribe_wayfire_events(&ctx); } else { warn!("Wayfire-IPC saknas — översikt/flytta-till-skärm/dodge avstängda (aktivera ipc-pluginen i wayfire.ini)"); } // första uppritningen if let Some(cb) = ctx.on_config_changed.borrow().as_ref() { cb(); } // live-omladdning när configfilen ändras { let ctx2 = ctx.clone(); config::watch( &cfg_path, Rc::new(move || { info!("configfilen ändrad — laddar om"); let fresh = config::load_or_create(&ctx2.cfg_path); *ctx2.cfg.borrow_mut() = fresh; if let Some(cb) = ctx2.on_config_changed.borrow().as_ref() { cb(); } }), ); } // skärmar in/ut → bygg om (debouncat, skärmar kommer i klungor) { let ctx2 = ctx.clone(); let pending = Rc::new(Cell::new(0u64)); let display = gdk::Display::default().expect("ingen display"); display.monitors().connect_items_changed(move |_, _, _, _| { let generation = pending.get() + 1; pending.set(generation); let ctx3 = ctx2.clone(); let pending2 = pending.clone(); glib::timeout_add_local_once(Duration::from_millis(500), move || { if pending2.get() == generation { info!("skärmuppsättningen ändrad — bygger om dockorna"); dock::rebuild_all(&ctx3); } }); }); } } /// Händelsestyrd dodge + urgency via Wayfires eventström. fn subscribe_wayfire_events(ctx: &Rc) { let ctx2 = ctx.clone(); let pending = Rc::new(Cell::new(0u64)); let result = wayfire_ipc::subscribe(Rc::new(move |ev| { let name = ev["event"].as_str().unwrap_or(""); match name { "view-hints-changed" => { let view = &ev["view"]; let app_id = view["app-id"].as_str().unwrap_or(""); if app_id.is_empty() { return; } let key = icons::lookup(app_id).key; let wants = ev["hints"]["demands-attention"].as_bool() .or_else(|| view["demands-attention"].as_bool()) .unwrap_or(false); let changed = if wants { ctx2.urgent.borrow_mut().insert(key) } else { ctx2.urgent.borrow_mut().remove(&key) }; if changed { dock::refresh_items(&ctx2); } } "view-focused" => { let app_id = ev["view"]["app-id"].as_str().unwrap_or(""); if !app_id.is_empty() { let key = icons::lookup(app_id).key; if ctx2.urgent.borrow_mut().remove(&key) { dock::refresh_items(&ctx2); } } schedule_dodge(&ctx2, &pending); } n if n.starts_with("view-") || n.starts_with("output-") => { schedule_dodge(&ctx2, &pending); } _ => {} } })); if let Err(e) = result { warn!("kunde inte prenumerera på wayfire-events: {e}"); } } /// Debounca dodge-omräkningen (events kommer i skurar). fn schedule_dodge(ctx: &Rc, pending: &Rc>) { if !ctx.cfg.borrow().autohide.enabled { return; } let generation = pending.get() + 1; pending.set(generation); let ctx = ctx.clone(); let pending = pending.clone(); glib::timeout_add_local_once(Duration::from_millis(120), move || { if pending.get() == generation { dock::recompute_dodge(&ctx); } }); }