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 = 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::(); let mut event_loop: EventLoop = 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> = 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(); } }