Files
waydock/src/main.rs
claude 570ff085bb
All checks were successful
check / check (push) Successful in 1m34s
Grab-fria menyer överallt, popover-stack, radmeny i fönsterlistan
- ALLA popovers är nu grab-fria (autohide av) med gemensam
  auto-stängning: pekar-leave + delay, watchdog om pekaren aldrig når
  popovern, och skroll-stegning räknas som aktivitet. Grabbande menyer
  krockade med tooltips ('grabbing popup with non-top most parent') —
  det var därför högerklicksmenyn ibland inte öppnades.
- Popover-stack ersätter en-åt-gången: en popover vars ankare sitter i
  en öppen popover nästlas i stället för att stänga föräldern, och
  förälderns auto-stängning pausas medan ett barn är överst.
- Högerklick på en rad i fönsterlistan öppnar den rutans egen meny
  (nästlad); högerklick på gruppikonen gäller som förut alla fönster
  i gruppen.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-30 00:58:32 +02:00

309 lines
10 KiB
Rust

// 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<PathBuf>,
log: Option<String>,
log_file: Option<String>,
}
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 <fil> annan configfil (default ~/.config/waydock/config.json)\n \
--log <nivå> error|warn|info|debug|trace (även env WAYDOCK_LOG)\n \
--log-file <fil> 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<Box<dyn simplelog::SharedLogger>> = 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: &gtk::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<RefCell<Option<gtk::CssProvider>>> = 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<dyn Fn()> = 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<dyn Fn()> = 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<dock::Ctx>) {
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<dock::Ctx>, pending: &Rc<Cell<u64>>) {
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);
}
});
}