Files
wldash/src/main.rs
T
Bendik Aagaard LynghaugandClaude Fable 5.1 42ffe9c492 cargo fmt
Pure formatting, no functional change.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-03 09:16:15 +00:00

403 lines
13 KiB
Rust

mod buffer;
mod color;
mod config;
mod draw;
mod event;
mod fonts;
mod keyboard;
mod state;
mod utils;
mod widgets;
use wayland_client::{Connection, QueueHandle, WaylandSource};
use calloop::{
EventLoop,
timer::{TimeoutAction, Timer},
};
use chrono::{Duration, Local, Timelike};
use buffer::BufferView;
use config::Config;
use event::{Event, Events};
use fonts::{FontMap, MaybeFontMap};
use keyboard::{KeyRepeatSource, RepeatMessage};
use state::State;
use utils::{
desktop::{load_desktop_files, write_desktop_cache},
xdg,
};
use widgets::{Geometry, Widget};
use std::{
env, fs,
fs::File,
io::{BufRead, BufReader, Write},
os::unix::net::{UnixListener, UnixStream},
path::Path,
rc::Rc,
thread,
};
fn print_usage() {
println!(
"
usage: wldash [OPTIONS]
OPTIONS:
--config CONFIG_FILE use the specified config file
(/etc/wldash/config.yml if present,
else ~/.config/wldash/config.yml)
--write-default-config [v1|v2|toplevel] generate and write a new config file
(v2 by default)
--desktop-refresh refresh desktop file cache
"
);
}
fn die(msg: String) -> ! {
eprintln!("wldash: {}", msg);
std::process::exit(1);
}
fn ensure_dir_exists(path: &str) {
if let Err(err) = fs::create_dir_all(path) {
die(format!("error creating directory {}: {}", path, err));
}
}
fn create_required_directories() {
let dir_config_wldash = format!("{}/wldash", xdg::config_folder());
let dir_cache_wldash = format!("{}/wldash", xdg::cache_folder());
ensure_dir_exists(&dir_config_wldash);
ensure_dir_exists(&dir_cache_wldash);
}
/// Default config path. On a shared (e.g. NFS-roamed) home, a host-specific
/// config at /etc/wldash/config.yml takes precedence over the roamed
/// ~/.config/wldash/config.yml, so each machine can keep its own layout
/// without conflicting. An explicit --config still overrides both.
fn default_config_file() -> String {
let etc = "/etc/wldash/config.yml";
if Path::new(etc).exists() {
return etc.to_string();
}
format!("{}/wldash/config.yml", xdg::config_folder())
}
fn main() {
let mut args = env::args();
// Skip program name
_ = args.next();
create_required_directories();
let mut config_file = default_config_file();
loop {
match args.next() {
Some(ref s) if s == "--config" => match args.next() {
Some(ref s) => config_file = s.clone(),
None => die("missing argument to --config".to_string()),
},
Some(ref s) if s == "--write-default-config" => {
let config = match args.next() {
Some(ref s) if s == "v1" => Config::generate_v1(),
Some(ref s) if s == "v2" => Config::generate_v2(false),
Some(ref s) if s == "toplevel" => Config::generate_v2(true),
None => Config::generate_v2(false),
Some(s) => die(format!("unknown argument to --write-default-config: {}", s)),
};
match File::create(&config_file) {
Ok(f) => {
if let Err(e) = serde_yaml::to_writer(f, &config) {
die(format!("could not serialize config: {}", e));
}
}
Err(e) => die(format!("could not write {}: {}", config_file, e)),
}
std::process::exit(0);
}
Some(ref s) if s == "--desktop-refresh" => {
let v = load_desktop_files();
if let Err(e) = write_desktop_cache(&v) {
die(format!("could not write desktop cache: {}", e));
}
std::process::exit(0);
}
Some(ref s) if s == "--help" => {
print_usage();
std::process::exit(0);
}
Some(_) => {
print_usage();
std::process::exit(1);
}
None => break,
}
}
let conn = match Connection::connect_to_env() {
Ok(c) => c,
Err(e) => die(format!("could not connect to the Wayland display: {}", e)),
};
let event_queue = conn.new_event_queue();
let qhandle: QueueHandle<State> = event_queue.handle();
let display = conn.display();
display.get_registry(&qhandle, ());
display.sync(&qhandle, ());
let (ping_sender, ping_source) = calloop::ping::make_ping().unwrap();
let (keyrepeat_sender, keyrepeat_channel) = calloop::channel::channel::<RepeatMessage>();
let mut event_loop: EventLoop<State> =
EventLoop::try_new().expect("Failed to initialize the event loop!");
let handle = event_loop.handle();
handle
.insert_source(
WaylandSource::new(event_queue).expect("Could not create WaylandSource!"),
|_event, queue, mut state| queue.dispatch_pending(&mut state),
)
.expect("Failed to insert event source!");
handle
.insert_source(ping_source, |(), &mut (), state| {
let events = state.events.lock().unwrap().flush();
for widget in state.widgets.iter_mut() {
for event in events.iter() {
widget.event(event);
}
}
state.dirty = true;
})
.expect("Failed to insert ping source!");
let clock_source = Timer::from_duration(std::time::Duration::from_secs(1));
handle
.insert_source(clock_source, |_event, _metadata, state| {
let now = Local::now().naive_local();
let target = (now + Duration::seconds(60))
.with_second(0)
.unwrap()
.with_nanosecond(0)
.unwrap();
let d = target - now;
for widget in state.widgets.iter_mut() {
widget.event(&Event::NewMinute);
}
state.dirty = true;
// The timer event source requires us to return a TimeoutAction to
// specify if the timer should be rescheduled. In our case we just drop it.
TimeoutAction::ToDuration(d.to_std().unwrap())
})
.expect("Failed to insert event source!");
let keyrepeat_source = KeyRepeatSource::new(keyrepeat_channel);
handle
.insert_source(keyrepeat_source, |event, _metadata, state| {
let ev = Event::KeyEvent(event);
for widget in state.widgets.iter_mut() {
widget.event(&ev);
}
state.dirty = true;
})
.expect("Failed to insert keyrepeat source!");
let socket_path = match env::var("XDG_RUNTIME_DIR") {
Ok(dir) => dir + "/wldash",
Err(_) => "/tmp/wldash".to_string(),
};
let events = Events::new(ping_sender);
let mut fm = FontMap::new();
let config: Config = match File::open(&config_file) {
Ok(f) => match serde_yaml::from_reader(f) {
Ok(c) => c,
Err(e) => die(format!("invalid configuration {}: {}", config_file, e)),
},
Err(e) => die(format!(
"could not open configuration {}: {} (try 'wldash --write-default-config')",
config_file, e
)),
};
if let Some(true) = config.server {
if let Ok(mut socket) = UnixStream::connect(socket_path.clone()) {
socket.write_all(b"kill\n").unwrap();
return;
};
let _ = std::fs::remove_file(socket_path.clone());
let _ = thread::Builder::new()
.name("ipc_server".to_string())
.spawn(move || {
let listener = UnixListener::bind(socket_path.clone()).unwrap();
loop {
match listener.accept() {
Ok((stream, _)) => {
let _ = thread::Builder::new().name("ipc_client".to_string()).spawn(
move || {
let r = BufReader::new(stream);
for line in r.lines() {
match line {
Ok(ref cmd) if cmd == "kill" => {
std::process::exit(0);
}
_ => {}
}
}
},
);
}
_ => (),
}
}
});
}
if let Some(font_paths) = config.font_paths {
for (key, value) in font_paths.into_iter() {
fm.add_font_path(Box::leak(key.into_boxed_str()), value);
}
}
let layout = Rc::new(config.widget.construct_layout(&mut 0));
let mut widgets: Vec<Box<dyn Widget>> = Vec::new();
config
.widget
.construct_widgets(&mut widgets, &mut fm, &events);
let font_thread = thread::Builder::new()
.name("fontloader".to_string())
.spawn(move || {
fm.load_fonts();
fm
})
.unwrap();
let mut state = State::new(
config.mode,
config.background,
widgets,
layout,
MaybeFontMap::Waiting(font_thread),
events,
keyrepeat_sender,
);
// Initial setup
//
while state.running && !state.ready {
event_loop
.dispatch(None, &mut state)
.expect("Could not dispatch event loop");
}
state.check_registry(&qhandle);
while state.running {
event_loop
.dispatch(None, &mut state)
.expect("Could not dispatch event loop");
if !state.configured || !state.dirty || !state.ready {
continue;
}
let mut force = false;
if state.bufmgr.buffers.is_empty() {
state.add_buffer(&qhandle);
force = true;
}
let mut bufcnt = state.bufmgr.buffers.len();
let buf = match state.bufmgr.next_buffer() {
Some(b) => b,
None => {
if state.bufmgr.buffers.len() >= 3 {
continue;
}
state.add_buffer(&qhandle);
bufcnt = state.bufmgr.buffers.len();
match state.bufmgr.next_buffer() {
Some(b) => b,
None => {
continue;
}
}
}
};
if buf.last_damage.is_empty() {
for _ in 0..state.widgets.len() {
buf.last_damage.push(Geometry::new());
}
}
state.dirty = false;
buf.acquire();
let mut bufview = BufferView::new(
&mut buf.mmap,
(state.dimensions.0 as u32, state.dimensions.1 as u32),
);
let surface = state.main_surface.wl_surface.as_ref().unwrap().clone();
if force {
for (idx, widget) in state.widgets.iter_mut().enumerate() {
if force || widget.get_dirty() {
let geo = widget.geometry();
let mut subview = bufview.subgeometry(geo);
buf.last_damage[idx] =
widget.draw(&mut state.fonts.unwrap().borrow_mut(), &mut subview);
}
}
surface.damage_buffer(0, 0, 0x7FFFFFFF, 0x7FFFFFFF);
} else {
let mut drew = false;
for (idx, widget) in state.widgets.iter_mut().enumerate() {
if bufcnt > 1 || widget.get_dirty() {
drew = true;
let old_damage = buf.last_damage[idx];
bufview.subgeometry(old_damage).clear();
let geo = widget.geometry();
let mut subview = bufview.subgeometry(geo);
let new_damage =
widget.draw(&mut state.fonts.unwrap().borrow_mut(), &mut subview);
let combined_damage = new_damage.expand(old_damage);
buf.last_damage[idx] = new_damage;
surface.damage_buffer(
combined_damage.x as i32,
combined_damage.y as i32,
combined_damage.width as i32,
combined_damage.height as i32,
);
}
}
if !drew {
buf.release();
conn.flush().unwrap();
continue;
}
}
state.ready = false;
surface.attach(Some(&buf.buffer), 0, 0);
surface.frame(&qhandle, ());
surface.commit();
conn.flush().unwrap();
// Now is a good as time as any to load the keymap
state.keyboard.resolve();
}
}