All checks were successful
check / check (push) Successful in 1m34s
Med en grupps lista öppen stegar skroll (på gruppikonen eller över listan) markeringen mellan raderna — markerad rad får accentbakgrund och triggar samma förhandsvisning som hover (peek/fokus enligt list_hover). Funkar i både hover- och klickläge; utan öppen lista beter sig skroll som förut (cykla fönster). Watchdogen räknar stegning som aktivitet så listan inte stängs mitt i. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
309 lines
10 KiB
Rust
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: >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),
|
|
current_popover: RefCell::new(None),
|
|
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);
|
|
}
|
|
});
|
|
}
|