Commit 4e5afcada08b
tarball11 files changed+937-73
@@ -10,7 +10,7 @@ workspace = true
1010
1111
[dependencies]1212
monolith-planner.workspace = true13
-slotmap = "1"13
+slotmap.workspace = true1414
1515
[dev-dependencies]1616
proptest.workspace = true@@ -366,6 +366,18 @@ impl Layout {
366366
}367367
}368368
369
+ /// The tag a launch from `o` belongs to. The focused window's tag, else370
+ /// the first viewed tag, else homeless. Homeless is always on screen, so371
+ /// focusing one of its windows does not mean the user left their tag.372
+ pub fn launch_tag(&self, o: OutputId) -> TagId {373
+ self.focused_on(o)374
+ .map(|w| &self.windows[w])375
+ .filter(|w| w.scratchpad.is_none() && !w.sticky && w.tag != self.homeless)376
+ .map(|w| w.tag)377
+ .or_else(|| self.outputs.get(o)?.viewed.first().copied())378
+ .unwrap_or(self.homeless)379
+ }380
+369381
pub fn view_everywhere(&mut self, tag: TagId) {370382
for o in self.outputs.keys().collect::<Vec<_>>() {371383
self.view(o, tag);@@ -533,12 +545,7 @@ impl Layout {
533545
self.focus(w);534546
return Some(w);535547
}536
- let here = self.outputs[o].area;537
- let next = nearest(538
- here,539
- dir,540
- self.outputs.iter().map(|(id, out)| (id, out.area)),541
- )?;548
+ let next = self.output_towards(o, dir)?;542549
self.active = Some(next);543550
let w = self.focused_on(next);544551
if let Some(w) = w {@@ -547,6 +554,16 @@ impl Layout {
547554
w548555
}549556
557
+ pub fn output_towards(&self, from: OutputId, dir: Dir) -> Option<OutputId> {558
+ let here = self.outputs.get(from)?.area;559
+ let others = self560
+ .outputs561
+ .iter()562
+ .filter(|&(id, _)| id != from)563
+ .map(|(id, out)| (id, out.area));564
+ nearest(here, dir, others)565
+ }566
+550567
fn rect_of(&self, w: WindowId) -> Option<Rect> {551568
let o = self.windows.get(w)?.output?;552569
let arr = self.arrange(o);@@ -13,7 +13,12 @@ profile = ["dep:tracy-client", "tracy-client/enable"]
1313
1414
[dependencies]1515
calloop-wayland-source = "0.4"16
+monolith-config.workspace = true17
+monolith-ipc.workspace = true18
+monolith-layout.workspace = true1619
monolith-render-vk.workspace = true20
+serde_json.workspace = true21
+slotmap.workspace = true1722
smithay = { workspace = true, features = ["desktop"] }1823
tracing = "0.1"1924
tracing-subscriber = { version = "0.3", features = ["env-filter"] }@@ -0,0 +1,190 @@
1
+use crate::state::State;2
+use monolith_config::{Config, Mods};3
+use monolith_ipc::Command;4
+use monolith_layout::{Axis, Dir, ExpandScope, WindowId};5
+use smithay::input::keyboard::{Keysym, ModifiersState, xkb};6
+use std::process::Stdio;7
+8
+pub struct Binds(Vec<(Mods, Keysym, Command)>);9
+10
+impl Binds {11
+ pub fn new(config: &Config) -> Self {12
+ let binds = config13
+ .binds14
+ .iter()15
+ .filter_map(|b| {16
+ let sym = xkb::keysym_from_name(&b.key, xkb::KEYSYM_CASE_INSENSITIVE);17
+ if sym.raw() == xkb::keysyms::KEY_NoSymbol {18
+ tracing::warn!("bind: unknown key {:?}", b.key);19
+ return None;20
+ }21
+ Some((b.mods, sym, b.command.clone()))22
+ })23
+ .collect();24
+ Self(binds)25
+ }26
+27
+ /// Matches on the unshifted keysyms so `Mod+Shift+1` means the 1 key.28
+ pub fn find(&self, mods: &ModifiersState, syms: &[Keysym]) -> Option<Command> {29
+ let held = Mods {30
+ logo: mods.logo,31
+ alt: mods.alt,32
+ ctrl: mods.ctrl,33
+ shift: mods.shift,34
+ };35
+ self.036
+ .iter()37
+ .find(|(m, sym, _)| *m == held && syms.contains(sym))38
+ .map(|(_, _, c)| c.clone())39
+ }40
+}41
+42
+fn dir(d: monolith_ipc::Dir) -> Dir {43
+ match d {44
+ monolith_ipc::Dir::Left => Dir::Left,45
+ monolith_ipc::Dir::Right => Dir::Right,46
+ monolith_ipc::Dir::Up => Dir::Up,47
+ monolith_ipc::Dir::Down => Dir::Down,48
+ }49
+}50
+51
+impl State {52
+ pub fn run(&mut self, cmd: Command) -> Result<(), String> {53
+ let output = self.layout.active_output();54
+ let focused = self.layout.focused();55
+ let on_focused = |state: &mut Self, f: fn(&mut monolith_layout::Layout, WindowId)| {56
+ if let Some(w) = focused {57
+ f(&mut state.layout, w);58
+ }59
+ };60
+ match cmd {61
+ Command::Spawn { argv } => self.spawn(&argv)?,62
+ Command::Close => {63
+ if let Some(t) = focused64
+ .and_then(|w| self.windows.get(&w))65
+ .and_then(|w| w.toplevel())66
+ {67
+ t.send_close();68
+ }69
+ }70
+ Command::Quit => self.signal.stop(),71
+ Command::Reload => self.reload()?,72
+ Command::Focus { dir: d } => {73
+ self.layout.focus_dir(dir(d));74
+ }75
+ Command::Move { dir: d } => {76
+ self.layout.move_window(dir(d));77
+ }78
+ Command::MoveBlock { dir: d } => {79
+ self.layout.move_block(dir(d));80
+ }81
+ Command::Resize { axis, delta } => {82
+ let axis = match axis {83
+ monolith_ipc::Axis::Horizontal => Axis::Horizontal,84
+ monolith_ipc::Axis::Vertical => Axis::Vertical,85
+ };86
+ self.layout.resize(axis, delta);87
+ }88
+ Command::Expand { scope } => {89
+ self.layout.expand(match scope {90
+ monolith_ipc::ExpandScope::Window => ExpandScope::Window,91
+ monolith_ipc::ExpandScope::Block => ExpandScope::Block,92
+ monolith_ipc::ExpandScope::All => ExpandScope::All,93
+ });94
+ }95
+ Command::Flip => output.into_iter().for_each(|o| self.layout.flip(o)),96
+ Command::Float => on_focused(self, |l, w| l.toggle_floating(w)),97
+ Command::Fullscreen => on_focused(self, |l, w| l.toggle_fullscreen(w)),98
+ Command::Sticky => on_focused(self, |l, w| l.toggle_sticky(w)),99
+ Command::View { tag } => {100
+ let t = self.layout.tag(&tag);101
+ output.into_iter().for_each(|o| self.layout.view(o, t));102
+ }103
+ Command::ToggleView { tag } => {104
+ let t = self.layout.tag(&tag);105
+ output106
+ .into_iter()107
+ .for_each(|o| self.layout.toggle_view(o, t));108
+ }109
+ Command::ViewEverywhere { tag } => {110
+ let t = self.layout.tag(&tag);111
+ self.layout.view_everywhere(t);112
+ }113
+ Command::Tag { tag } => {114
+ let t = self.layout.tag(&tag);115
+ if let Some(w) = focused {116
+ self.layout.set_tag(w, t);117
+ }118
+ }119
+ Command::ReorderTag { tag, index } => {120
+ let t = self121
+ .layout122
+ .tag_id(&tag)123
+ .ok_or_else(|| format!("no tag {tag}"))?;124
+ self.layout.reorder_tag(t, index);125
+ }126
+ Command::FocusOutput { dir: d } => {127
+ if let Some(o) = output.and_then(|o| self.layout.output_towards(o, dir(d))) {128
+ self.layout.set_active_output(o);129
+ if let Some(w) = self.layout.focused_on(o) {130
+ self.layout.focus(w);131
+ }132
+ }133
+ }134
+ Command::SendToOutput { dir: d } => {135
+ if let (Some(w), Some(o)) = (136
+ focused,137
+ output.and_then(|o| self.layout.output_towards(o, dir(d))),138
+ ) {139
+ self.layout.send_to_output(w, o);140
+ }141
+ }142
+ Command::Scratchpad { name } => {143
+ if let Some(w) = focused {144
+ self.layout.scratchpad_add(w, &name);145
+ }146
+ }147
+ Command::Summon { name } => {148
+ if !self.layout.scratchpads().iter().any(|s| s.name == name) {149
+ return Err(format!("no scratchpad {name}"));150
+ }151
+ self.layout.summon(&name);152
+ }153
+ Command::Dismiss { name } => self.layout.dismiss(name.as_deref()),154
+ }155
+ self.apply_layout();156
+ Ok(())157
+ }158
+159
+ pub fn spawn(&mut self, argv: &[String]) -> Result<(), String> {160
+ let (program, args) = argv.split_first().ok_or("nothing to spawn")?;161
+ let mut cmd = std::process::Command::new(program);162
+ cmd.args(args)163
+ .env("WAYLAND_DISPLAY", &self.socket_name)164
+ .stdin(Stdio::null());165
+ if let Some(sock) = &self.ipc.path {166
+ cmd.env("MONOLITH_SOCKET", sock);167
+ }168
+ let mut child = cmd.spawn().map_err(|e| format!("{program}: {e}"))?;169
+ let output = self.layout.active_output();170
+ let tag = output.map_or(self.layout.homeless(), |o| self.layout.launch_tag(o));171
+ self.launches.record(child.id(), output, tag);172
+ std::thread::spawn(move || child.wait());173
+ Ok(())174
+ }175
+176
+ /// Swaps in a freshly parsed config. A broken file leaves the running177
+ /// config untouched.178
+ pub fn reload(&mut self) -> Result<(), String> {179
+ let config = Config::load(self.config_path.as_deref()).map_err(|e| e.to_string())?;180
+ let keyboard_changed = config.keyboard != self.config.keyboard;181
+ self.binds = Binds::new(&config);182
+ self.layout.gaps = crate::state::gaps(&config);183
+ self.config = config;184
+ if keyboard_changed {185
+ self.reconfigure_keyboard();186
+ }187
+ tracing::info!("config reloaded");188
+ Ok(())189
+ }190
+}@@ -7,6 +7,8 @@ use smithay::input::dnd::{DndGrabHandler, GrabType, Source};
77
use smithay::input::pointer::CursorImageStatus;88
use smithay::input::{Seat, SeatHandler, SeatState};99
use smithay::reexports::wayland_server::protocol::wl_buffer::WlBuffer;10
+use smithay::reexports::wayland_protocols::xdg::decoration::zv1::server::zxdg_toplevel_decoration_v1::Mode as DecorationMode;11
+use smithay::reexports::wayland_server::protocol::wl_output::WlOutput;1012
use smithay::reexports::wayland_server::protocol::wl_seat::WlSeat;1113
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;1214
use smithay::reexports::wayland_server::{Client, Resource};@@ -21,6 +23,7 @@ use smithay::wayland::selection::SelectionHandler;
2123
use smithay::wayland::selection::data_device::{2224
DataDeviceHandler, DataDeviceState, WaylandDndGrabHandler, set_data_device_focus,2325
};26
+use smithay::wayland::shell::xdg::decoration::XdgDecorationHandler;2427
use smithay::wayland::shell::xdg::{2528
PopupSurface, PositionerState, ToplevelSurface, XdgShellHandler, XdgShellState,2629
};@@ -46,22 +49,25 @@ impl CompositorHandler for State {
4649
root = parent;4750
}4851
if let Some(window) = self49
- .space50
- .elements()52
+ .windows53
+ .values()54
+ .chain(&self.unmapped)5155
.find(|w| w.toplevel().is_some_and(|t| t.wl_surface() == &root))5256
{5357
window.on_commit();5458
}5559
}56
- if let Some(window) = self57
- .space58
- .elements()59
- .find(|w| w.toplevel().is_some_and(|t| t.wl_surface() == surface))60
- .cloned()61
- && let Some(toplevel) = window.toplevel()62
- && !toplevel.is_initial_configure_sent()60
+ if let Some(i) = self61
+ .unmapped62
+ .iter()63
+ .position(|w| w.toplevel().is_some_and(|t| t.wl_surface() == surface))6364
{64
- toplevel.send_configure();65
+ let window = self.unmapped.remove(i);66
+ let toplevel = window.toplevel().cloned();67
+ self.map_window(window);68
+ if let Some(t) = toplevel {69
+ t.send_configure();70
+ }6571
}6672
self.popups.commit(surface);6773
if let Some(PopupKind::Xdg(popup)) = self.popups.find_popup(surface)@@ -108,17 +114,19 @@ impl XdgShellHandler for State {
108114
}109115
110116
fn new_toplevel(&mut self, surface: ToplevelSurface) {111
- let focus = surface.wl_surface().clone();112
- self.space113
- .map_element(Window::new_wayland_window(surface), (0, 0), true);114
- self.relayout();115
- if let Some(keyboard) = self.seat.get_keyboard() {116
- keyboard.set_focus(self, Some(focus), Serial::from(0));117
- }117
+ self.unmapped.push(Window::new_wayland_window(surface));118
+ }119
+120
+ fn toplevel_destroyed(&mut self, surface: ToplevelSurface) {121
+ self.unmap_window(surface.wl_surface());122
+ }123
+124
+ fn fullscreen_request(&mut self, surface: ToplevelSurface, _output: Option<WlOutput>) {125
+ self.set_fullscreen(surface.wl_surface(), true);118126
}119127
120
- fn toplevel_destroyed(&mut self, _surface: ToplevelSurface) {121
- self.relayout();128
+ fn unfullscreen_request(&mut self, surface: ToplevelSurface) {129
+ self.set_fullscreen(surface.wl_surface(), false);122130
}123131
124132
fn new_popup(&mut self, surface: PopupSurface, positioner: PositionerState) {@@ -204,3 +212,36 @@ impl smithay::wayland::pointer_constraints::PointerConstraintsHandler for State
204212
}205213
206214
smithay::delegate_dispatch2!(State);215
+216
+impl XdgDecorationHandler for State {217
+ fn new_decoration(&mut self, toplevel: ToplevelSurface) {218
+ server_side(&toplevel);219
+ }220
+221
+ fn request_mode(&mut self, toplevel: ToplevelSurface, _mode: DecorationMode) {222
+ server_side(&toplevel);223
+ }224
+225
+ fn unset_mode(&mut self, toplevel: ToplevelSurface) {226
+ server_side(&toplevel);227
+ }228
+}229
+230
+/// monolith draws no titlebars, and clients are told so whatever they ask for.231
+fn server_side(toplevel: &ToplevelSurface) {232
+ toplevel.with_pending_state(|s| s.decoration_mode = Some(DecorationMode::ServerSide));233
+ if toplevel.is_initial_configure_sent() {234
+ toplevel.send_pending_configure();235
+ }236
+}237
+238
+impl State {239
+ fn set_fullscreen(&mut self, surface: &WlSurface, on: bool) {240
+ if let Some(id) = self.window_id(surface)241
+ && self.layout.window(id).is_some_and(|w| w.fullscreen != on)242
+ {243
+ self.layout.toggle_fullscreen(id);244
+ self.apply_layout();245
+ }246
+ }247
+}@@ -133,14 +133,37 @@ impl Dispatch<wl_keyboard::WlKeyboard, ()> for State {
133133
_ => KeyState::Released,134134
};135135
let time = InputTime::now();136
- keyboard.input::<(), _>(136
+ let code = Keycode::new(key + 8);137
+ let action = keyboard.input(137138
state,138
- Keycode::new(key + 8),139
+ code,139140
key_state,140141
SERIAL_COUNTER.next_serial(),141142
time,142
- |_, _, _| FilterResult::Forward,143
+ |state, mods, handle| {144
+ if key_state == KeyState::Released {145
+ let before = state.suppressed.len();146
+ state.suppressed.retain(|k| *k != code);147
+ return if state.suppressed.len() < before {148
+ FilterResult::Intercept(None)149
+ } else {150
+ FilterResult::Forward151
+ };152
+ }153
+ match state.binds.find(mods, &handle.raw_syms()) {154
+ Some(cmd) => {155
+ state.suppressed.push(code);156
+ FilterResult::Intercept(Some(cmd))157
+ }158
+ None => FilterResult::Forward,159
+ }160
+ },143161
);162
+ if let Some(Some(cmd)) = action163
+ && let Err(e) = state.run(cmd)164
+ {165
+ tracing::warn!("bind failed: {e}");166
+ }144167
}145168
}146169
}@@ -149,15 +172,15 @@ fn focus_under_pointer(state: &mut State) {
149172
let Some(pos) = state.host.as_ref().map(|h| h.pointer_pos) else {150173
return;151174
};152
- let Some((window, _)) = state.space.element_under(pos).map(|(w, l)| (w.clone(), l)) else {153
- return;154
- };155
- state.space.raise_element(&window, true);156
- if let (Some(keyboard), Some(toplevel)) = (state.seat.get_keyboard(), window.toplevel()) {157
- keyboard.set_focus(158
- state,159
- Some(toplevel.wl_surface().clone()),160
- SERIAL_COUNTER.next_serial(),161
- );175
+ let id = state176
+ .space177
+ .element_under(pos)178
+ .and_then(|(w, _)| w.toplevel())179
+ .and_then(|t| state.window_id(t.wl_surface()));180
+ if let Some(id) = id181
+ && state.layout.focused() != Some(id)182
+ {183
+ state.layout.focus(id);184
+ state.apply_layout();162185
}163186
}@@ -0,0 +1,272 @@
1
+//! The JSONL control socket, and the `msg`, `get` and `subscribe` client2
+//! subcommands that talk to it.3
+4
+use crate::state::{State, identity};5
+use monolith_ipc::{Answer, Command, Envelope, Query, Reply, Request, VERSION};6
+use smithay::reexports::calloop::generic::Generic;7
+use smithay::reexports::calloop::{EventLoop, Interest, LoopHandle, Mode, PostAction};8
+use std::io::{BufRead, BufReader, ErrorKind, Read, Write};9
+use std::os::unix::net::{UnixListener, UnixStream};10
+use std::path::PathBuf;11
+use std::process::ExitCode;12
+13
+#[derive(Default)]14
+pub struct Server {15
+ pub path: Option<PathBuf>,16
+ handle: Option<LoopHandle<'static, State>>,17
+ subscribers: Vec<UnixStream>,18
+ last: Option<monolith_ipc::State>,19
+}20
+21
+impl Drop for Server {22
+ fn drop(&mut self) {23
+ if let Some(p) = &self.path {24
+ let _ = std::fs::remove_file(p);25
+ }26
+ }27
+}28
+29
+pub fn init(event_loop: &EventLoop<'static, State>, state: &mut State) -> std::io::Result<()> {30
+ let Some(path) = state31
+ .socket_name32
+ .to_str()33
+ .and_then(monolith_ipc::socket_path_for)34
+ else {35
+ tracing::warn!("no XDG_RUNTIME_DIR, the control socket is disabled");36
+ return Ok(());37
+ };38
+ let _ = std::fs::remove_file(&path);39
+ let listener = UnixListener::bind(&path)?;40
+ listener.set_nonblocking(true)?;41
+ tracing::info!(path = %path.display(), "control socket");42
+ state.ipc.path = Some(path);43
+ let handle = event_loop.handle();44
+ state.ipc.handle = Some(handle.clone());45
+ handle46
+ .insert_source(47
+ Generic::new(listener, Interest::READ, Mode::Level),48
+ |_, listener, state| {49
+ loop {50
+ match listener.accept() {51
+ Ok((stream, _)) => add_client(state, stream),52
+ Err(e) if e.kind() == ErrorKind::WouldBlock => break,53
+ Err(e) => return Err(e),54
+ }55
+ }56
+ Ok(PostAction::Continue)57
+ },58
+ )59
+ .map_err(|e| std::io::Error::other(e.to_string()))?;60
+ Ok(())61
+}62
+63
+fn add_client(state: &mut State, stream: UnixStream) {64
+ let Some(handle) = state.ipc.handle.clone() else {65
+ return;66
+ };67
+ if stream.set_nonblocking(true).is_err() {68
+ return;69
+ }70
+ let mut buf = Vec::new();71
+ let source = Generic::new(stream, Interest::READ, Mode::Level);72
+ let inserted = handle.insert_source(source, move |_, stream, state| {73
+ let mut chunk = [0u8; 4096];74
+ loop {75
+ match (&**stream).read(&mut chunk) {76
+ Ok(0) => return Ok(PostAction::Remove),77
+ Ok(n) => buf.extend_from_slice(&chunk[..n]),78
+ Err(e) if e.kind() == ErrorKind::WouldBlock => break,79
+ Err(_) => return Ok(PostAction::Remove),80
+ }81
+ }82
+ while let Some(i) = buf.iter().position(|&b| b == b'\n') {83
+ let line: Vec<u8> = buf.drain(..=i).collect();84
+ if let Some(reply) = handle_line(state, stream, &line[..i]) {85
+ send(stream, &reply);86
+ }87
+ }88
+ Ok(PostAction::Continue)89
+ });90
+ if let Err(e) = inserted {91
+ tracing::warn!("ipc client dropped: {e}");92
+ }93
+}94
+95
+fn handle_line(state: &mut State, stream: &UnixStream, line: &[u8]) -> Option<Envelope<Reply>> {96
+ let req: Envelope<Request> = match serde_json::from_slice(line) {97
+ Ok(r) => r,98
+ Err(e) => return Some(Envelope::new(None, Reply::Error(e.to_string()))),99
+ };100
+ let id = req.id;101
+ if req.v != VERSION {102
+ let msg = format!("protocol version {} is not {VERSION}", req.v);103
+ return Some(Envelope::new(id, Reply::Error(msg)));104
+ }105
+ let reply = match req.body {106
+ Request::Run(cmd) => match state.run(cmd) {107
+ Ok(()) => Reply::Ok(None),108
+ Err(e) => Reply::Error(e),109
+ },110
+ Request::Get(Query::State) => Reply::Ok(Some(Answer::State(snapshot(state)))),111
+ Request::Get(Query::Version) => {112
+ Reply::Ok(Some(Answer::Version(env!("CARGO_PKG_VERSION").into())))113
+ }114
+ Request::Subscribe {} => {115
+ let Ok(sub) = stream.try_clone() else {116
+ return Some(Envelope::new(id, Reply::Error("cannot subscribe".into())));117
+ };118
+ send(&sub, &Envelope::new(None, Reply::Event(snapshot(state))));119
+ state.ipc.subscribers.push(sub);120
+ return None;121
+ }122
+ };123
+ Some(Envelope::new(id, reply))124
+}125
+126
+fn send(mut stream: &UnixStream, msg: &Envelope<Reply>) -> bool {127
+ let Ok(mut line) = serde_json::to_vec(msg) else {128
+ return false;129
+ };130
+ line.push(b'\n');131
+ stream.write_all(&line).is_ok()132
+}133
+134
+/// Sends subscribers the new state whenever it differs from the last one.135
+pub fn broadcast(state: &mut State) {136
+ if state.ipc.subscribers.is_empty() {137
+ return;138
+ }139
+ let now = snapshot(state);140
+ if state.ipc.last.as_ref() == Some(&now) {141
+ return;142
+ }143
+ let msg = Envelope::new(None, Reply::Event(now.clone()));144
+ state.ipc.subscribers.retain(|s| send(s, &msg));145
+ state.ipc.last = Some(now);146
+}147
+148
+fn snapshot(state: &State) -> monolith_ipc::State {149
+ let l = &state.layout;150
+ let focused = l.focused();151
+ let tags = l152
+ .tags()153
+ .map(|(_, t)| monolith_ipc::Tag {154
+ name: t.name.clone(),155
+ urgent: t.urgent,156
+ })157
+ .collect();158
+ let outputs = l159
+ .outputs()160
+ .map(|(id, o)| monolith_ipc::Output {161
+ name: o.name.clone(),162
+ area: [o.area.x, o.area.y, o.area.w, o.area.h],163
+ viewed: o164
+ .viewed165
+ .iter()166
+ .filter_map(|&t| l.tags().find(|(id, _)| *id == t))167
+ .map(|(_, t)| t.name.clone())168
+ .collect(),169
+ active: l.active_output() == Some(id),170
+ })171
+ .collect();172
+ let mut windows: Vec<monolith_ipc::Window> = l173
+ .windows()174
+ .map(|(id, w)| {175
+ let (app_id, title) = state176
+ .windows177
+ .get(&id)178
+ .and_then(|w| w.toplevel())179
+ .map(identity)180
+ .unwrap_or_default();181
+ monolith_ipc::Window {182
+ id: slotmap::Key::data(&id).as_ffi(),183
+ app_id,184
+ title,185
+ tag: l186
+ .tags()187
+ .find(|(t, _)| *t == w.tag)188
+ .map(|(_, t)| t.name.clone())189
+ .unwrap_or_default(),190
+ output: w.output.and_then(|o| l.output(o)).map(|o| o.name.clone()),191
+ floating: w.floating,192
+ fullscreen: w.fullscreen,193
+ sticky: w.sticky,194
+ scratchpad: w195
+ .scratchpad196
+ .and_then(|s| l.scratchpads().get(s))197
+ .map(|s| s.name.clone()),198
+ visible: l.visible(id),199
+ focused: focused == Some(id),200
+ }201
+ })202
+ .collect();203
+ windows.sort_by_key(|w| w.id);204
+ monolith_ipc::State {205
+ tags,206
+ outputs,207
+ windows,208
+ }209
+}210
+211
+/// Runs a client subcommand, if `args` names one.212
+pub fn client(args: &[String]) -> Option<ExitCode> {213
+ let (sub, rest) = args.split_first()?;214
+ let request = match sub.as_str() {215
+ "msg" => match Command::parse(rest) {216
+ Ok(cmd) => Request::Run(cmd),217
+ Err(e) => return Some(fail(&e.to_string())),218
+ },219
+ "get" => match rest {220
+ [q] if q == "state" => Request::Get(Query::State),221
+ [q] if q == "version" => Request::Get(Query::Version),222
+ _ => return Some(fail("get takes state or version")),223
+ },224
+ "subscribe" => Request::Subscribe {},225
+ _ => return None,226
+ };227
+ Some(talk(request))228
+}229
+230
+fn fail(msg: &str) -> ExitCode {231
+ eprintln!("monolith: {msg}");232
+ ExitCode::FAILURE233
+}234
+235
+fn talk(request: Request) -> ExitCode {236
+ let Some(path) = monolith_ipc::socket_path() else {237
+ return fail("no socket, set MONOLITH_SOCKET or WAYLAND_DISPLAY");238
+ };239
+ let mut stream = match UnixStream::connect(&path) {240
+ Ok(s) => s,241
+ Err(e) => return fail(&format!("{}: {e}", path.display())),242
+ };243
+ let subscribe = matches!(request, Request::Subscribe {});244
+ let mut line =245
+ serde_json::to_vec(&Envelope::new(Some(1), request)).expect("requests serialize");246
+ line.push(b'\n');247
+ if let Err(e) = stream.write_all(&line) {248
+ return fail(&e.to_string());249
+ }250
+ for line in BufReader::new(stream).lines() {251
+ let Ok(line) = line else { break };252
+ let reply: Envelope<Reply> = match serde_json::from_str(&line) {253
+ Ok(r) => r,254
+ Err(e) => return fail(&format!("bad reply: {e}")),255
+ };256
+ match reply.body {257
+ Reply::Error(e) => return fail(&e),258
+ Reply::Ok(None) => {}259
+ Reply::Ok(Some(answer)) => match answer {260
+ Answer::Version(v) => println!("{v}"),261
+ Answer::State(s) => {262
+ println!("{}", serde_json::to_string_pretty(&s).unwrap_or_default())263
+ }264
+ },265
+ Reply::Event(s) => println!("{}", serde_json::to_string(&s).unwrap_or_default()),266
+ }267
+ if !subscribe {268
+ break;269
+ }270
+ }271
+ ExitCode::SUCCESS272
+}@@ -0,0 +1,47 @@
1
+//! Launch context. A window goes to the output and tag the user was on when2
+//! its launch began, found by matching its process or an ancestor against3
+//! the programs monolith spawned.4
+5
+use monolith_layout::{OutputId, TagId};6
+use std::time::{Duration, Instant};7
+8
+const KEEP: Duration = Duration::from_secs(120);9
+10
+struct Launch {11
+ pid: u32,12
+ output: Option<OutputId>,13
+ tag: TagId,14
+ at: Instant,15
+}16
+17
+#[derive(Default)]18
+pub struct Launches(Vec<Launch>);19
+20
+impl Launches {21
+ pub fn record(&mut self, pid: u32, output: Option<OutputId>, tag: TagId) {22
+ self.0.retain(|l| l.at.elapsed() < KEEP);23
+ self.0.push(Launch {24
+ pid,25
+ output,26
+ tag,27
+ at: Instant::now(),28
+ });29
+ }30
+31
+ pub fn find(&self, pid: u32) -> Option<(Option<OutputId>, TagId)> {32
+ let mut pid = pid;33
+ for _ in 0..32 {34
+ if let Some(l) = self.0.iter().find(|l| l.pid == pid) {35
+ return Some((l.output, l.tag));36
+ }37
+ pid = parent(pid).filter(|&p| p > 1)?;38
+ }39
+ None40
+ }41
+}42
+43
+fn parent(pid: u32) -> Option<u32> {44
+ let stat = std::fs::read_to_string(format!("/proc/{pid}/stat")).ok()?;45
+ let rest = stat.get(stat.rfind(')')? + 2..)?;46
+ rest.split(' ').nth(1)?.parse().ok()47
+}@@ -1,32 +1,53 @@
1
+mod commands;12
mod handlers;23
mod input;4
+mod ipc;5
+mod launch;36
mod nested;47
mod state;58
69
use smithay::reexports::calloop::EventLoop;710
use smithay::reexports::wayland_server::Display;811
use state::State;9
-use std::process::Command;12
+use std::process::ExitCode;1013
use tracing_subscriber::EnvFilter;1114
12
-fn main() -> Result<(), Box<dyn std::error::Error>> {15
+fn main() -> ExitCode {16
+ let args: Vec<String> = std::env::args().skip(1).collect();17
+ if let Some(code) = ipc::client(&args) {18
+ return code;19
+ }1320
tracing_subscriber::fmt()1421
.with_env_filter(1522
EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info")),1623
)1724
.init();18
- let mut event_loop: EventLoop<State> = EventLoop::try_new()?;25
+ match run(&args) {26
+ Ok(()) => ExitCode::SUCCESS,27
+ Err(e) => {28
+ tracing::error!("{e}");29
+ ExitCode::FAILURE30
+ }31
+ }32
+}33
+34
+/// Runs the compositor. Any arguments are a program to start inside it.35
+fn run(args: &[String]) -> Result<(), Box<dyn std::error::Error>> {36
+ let mut event_loop: EventLoop<'static, State> = EventLoop::try_new()?;1937
let display: Display<State> = Display::new()?;2038
let mut state = State::new(&mut event_loop, display)?;2139
nested::init(&mut event_loop, &mut state)?;40
+ ipc::init(&event_loop, &mut state)?;2241
tracing::info!(socket = ?state.socket_name, device = state.renderer.device_name(), "monolith running nested");2342
24
- let args: Vec<String> = std::env::args().skip(1).collect();25
- if let Some((program, rest)) = args.split_first() {26
- Command::new(program)27
- .args(rest)28
- .env("WAYLAND_DISPLAY", &state.socket_name)29
- .spawn()?;43
+ let startup = state.config.startup.clone();44
+ for argv in startup45
+ .iter()46
+ .chain((!args.is_empty()).then_some(&args.to_vec()))47
+ {48
+ if let Err(e) = state.spawn(argv) {49
+ tracing::warn!("startup: {e}");50
+ }3051
}3152
3253
event_loop.run(None, &mut state, |state| state.after_dispatch())?;@@ -5,6 +5,7 @@
55
66
use crate::state::State;77
use calloop_wayland_source::WaylandSource;8
+use monolith_layout::Rect;89
use monolith_render_vk::VulkanAllocator;910
use smithay::backend::allocator::dmabuf::Dmabuf;1011
use smithay::backend::allocator::{Format, Fourcc, Modifier, Slot, Swapchain};@@ -107,6 +108,13 @@ pub fn init(
107108
);108109
output.set_preferred(mode);109110
state.space.map_output(&output, (0, 0));111
+ let area = Rect::new(112
+ 0.0,113
+ 0.0,114
+ f64::from(DEFAULT_SIZE.0),115
+ f64::from(DEFAULT_SIZE.1),116
+ );117
+ state.output_id = Some(state.layout.add_output("nested", area));110118
111119
state.host = Some(Host {112120
size: DEFAULT_SIZE.into(),@@ -359,7 +367,7 @@ impl Dispatch<xdg_toplevel::XdgToplevel, ()> for State {
359367
{360368
host.pending_size = Some(size);361369
host.size = size;362
- state.relayout();370
+ state.resize_output(size);363371
}364372
}365373
xdg_toplevel::Event::Close => state.signal.stop(),@@ -1,22 +1,30 @@
1
+use crate::commands::Binds;12
use crate::nested::Host;3
+use monolith_config::Config;4
+use monolith_layout::{Layout, OutputId, Rect, WindowId};25
use monolith_render_vk::VulkanRenderer;36
use smithay::backend::renderer::ImportDma;47
use smithay::desktop::{PopupManager, Space, Window, WindowSurfaceType};8
+use smithay::input::keyboard::{Keycode, XkbConfig};59
use smithay::input::{Seat, SeatState};610
use smithay::reexports::calloop::generic::Generic;711
use smithay::reexports::calloop::{EventLoop, Interest, LoopSignal, Mode, PostAction};12
+use smithay::reexports::wayland_protocols::xdg::shell::server::xdg_toplevel::State as XdgState;813
use smithay::reexports::wayland_server::backend::{ClientData, ClientId, DisconnectReason};914
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;10
-use smithay::reexports::wayland_server::{Display, DisplayHandle};11
-use smithay::utils::{Logical, Point, Size};12
-use smithay::wayland::compositor::{CompositorClientState, CompositorState};15
+use smithay::reexports::wayland_server::{Display, DisplayHandle, Resource};16
+use smithay::utils::{Logical, Point, SERIAL_COUNTER, Size};17
+use smithay::wayland::compositor::{CompositorClientState, CompositorState, with_states};1318
use smithay::wayland::dmabuf::{DmabufGlobal, DmabufState};1419
use smithay::wayland::output::OutputManagerState;1520
use smithay::wayland::selection::data_device::DataDeviceState;16
-use smithay::wayland::shell::xdg::XdgShellState;21
+use smithay::wayland::shell::xdg::decoration::XdgDecorationState;22
+use smithay::wayland::shell::xdg::{XdgShellState, XdgToplevelSurfaceData};1723
use smithay::wayland::shm::ShmState;1824
use smithay::wayland::socket::ListeningSocketSource;25
+use std::collections::HashMap;1926
use std::ffi::OsString;27
+use std::path::PathBuf;2028
use std::sync::Arc;2129
use std::time::Instant;2230
@@ -31,8 +39,22 @@ pub struct State {
3139
pub host: Option<Host>,3240
pub needs_redraw: bool,3341
42
+ pub layout: Layout,43
+ pub windows: HashMap<WindowId, Window>,44
+ /// Toplevels that have not committed yet. Rules need their app_id and45
+ /// title, which clients set after creating the toplevel.46
+ pub unmapped: Vec<Window>,47
+ pub output_id: Option<OutputId>,48
+ pub config: Config,49
+ pub config_path: Option<PathBuf>,50
+ pub binds: Binds,51
+ pub suppressed: Vec<Keycode>,52
+ pub ipc: crate::ipc::Server,53
+ pub launches: crate::launch::Launches,54
+3455
pub compositor_state: CompositorState,3556
pub xdg_shell_state: XdgShellState,57
+ pub _xdg_decoration_state: XdgDecorationState,3658
pub shm_state: ShmState,3759
pub dmabuf_state: DmabufState,3860
pub _dmabuf_global: DmabufGlobal,@@ -48,12 +70,18 @@ impl State {
4870
display: Display<State>,4971
) -> Result<Self, Box<dyn std::error::Error>> {5072
let dh = display.handle();73
+ let config_path = Config::path();74
+ let config = Config::load(config_path.as_deref()).unwrap_or_else(|e| {75
+ tracing::error!("config: {e}, using the built-in default");76
+ Config::parse(monolith_config::DEFAULT).expect("the default config parses")77
+ });5178
let renderer = VulkanRenderer::new()?;5279
let mut dmabuf_state = DmabufState::new();5380
let dmabuf_global = dmabuf_state.create_global::<State>(&dh, renderer.dmabuf_formats());5481
let mut seat_state = SeatState::new();5582
let mut seat = seat_state.new_wl_seat(&dh, "seat0");56
- seat.add_keyboard(Default::default(), 200, 25)?;83
+ let kb = &config.keyboard;84
+ seat.add_keyboard(xkb_config(kb), kb.repeat_delay, kb.repeat_rate)?;5785
seat.add_pointer();5886
5987
let listener = ListeningSocketSource::new_auto()?;@@ -76,6 +104,9 @@ impl State {
76104
},77105
)?;78106
107
+ let mut layout = Layout::default();108
+ layout.gaps = gaps(&config);109
+ let binds = Binds::new(&config);79110
Ok(Self {80111
start: Instant::now(),81112
socket_name,@@ -85,8 +116,19 @@ impl State {
85116
popups: PopupManager::default(),86117
host: None,87118
needs_redraw: true,119
+ layout,120
+ windows: HashMap::new(),121
+ unmapped: Vec::new(),122
+ output_id: None,123
+ config,124
+ config_path,125
+ binds,126
+ suppressed: Vec::new(),127
+ ipc: crate::ipc::Server::default(),128
+ launches: Default::default(),88129
compositor_state: CompositorState::new::<State>(&dh),89130
xdg_shell_state: XdgShellState::new::<State>(&dh),131
+ _xdg_decoration_state: XdgDecorationState::new::<State>(&dh),90132
shm_state: ShmState::new::<State>(&dh, vec![]),91133
dmabuf_state,92134
_dmabuf_global: dmabuf_global,@@ -103,37 +145,157 @@ impl State {
103145
self.space.refresh();104146
self.popups.cleanup();105147
crate::nested::render(self);148
+ crate::ipc::broadcast(self);106149
let _ = self.display.flush_clients();107150
}108151
109
- pub fn output_size(&self) -> Size<i32, Logical> {110
- self.host.as_ref().map_or((1280, 800).into(), |h| h.size)152
+ pub fn resize_output(&mut self, size: Size<i32, Logical>) {153
+ if let Some(o) = self.output_id {154
+ let area = Rect::new(0.0, 0.0, f64::from(size.w), f64::from(size.h));155
+ self.layout.set_output_area(o, area);156
+ self.apply_layout();157
+ }111158
}112159
113
- /// Lays toplevels out side by side across the output. A stand-in until114
- /// the layout crate takes over.115
- pub fn relayout(&mut self) {116
- let windows: Vec<Window> = self.space.elements().cloned().collect();117
- let size = self.output_size();118
- let n = windows.len() as i32;119
- if n == 0 {120
- return;160
+ /// Adds a toplevel to the layout on its first commit, where its app_id161
+ /// and title are known and the initial configure can carry its size.162
+ pub fn map_window(&mut self, window: Window) {163
+ let (app_id, title) = window.toplevel().map(identity).unwrap_or_default();164
+ let rule = self.config.placement(&app_id, &title);165
+ let launch = window166
+ .toplevel()167
+ .and_then(|t| t.wl_surface().client())168
+ .and_then(|c| c.get_credentials(&self.display).ok())169
+ .and_then(|c| self.launches.find(c.pid as u32));170
+ let output = rule171
+ .output172
+ .as_deref()173
+ .and_then(|name| {174
+ self.layout175
+ .outputs()176
+ .find(|(_, o)| o.name == name)177
+ .map(|(id, _)| id)178
+ })179
+ .or(launch.and_then(|l| l.0));180
+ let tag = match rule.tag.as_deref() {181
+ Some(t) => Some(self.layout.tag(t)),182
+ None => launch.map(|l| l.1),183
+ };184
+ let id = self.layout.add_window(output, tag, None);185
+ if rule.floating == Some(true) {186
+ self.layout.toggle_floating(id);187
+ }188
+ if rule.sticky == Some(true) {189
+ self.layout.toggle_sticky(id);121190
}122
- let gap = 8;123
- let w = (size.w - gap * (n + 1)) / n;124
- for (i, window) in windows.into_iter().enumerate() {125
- let loc = (gap + i as i32 * (w + gap), gap);126
- if let Some(toplevel) = window.toplevel() {127
- toplevel.with_pending_state(|s| s.size = Some((w, size.h - 2 * gap).into()));128
- if toplevel.is_initial_configure_sent() {129
- toplevel.send_pending_configure();191
+ if let Some(name) = &rule.scratchpad {192
+ self.layout.scratchpad_add(id, name);193
+ }194
+ self.windows.insert(id, window);195
+ if self.layout.visible(id) {196
+ self.layout.focus(id);197
+ }198
+ self.apply_layout();199
+ }200
+201
+ pub fn unmap_window(&mut self, surface: &WlSurface) {202
+ self.unmapped203
+ .retain(|w| w.toplevel().is_some_and(|t| t.wl_surface() != surface));204
+ if let Some(id) = self.window_id(surface) {205
+ if let Some(window) = self.windows.remove(&id) {206
+ self.space.unmap_elem(&window);207
+ }208
+ self.layout.remove_window(id);209
+ self.apply_layout();210
+ }211
+ }212
+213
+ pub fn window_id(&self, surface: &WlSurface) -> Option<WindowId> {214
+ self.windows215
+ .iter()216
+ .find(|(_, w)| w.toplevel().is_some_and(|t| t.wl_surface() == surface))217
+ .map(|(id, _)| *id)218
+ }219
+220
+ /// Pushes the layout's target geometry into the space and to clients.221
+ pub fn apply_layout(&mut self) {222
+ let mut shown = Vec::new();223
+ let outputs: Vec<OutputId> = self.layout.outputs().map(|(o, _)| o).collect();224
+ for o in outputs {225
+ let arr = self.layout.arrange(o);226
+ let area = self.layout.output(o).map(|o| o.area).unwrap_or_default();227
+ for p in &arr.tiled {228
+ if p.visible && arr.fullscreen != Some(p.window) {229
+ shown.push((p.window, p.rect, Kind::Tiled));230
+ }231
+ }232
+ for &(w, rect) in &arr.floating {233
+ if arr.fullscreen != Some(w) {234
+ shown.push((w, rect, Kind::Floating));130235
}131236
}132
- self.space.map_element(window, loc, false);237
+ if let Some(w) = arr.fullscreen {238
+ shown.push((w, area, Kind::Fullscreen));239
+ }240
+ }241
+ let mut mapped = Vec::new();242
+ for (id, rect, kind) in shown {243
+ if let Some(window) = self.windows.get(&id)244
+ && configure(window, rect, kind)245
+ {246
+ let loc = (rect.x.round() as i32, rect.y.round() as i32);247
+ let loc = Point::from(loc) - window.geometry().loc;248
+ self.space.map_element(window.clone(), loc, false);249
+ mapped.push(id);250
+ }133251
}252
+ let hidden: Vec<Window> = self253
+ .windows254
+ .iter()255
+ .filter(|(id, _)| !mapped.contains(id))256
+ .map(|(_, w)| w.clone())257
+ .collect();258
+ for window in hidden {259
+ self.space.unmap_elem(&window);260
+ }261
+ for id in &mapped {262
+ self.space.raise_element(&self.windows[id], false);263
+ }264
+ self.sync_focus();134265
self.needs_redraw = true;135266
}136267
268
+ fn sync_focus(&mut self) {269
+ let focused = self.layout.focused();270
+ for (id, window) in &self.windows {271
+ if window.set_activated(Some(*id) == focused)272
+ && let Some(t) = window.toplevel()273
+ && t.is_initial_configure_sent()274
+ {275
+ t.send_pending_configure();276
+ }277
+ }278
+ let target = focused279
+ .and_then(|id| self.windows.get(&id))280
+ .and_then(|w| w.toplevel())281
+ .map(|t| t.wl_surface().clone());282
+ if let Some(keyboard) = self.seat.get_keyboard()283
+ && keyboard.current_focus() != target284
+ {285
+ keyboard.set_focus(self, target, SERIAL_COUNTER.next_serial());286
+ }287
+ }288
+289
+ pub fn reconfigure_keyboard(&mut self) {290
+ let kb = self.config.keyboard.clone();291
+ if let Some(keyboard) = self.seat.get_keyboard() {292
+ if let Err(e) = keyboard.set_xkb_config(self, xkb_config(&kb)) {293
+ tracing::warn!("keyboard config rejected: {e:?}");294
+ }295
+ keyboard.change_repeat_info(kb.repeat_rate, kb.repeat_delay);296
+ }297
+ }298
+137299
pub fn surface_under(138300
&self,139301
pos: Point<f64, Logical>,@@ -148,6 +310,84 @@ impl State {
148310
}149311
}150312
313
+#[derive(Clone, Copy, PartialEq, Eq)]314
+enum Kind {315
+ Tiled,316
+ Floating,317
+ Fullscreen,318
+}319
+320
+/// Sets a window's pending size and states. Returns false for windows321
+/// squeezed to a line by an expanded sibling, which are not shown.322
+fn configure(window: &Window, rect: Rect, kind: Kind) -> bool {323
+ let (x, y) = (rect.x.round(), rect.y.round());324
+ let w = (rect.right().round() - x) as i32;325
+ let h = (rect.bottom().round() - y) as i32;326
+ if w < 1 || h < 1 {327
+ return false;328
+ }329
+ if let Some(t) = window.toplevel() {330
+ t.with_pending_state(|s| {331
+ s.size = Some((w, h).into());332
+ let tiled = [333
+ XdgState::TiledLeft,334
+ XdgState::TiledRight,335
+ XdgState::TiledTop,336
+ XdgState::TiledBottom,337
+ ];338
+ for st in tiled {339
+ if kind == Kind::Tiled {340
+ s.states.set(st);341
+ } else {342
+ s.states.unset(st);343
+ }344
+ }345
+ if kind == Kind::Fullscreen {346
+ s.states.set(XdgState::Fullscreen);347
+ } else {348
+ s.states.unset(XdgState::Fullscreen);349
+ }350
+ });351
+ if t.is_initial_configure_sent() {352
+ t.send_pending_configure();353
+ }354
+ }355
+ true356
+}357
+358
+pub fn identity(t: &smithay::wayland::shell::xdg::ToplevelSurface) -> (String, String) {359
+ with_states(t.wl_surface(), |s| {360
+ let data = s361
+ .data_map362
+ .get::<XdgToplevelSurfaceData>()363
+ .and_then(|d| d.lock().ok());364
+ data.map(|d| {365
+ (366
+ d.app_id.clone().unwrap_or_default(),367
+ d.title.clone().unwrap_or_default(),368
+ )369
+ })370
+ .unwrap_or_default()371
+ })372
+}373
+374
+pub fn gaps(config: &Config) -> monolith_layout::Gaps {375
+ monolith_layout::Gaps {376
+ inner: config.general.gaps.inner,377
+ outer: config.general.gaps.outer,378
+ }379
+}380
+381
+fn xkb_config(kb: &monolith_config::Keyboard) -> XkbConfig<'_> {382
+ XkbConfig {383
+ rules: &kb.rules,384
+ model: &kb.model,385
+ layout: &kb.layout,386
+ variant: &kb.variant,387
+ options: kb.options.clone(),388
+ }389
+}390
+151391
#[derive(Default)]152392
pub struct ClientState {153393
pub compositor_state: CompositorClientState,