Wayland compositor (wlroots)
git clone https://git.lucas.co/cce-compositor.git
Update system configuration and interface modules
protocol/river-input-management-v1.xml | 14 +-
protocol/river-libinput-config-v1.xml | 18 +-
protocol/river-window-management-v1.xml | 27 +--
protocol/river-xkb-bindings-v1.xml | 54 +++++-
protocol/river-xkb-config-v1.xml | 287 ++++++++++++++++++++++++++++++++
src/clearctl.rs | 18 +-
src/decorations.rs | 41 +++--
src/input.rs | 6 +-
src/ipc_server.rs | 11 +-
src/lib.rs | 2 +
src/main.rs | 14 +-
src/paths.rs | 97 +++++++++++
src/restart.rs | 10 +-
src/status.rs | 9 +-
src/status_server.rs | 14 +-
src/wayland.rs | 9 +-
start-river.sh | 12 +-
17 files changed, 562 insertions(+), 81 deletions(-)
diff --git a/protocol/river-input-management-v1.xml b/protocol/river-input-management-v1.xml
index 81f707c..e0f5498 100644
--- a/protocol/river-input-management-v1.xml
+++ b/protocol/river-input-management-v1.xml
@@ -32,7 +32,7 @@
document are to be interpreted as described in IETF RFC 2119.
</description>
- <interface name="river_input_manager_v1" version="1">
+ <interface name="river_input_manager_v1" version="2">
<description summary="input manager global interface">
Input manager global interface.
</description>
@@ -110,7 +110,7 @@
</event>
</interface>
- <interface name="river_input_device_v1" version="1">
+ <interface name="river_input_device_v1" version="2">
<description summary="an input device">
An input device represents a physical keyboard, mouse, touchscreen, or
drawing tablet tool. It is assigned to exactly one seat at a time.
@@ -230,5 +230,15 @@
<arg name="width" type="int"/>
<arg name="height" type="int"/>
</request>
+
+ <event name="done" since="2">
+ <description summary="all information has been sent">
+ This event is sent after all information about the input device has
+ been sent.
+
+ This allows changes to one or more river_input_device_v1 properties to
+ be seen as atomic, even if they happen via multiple events.
+ </description>
+ </event>
</interface>
</protocol>
diff --git a/protocol/river-libinput-config-v1.xml b/protocol/river-libinput-config-v1.xml
index 5e38b08..46fad57 100644
--- a/protocol/river-libinput-config-v1.xml
+++ b/protocol/river-libinput-config-v1.xml
@@ -47,7 +47,7 @@
document are to be interpreted as described in IETF RFC 2119.
</description>
- <interface name="river_libinput_config_v1" version="1">
+ <interface name="river_libinput_config_v1" version="2">
<description summary="libinput config global interface">
Global interface for configuring libinput devices. This global should
only be advertised if river_input_manager_v1 is advertised as well.
@@ -115,7 +115,7 @@
</request>
</interface>
- <interface name="river_libinput_device_v1" version="1">
+ <interface name="river_libinput_device_v1" version="2">
<description summary="a libinput device">
In general, *_support events will be sent exactly once directly after the
river_libinput_device_v1 is created. *_default events will be sent after
@@ -411,14 +411,14 @@
</event>
<event name="accel_profile_current">
- <description summary="current send events mode">
+ <description summary="current acceleration profile">
Current acceleration profile.
</description>
<arg name="profile" type="uint" enum="accel_profile"/>
</event>
<request name="set_accel_profile">
- <description summary="set send events mode">
+ <description summary="set acceleration profile">
Set the acceleration profile.
</description>
<arg name="result" type="new_id" interface="river_libinput_result_v1"/>
@@ -828,6 +828,16 @@
<arg name="result" type="new_id" interface="river_libinput_result_v1"/>
<arg name="angle" type="uint"/>
</request>
+
+ <event name="done" since="2">
+ <description summary="all information has been sent">
+ This event is sent after all information about the libinput device has
+ been sent.
+
+ This allows changes to one or more river_libinput_device_v1 properties
+ to be seen as atomic, even if they happen via multiple events.
+ </description>
+ </event>
</interface>
<interface name="river_libinput_accel_config_v1" version="1">
diff --git a/protocol/river-window-management-v1.xml b/protocol/river-window-management-v1.xml
index 608225d..f835971 100644
--- a/protocol/river-window-management-v1.xml
+++ b/protocol/river-window-management-v1.xml
@@ -42,14 +42,14 @@
There are two disjoint categories of state managed by this protocol:
- Window management state influences the communication between the server
- and individual window clients (e.g. xdg_toplevels). Window management
+ Window management state influences the communication between the
+ compositor and individual windows (e.g. xdg_toplevels). Window management
state includes window dimensions, fullscreen state, keyboard focus,
keyboard bindings, and more.
Rendering state only affects the rendered output of the compositor and
- does not influence communication between the server and individual window
- clients. Rendering state includes the position and rendering order of
+ does not influence communication between the compositor and individual
+ windows. Rendering state includes the position and rendering order of
windows, shell surfaces, decoration surfaces, borders, and more.
Window management state may only be modified by the window manager as part
@@ -211,8 +211,8 @@
<event name="render_start">
<description summary="start a render sequence">
- This event indicates that the server has sent all river_node_v1.position
- and river_window_v1.dimensions events necessary.
+ This event indicates that the server has sent all
+ river_window_v1.dimensions events necessary.
In response to this event, the client should make requests modifying
rendering state as it chooses. Then, the client must make the
@@ -620,6 +620,11 @@
The color is defined by four 32-bit RGBA values. Unless specified in
another protocol extension, the RGBA values use pre-multiplied alpha.
+ The valid range for the RGBA values is from 0x00000000 to 0xffffffff.
+ These values are interpreted as a percentage:
+ - 0x00000000 means 0% of the given color component
+ - 0xffffffff means 100% of the given color component
+
Setting the edges to none or the width to 0 disables the borders.
Setting a negative width is a protocol error.
@@ -661,7 +666,7 @@
</request>
<request name="get_decoration_above">
- <description summary="create a decoration surface above the window">
+ <description summary="create a decoration above the window in z-order">
Create a decoration surface and assign the river_decoration_v1 role to
the surface. The created decoration is placed above the window in
rendering order, see the description of river_decoration_v1.
@@ -676,7 +681,7 @@
</request>
<request name="get_decoration_below">
- <description summary="create a decoration surface below the window">
+ <description summary="create a decoration below the window in z-order">
Create a decoration surface and assign the river_decoration_v1 role to
the surface. The created decoration is placed below the window in
rendering order, see the description of river_decoration_v1.
@@ -816,7 +821,7 @@
maximized.
The window manager is free to honor this request using
- river_window_v1.inform_maximize or ignore it.
+ river_window_v1.inform_maximized or ignore it.
This event will be followed by a manage_start event after all other new
state has been sent by the server.
@@ -1358,7 +1363,7 @@
mapping between wl_output and river_output_v1 objects.
The global_remove event for the corresponding wl_output may be sent
- before the river_output_v1.remove event. This is due to the fact that
+ before the river_output_v1.removed event. This is due to the fact that
river_output_v1 state changes are synced to the river window management
manage sequence while changes to globals are not.
@@ -1502,7 +1507,7 @@
mapping between wl_seat and river_seat_v1 objects.
The global_remove event for the corresponding wl_seat may be sent before
- the river_seat_v1.remove event. This is due to the fact that
+ the river_seat_v1.removed event. This is due to the fact that
river_seat_v1 state changes are synced to the river window management
manage sequence while changes to globals are not.
diff --git a/protocol/river-xkb-bindings-v1.xml b/protocol/river-xkb-bindings-v1.xml
index de4f100..55fb72f 100644
--- a/protocol/river-xkb-bindings-v1.xml
+++ b/protocol/river-xkb-bindings-v1.xml
@@ -33,7 +33,7 @@
document are to be interpreted as described in IETF RFC 2119.
</description>
- <interface name="river_xkb_bindings_v1" version="2">
+ <interface name="river_xkb_bindings_v1" version="3">
<description summary="xkbcommon bindings global interface">
This global interface should only be advertised to the client if the
river_window_manager_v1 global is also advertised.
@@ -76,7 +76,7 @@
</request>
</interface>
- <interface name="river_xkb_binding_v1" version="2">
+ <interface name="river_xkb_binding_v1" version="3">
<description summary="configure a xkb key binding, receive trigger events">
This object allows the window manager to configure a xkbcommon key binding
and receive events when the key binding is triggered.
@@ -190,7 +190,7 @@
the window manager has been repeating some action since the pressed
event.
- This event is generally sent when some other (possible unbound) key is
+ This event is generally sent when some other (possibly unbound) key is
pressed after the pressed event is sent and before the released event
is sent for this binding.
@@ -200,7 +200,7 @@
</event>
</interface>
- <interface name="river_xkb_bindings_seat_v1" version="2">
+ <interface name="river_xkb_bindings_seat_v1" version="3">
<description summary="xkb bindings seat">
This object manages xkb bindings state associated with a specific seat.
</description>
@@ -264,5 +264,51 @@
state has been sent by the server.
</description>
</event>
+
+ <request name="modifiers_watch" since="3">
+ <description summary="watch for change in active modifiers">
+ Request that the server send the modifiers_update event whenever a state
+ change occurs for at least one of the modifiers specified by the
+ modifiers argument.
+
+ The window manager should make this request with the modifiers argument
+ set to 0 when it no longer wishes to take action based on a change in
+ modifiers.
+
+ This request modifies window management state and may only be made as
+ part of a manage sequence, see the river_window_manager_v1 description.
+ </description>
+ <arg name="modifiers" type="uint" enum="river_seat_v1.modifiers"/>
+ </request>
+
+ <event name="modifiers_update" since="3">
+ <description summary="active modifiers for the seat changed">
+ The set of currently active modifiers for the seat changed. This event
+ is only sent when there is a change in state for modifiers marked as
+ watched using the modifiers_watch request.
+
+ The old and new arguments convey the set of modifiers active before and
+ after the change. All modifiers are included in the old and new
+ arguments, including modifiers that are not watched.
+
+ Since this event is only sent when there is a change in state for
+ watched modifiers, it follows that at least one watched modifier is
+ active in old but inactive in new or vice-versa.
+
+ This event will be followed by a manage_start event after all other new
+ state has been sent by the server.
+
+ The compositor should wait for the manage sequence to complete before
+ processing further input events. This allows the window manager client
+ to, for example, modify key bindings and keyboard focus without racing
+ against future input events. The window manager should of course respond
+ as soon as possible as the capacity of the compositor to buffer incoming
+ input events is finite.
+ </description>
+ <arg name="old" type="uint" enum="river_seat_v1.modifiers"
+ summary="previously active modifiers"/>
+ <arg name="new" type="uint" enum="river_seat_v1.modifiers"
+ summary="currently active modifiers"/>
+ </event>
</interface>
</protocol>
diff --git a/protocol/river-xkb-config-v1.xml b/protocol/river-xkb-config-v1.xml
new file mode 100644
index 0000000..f73ec67
--- /dev/null
+++ b/protocol/river-xkb-config-v1.xml
@@ -0,0 +1,287 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<protocol name="river_xkb_config_v1">
+ <copyright>
+ SPDX-FileCopyrightText: © 2026 Isaac Freund
+ SPDX-License-Identifier: MIT
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to
+ deal in the Software without restriction, including without limitation the
+ rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ sell copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ IN THE SOFTWARE.
+ </copyright>
+
+ <description summary="configure xkbcommon keyboards">
+ This protocol allow a client to set the xkbcommon keymap of individual
+ keyboard input devices. It also allows switching between the layouts of a
+ keymap and toggling capslock/numlock state.
+
+ The key words "must", "must not", "required", "shall", "shall not",
+ "should", "should not", "recommended", "may", and "optional" in this
+ document are to be interpreted as described in IETF RFC 2119.
+ </description>
+
+ <interface name="river_xkb_config_v1" version="2">
+ <description summary="xkb config global interface">
+ Global interface for configuring xkb devices.
+
+ This global should only be advertised if river_input_manager_v1 is
+ advertised as well.
+ </description>
+
+ <enum name="error">
+ <entry name="invalid_destroy" value="0"/>
+ <entry name="invalid_format" value="1"/>
+ </enum>
+
+ <request name="stop">
+ <description summary="stop sending events">
+ This request indicates that the client no longer wishes to receive
+ events on this object.
+
+ The Wayland protocol is asynchronous, which means the server may send
+ further events until the stop request is processed. The client must wait
+ for a river_xkb_config_v1.finished event before destroying this object.
+ </description>
+ </request>
+
+ <event name="finished">
+ <description summary="the server has finished with the object">
+ This event indicates that the server will send no further events on this
+ object. The client should destroy the object. See
+ river_xkb_config_v1.destroy for more information.
+ </description>
+ </event>
+
+ <request name="destroy" type="destructor">
+ <description summary="destroy the river_xkb_config_v1 object">
+ This request should be called after the finished event has been received
+ to complete destruction of the object.
+
+ It is a protocol error to make this request before the finished event
+ has been received.
+
+ If a client wishes to destroy this object it should send a
+ river_xkb_config_v1.stop request and wait for a
+ river_xkb_config_v1.finished event. Once the finished event is received
+ it is safe to destroy this object and any other objects created through
+ this interface.
+ </description>
+ </request>
+
+ <enum name="keymap_format">
+ <entry name="text_v1" value="1" summary="XKB_KEYMAP_FORMAT_TEXT_V1"/>
+ <entry name="text_v2" value="2" summary="XKB_KEYMAP_FORMAT_TEXT_V2"/>
+ </enum>
+
+ <request name="create_keymap">
+ <description summary="create a keymap object">
+ The server must be able to mmap the fd with MAP_PRIVATE.
+ The server will fstat the fd to obtain the size of the keymap.
+ The client must not modify the contents of the fd after making this request.
+ The client should seal the fd with fcntl.
+ </description>
+ <arg name="id" type="new_id" interface="river_xkb_keymap_v1"/>
+ <arg name="fd" type="fd"/>
+ <arg name="format" type="uint" enum="keymap_format"/>
+ </request>
+
+ <event name="xkb_keyboard">
+ <description summary="new xkb keyboard">
+ A new xkbcommon keyboard has been created. Not every
+ river_input_device_v1 is necessarily an xkbcommon keyboard as well.
+ </description>
+ <arg name="id" type="new_id" interface="river_xkb_keyboard_v1"/>
+ </event>
+ </interface>
+
+ <interface name="river_xkb_keymap_v1" version="2">
+ <description summary="xkbcommon keymap">
+ This object is the result of attempting to create an xkbcommon keymap.
+ </description>
+
+ <request name="destroy" type="destructor">
+ <description summary="destroy the keymap object">
+ This request indicates that the client will no longer use the keymap
+ object and that it may be safely destroyed.
+ </description>
+ </request>
+
+ <event name="success">
+ <description summary="keymap creation succeeded">
+ The keymap object was successfully created and may be used with the
+ river_xkb_keyboard_v1.set_keymap request.
+ </description>
+ </event>
+
+ <event name="failure">
+ <description summary="keymap creation failed">
+ The compositor failed to create a keymap from the given parameters.
+
+ It is a protocol error to use this keymap object with
+ river_xkb_keyboard_v1.set_keymap.
+ </description>
+ <arg name="error_msg" type="string"/>
+ </event>
+ </interface>
+
+ <interface name="river_xkb_keyboard_v1" version="2">
+ <description summary="xkbcommon keyboard device">
+ This object represent a physical keyboard which has its configuration and
+ state managed by xkbcommon.
+ </description>
+
+ <enum name="error">
+ <entry name="invalid_keymap" value="0"/>
+ </enum>
+
+ <request name="destroy" type="destructor">
+ <description summary="destroy the xkb keyboard object">
+ This request indicates that the client will no longer use the keyboard
+ object and that it may be safely destroyed.
+ </description>
+ </request>
+
+ <event name="removed">
+ <description summary="the xkb keyboard is removed">
+ This event indicates that the xkb keyboard has been removed.
+
+ The server will send no further events on this object and ignore any
+ request (other than river_xkb_keyboard_v1.destroy) made after this event
+ is sent. The client should destroy this object with the
+ river_xkb_keyboard_v1.destroy request to free up resources.
+ </description>
+ </event>
+
+ <event name="input_device">
+ <description summary="corresponding river input device">
+ The river_input_device_v1 corresponding to this xkb keyboard. This event
+ will always be the first event sent on the river_xkb_keyboard_v1 object,
+ and it will be sent exactly once.
+ </description>
+ <arg name="device" type="object" interface="river_input_device_v1"/>
+ </event>
+
+ <request name="set_keymap">
+ <description summary="set the keymap">
+ Set the keymap for the keyboard.
+
+ Setting a keymap will reset all layout/modifier state.
+
+ It is a protocol error to pass a keymap object for which the
+ river_xkb_keymap_v1.success event was not received.
+ </description>
+ <arg name="keymap" type="object" interface="river_xkb_keymap_v1"/>
+ </request>
+
+ <request name="set_layout_by_index">
+ <description summary="set the active layout by index">
+ Set the active layout for the keyboard's keymap. Has no effect if the
+ layout index is out of bounds for the current keymap.
+ </description>
+ <arg name="index" type="int"/>
+ </request>
+
+ <request name="set_layout_by_name">
+ <description summary="set the active layout by name">
+ Set the active layout for the keyboard's keymap. Has no effect if there
+ is no layout with the give name for the keyboard's keymap.
+ </description>
+ <arg name="name" type="string"/>
+ </request>
+
+ <event name="layout">
+ <description summary="currently active layout">
+ The currently active layout index and name. The name arg may be null if
+ the active layout does not have a name.
+
+ This event is sent once when the river_xkb_keyboard_v1 is created and
+ again whenever the layout changes.
+ </description>
+ <arg name="index" type="uint"/>
+ <arg name="name" type="string" allow-null="true"/>
+ </event>
+
+ <request name="capslock_enable">
+ <description summary="enable capslock">
+ Enable capslock for the keyboard.
+ </description>
+ </request>
+
+ <request name="capslock_disable">
+ <description summary="disable capslock">
+ Disable capslock for the keyboard.
+ </description>
+ </request>
+
+ <event name="capslock_enabled">
+ <description summary="capslock is currently enabled">
+ Capslock is currently enabled for the keyboard.
+
+ This event is sent once when the river_xkb_keyboard_v1 is created and
+ again whenever the capslock state changes.
+ </description>
+ </event>
+
+ <event name="capslock_disabled">
+ <description summary="capslock is currently disabled">
+ Capslock is currently disabled for the keyboard.
+
+ This event is sent once when the river_xkb_keyboard_v1 is created and
+ again whenever the capslock state changes.
+ </description>
+ </event>
+
+ <request name="numlock_enable">
+ <description summary="enable numlock">
+ Enable numlock for the keyboard.
+ </description>
+ </request>
+
+ <request name="numlock_disable">
+ <description summary="disable numlock">
+ Disable numlock for the keyboard.
+ </description>
+ </request>
+
+ <event name="numlock_enabled">
+ <description summary="numlock is currently enabled">
+ Numlock is currently enabled for the keyboard.
+
+ This event is sent once when the river_xkb_keyboard_v1 is created and
+ again whenever the numlock state changes.
+ </description>
+ </event>
+
+ <event name="numlock_disabled">
+ <description summary="numlock is currently disabled">
+ Numlock is currently disabled for the keyboard.
+
+ This event is sent once when the river_xkb_keyboard_v1 is created and
+ again whenever the numlock state changes.
+ </description>
+ </event>
+
+ <event name="done" since="2">
+ <description summary="all information has been sent">
+ This event is sent after all information about the keyboard has been
+ sent.
+
+ This allows changes to one or more river_xkb_keyboard_v1 properties to
+ be seen as atomic, even if they happen via multiple events.
+ </description>
+ </event>
+ </interface>
+</protocol>
diff --git a/src/clearctl.rs b/src/clearctl.rs
index b57b2f1..ede805a 100644
--- a/src/clearctl.rs
+++ b/src/clearctl.rs
@@ -6,7 +6,19 @@ use std::io::{Read, Write};
use std::os::unix::net::UnixStream;
use std::process;
-const SOCKET_PATH: &str = "/tmp/clearwm.sock";
+fn get_socket_path() -> String {
+ match env::var("WAYLAND_DISPLAY") {
+ Ok(display) => format!("/tmp/clearwm-{}.sock", display),
+ Err(_) => "/tmp/clearwm.sock".to_string(),
+ }
+}
+
+fn get_windows_path() -> String {
+ match env::var("WAYLAND_DISPLAY") {
+ Ok(display) => format!("/tmp/clearwm-windows-{}", display),
+ Err(_) => "/tmp/clearwm-windows".to_string(),
+ }
+}
fn usage(name: &str, to_stderr: bool) {
let print = |s: &str| {
@@ -55,7 +67,7 @@ fn main() {
// Special case: "windows" reads the status file directly
if args[1] == "windows" {
- match fs::read_to_string("/tmp/clearwm-windows") {
+ match fs::read_to_string(get_windows_path()) {
Ok(content) => print!("{}", content),
Err(_) => eprintln!("No windows info (clearwm may not be running)"),
}
@@ -63,7 +75,7 @@ fn main() {
}
// Connect to IPC socket
- let stream = match UnixStream::connect(SOCKET_PATH) {
+ let stream = match UnixStream::connect(get_socket_path()) {
Ok(s) => s,
Err(e) => {
eprintln!("connect: {}", e);
diff --git a/src/decorations.rs b/src/decorations.rs
index 6758623..0112809 100644
--- a/src/decorations.rs
+++ b/src/decorations.rs
@@ -12,7 +12,7 @@ use crate::wayland::AppState;
fn resolve_window_border_font_path() -> Option<String> {
use std::io::Read;
let mut child = std::process::Command::new("fc-match")
- .args(&["-f", "%{file}", "window-borders"])
+ .args(&["-f", "%{file}", "window\\-borders"])
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::null())
.spawn()
@@ -325,10 +325,14 @@ pub fn update_decorations(state: &mut AppState, qhandle: &QueueHandle<AppState>)
let wp = &mut state.window_proxies[wp_idx].1;
+ let scale = (state.wm.output_scale.round() as i32).max(1);
+
// Determine titlebar dimensions:
// Height equals border_width (or 16 if border_width is too small to display font)
- let dec_height = std::cmp::max(border_width, 16);
- let dec_width = win_width + 2 * border_width;
+ let logical_height = std::cmp::max(border_width, 16);
+ let logical_width = win_width + 2 * border_width;
+ let dec_height = logical_height * scale;
+ let dec_width = logical_width * scale;
let needs_new_buffer = match &wp.decoration {
Some(dec) => dec.width != dec_width || dec.height != dec_height,
@@ -405,6 +409,7 @@ pub fn update_decorations(state: &mut AppState, qhandle: &QueueHandle<AppState>)
dec.height = dec_height;
dec.mapped_data = mapped_data;
dec.mapped_size = size;
+ dec.surface.set_buffer_scale(scale);
} else {
eprintln!("[decorations] failed to mmap decoration buffer");
unsafe { libc::close(fd); }
@@ -431,16 +436,16 @@ pub fn update_decorations(state: &mut AppState, qhandle: &QueueHandle<AppState>)
// Draw window title text
if let Some(ref font) = state.border_font {
let font_size = if state.wm.layout.border_font_size > 0 {
- state.wm.layout.border_font_size as f32
+ state.wm.layout.border_font_size as f32 * scale as f32
} else {
- if dec_height >= 32 {
- 22.0
- } else if dec_height >= 24 {
- 16.0
- } else if dec_height >= 16 {
- 11.0
+ if logical_height >= 32 {
+ 22.0 * scale as f32
+ } else if logical_height >= 24 {
+ 16.0 * scale as f32
+ } else if logical_height >= 16 {
+ 11.0 * scale as f32
} else {
- (dec_height as f32 - 4.0).max(8.0)
+ (logical_height as f32 - 4.0).max(8.0) * scale as f32
}
};
@@ -452,7 +457,7 @@ pub fn update_decorations(state: &mut AppState, qhandle: &QueueHandle<AppState>)
});
let baseline_y = (dec_height as f32 + line_metrics.ascent + line_metrics.descent) / 2.0;
- let mut text_x = 8.0f32; // Margin from left
+ let mut text_x = 8.0f32 * scale as f32; // Margin from left
for c in title.chars() {
let (metrics, bitmap) = font.rasterize(c, font_size);
if text_x + metrics.xmin as f32 + metrics.width as f32 > dec_width as f32 {
@@ -499,19 +504,19 @@ pub fn update_decorations(state: &mut AppState, qhandle: &QueueHandle<AppState>)
}
} else {
// Determine font scale (1x if height < 32, 2x if height >= 32)
- let scale = if dec_height >= 32 { 2 } else { 1 };
- let font_h = 8 * scale;
+ let drawing_scale = if logical_height >= 32 { 2 } else { 1 };
+ let font_h = 8 * drawing_scale * scale;
// Vertically center the text inside the titlebar
let text_y = (dec_height - font_h) / 2;
- let mut text_x = 8; // Margin from left
+ let mut text_x = 8 * scale; // Margin from left
for c in title.chars() {
- if text_x + 8 * scale > dec_width {
+ if text_x + 8 * drawing_scale * scale > dec_width {
break; // Out of bounds
}
- draw_char(buffer_slice, dec_width, dec_height, c, text_x, text_y, scale, text_color);
- text_x += 8 * scale;
+ draw_char(buffer_slice, dec_width, dec_height, c, text_x, text_y, drawing_scale * scale, text_color);
+ text_x += 8 * drawing_scale * scale;
}
}
diff --git a/src/input.rs b/src/input.rs
index c0e0d11..53b5887 100644
--- a/src/input.rs
+++ b/src/input.rs
@@ -450,9 +450,9 @@ pub fn run_input_daemon(
// Broadcast coordinate socket setup
let (broadcast_tx, _) = tokio::sync::broadcast::channel::<String>(32);
- let socket_path = "/tmp/clear-input-coords.sock";
- let _ = fs::remove_file(socket_path);
- let listener = tokio::net::UnixListener::bind(socket_path)?;
+ let socket_path = crate::paths::get_input_coords_socket_path();
+ let _ = fs::remove_file(&socket_path);
+ let listener = tokio::net::UnixListener::bind(&socket_path)?;
let b_tx = broadcast_tx.clone();
tokio::spawn(async move {
loop {
diff --git a/src/ipc_server.rs b/src/ipc_server.rs
index a73dcd4..86ef998 100644
--- a/src/ipc_server.rs
+++ b/src/ipc_server.rs
@@ -2,7 +2,7 @@ use std::io::{Read, Write};
use std::os::unix::net::{UnixListener, UnixStream};
use std::sync::mpsc;
-pub const IPC_SOCKET_PATH: &str = "/tmp/clearwm.sock";
+use crate::paths;
pub struct IpcReceiver {
pub rx: mpsc::Receiver<String>,
@@ -24,12 +24,13 @@ pub fn spawn_ipc_server(pipe_write: libc::c_int) -> IpcReceiver {
}
fn ipc_server_main(tx: mpsc::Sender<String>, pipe_write: libc::c_int) {
- let _ = std::fs::remove_file(IPC_SOCKET_PATH);
+ let socket_path = paths::get_socket_path();
+ let _ = std::fs::remove_file(&socket_path);
- let listener = match UnixListener::bind(IPC_SOCKET_PATH) {
+ let listener = match UnixListener::bind(&socket_path) {
Ok(l) => l,
Err(e) => {
- eprintln!("[ipc] failed to bind {}: {}", IPC_SOCKET_PATH, e);
+ eprintln!("[ipc] failed to bind {}: {}", socket_path, e);
return;
}
};
@@ -39,7 +40,7 @@ fn ipc_server_main(tx: mpsc::Sender<String>, pipe_write: libc::c_int) {
return;
}
- eprintln!("[ipc] listening on {}", IPC_SOCKET_PATH);
+ eprintln!("[ipc] listening on {}", socket_path);
let mut streams: Vec<UnixStream> = Vec::new();
diff --git a/src/lib.rs b/src/lib.rs
index 9ed148a..8d13552 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -16,4 +16,6 @@ pub mod types;
pub mod wayland;
pub mod wm;
pub mod input;
+pub mod paths;
+
diff --git a/src/main.rs b/src/main.rs
index 634ef99..df2a918 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -9,7 +9,7 @@ use clearwm::wayland::wayland_init;
use std::env;
use std::fs;
-const SOCKET_PATH: &str = "/tmp/clearwm.sock";
+use clearwm::paths;
/// Write a crash/exit trace to /tmp/clearwm-death.log so we can diagnose
/// why clearwm dies even when the normal log gets overwritten on restart.
@@ -18,7 +18,7 @@ fn log_death(msg: &str) {
if let Ok(mut f) = std::fs::OpenOptions::new()
.create(true)
.append(true)
- .open("/tmp/clearwm-death.log")
+ .open(paths::get_death_log_path())
{
let _ = writeln!(f, "{}", msg);
}
@@ -34,7 +34,7 @@ fn main() {
if let Ok(mut f) = std::fs::OpenOptions::new()
.create(true)
.append(true)
- .open("/tmp/clearwm-death.log")
+ .open(paths::get_death_log_path())
{
let _ = writeln!(f, "{}", msg);
}
@@ -177,7 +177,7 @@ fn main() {
// 2. Flush outgoing requests to the compositor
if let Err(e) = event_queue.flush() {
log_death(&format!("flush error: {:?}", e));
- let _ = std::fs::copy("/tmp/clearwm.log", "/tmp/clearwm-prev.log");
+ let _ = std::fs::copy(paths::get_log_path(), paths::get_prev_log_path());
if !state.wm.exit_requested {
restart::wm_restart();
}
@@ -263,10 +263,10 @@ fn main() {
break;
}
}
- let _ = fs::remove_file(SOCKET_PATH);
+ let _ = fs::remove_file(paths::get_socket_path());
if !state.wm.exit_requested {
// Save log before restart overwrites it
- let _ = std::fs::copy("/tmp/clearwm.log", "/tmp/clearwm-prev.log");
+ let _ = std::fs::copy(paths::get_log_path(), paths::get_prev_log_path());
restart::wm_restart();
}
log_death("main loop exited cleanly");
@@ -292,7 +292,7 @@ extern "C" fn dump_backtrace(_sig: nix::libc::c_int) {
.create(true)
.write(true)
.truncate(true)
- .open("/tmp/clearwm-bt.txt")
+ .open(paths::get_bt_path())
{
let _ = writeln!(f, "SIGUSR2 backtrace:\n{:?}", bt);
let _ = f.sync_all();
diff --git a/src/paths.rs b/src/paths.rs
new file mode 100644
index 0000000..cff52f3
--- /dev/null
+++ b/src/paths.rs
@@ -0,0 +1,97 @@
+use std::env;
+
+pub fn get_socket_path() -> String {
+ if let Ok(display) = env::var("WAYLAND_DISPLAY") {
+ format!("/tmp/clearwm-{}.sock", display)
+ } else {
+ "/tmp/clearwm.sock".to_string()
+ }
+}
+
+pub fn get_status_socket_path() -> String {
+ if let Ok(display) = env::var("WAYLAND_DISPLAY") {
+ format!("/tmp/clearwm-status-{}.sock", display)
+ } else {
+ "/tmp/clearwm-status.sock".to_string()
+ }
+}
+
+pub fn get_windows_path() -> String {
+ if let Ok(display) = env::var("WAYLAND_DISPLAY") {
+ format!("/tmp/clearwm-windows-{}", display)
+ } else {
+ "/tmp/clearwm-windows".to_string()
+ }
+}
+
+pub fn get_tags_path() -> String {
+ if let Ok(display) = env::var("WAYLAND_DISPLAY") {
+ format!("/tmp/clearwm-tags-{}", display)
+ } else {
+ "/tmp/clearwm-tags".to_string()
+ }
+}
+
+pub fn get_layout_path() -> String {
+ if let Ok(display) = env::var("WAYLAND_DISPLAY") {
+ format!("/tmp/clearwm-layout-{}", display)
+ } else {
+ "/tmp/clearwm-layout".to_string()
+ }
+}
+
+pub fn get_title_path() -> String {
+ if let Ok(display) = env::var("WAYLAND_DISPLAY") {
+ format!("/tmp/clearwm-title-{}", display)
+ } else {
+ "/tmp/clearwm-title".to_string()
+ }
+}
+
+pub fn get_death_log_path() -> String {
+ if let Ok(display) = env::var("WAYLAND_DISPLAY") {
+ format!("/tmp/clearwm-death-{}.log", display)
+ } else {
+ "/tmp/clearwm-death.log".to_string()
+ }
+}
+
+pub fn get_bt_path() -> String {
+ if let Ok(display) = env::var("WAYLAND_DISPLAY") {
+ format!("/tmp/clearwm-bt-{}.txt", display)
+ } else {
+ "/tmp/clearwm-bt.txt".to_string()
+ }
+}
+
+pub fn get_input_coords_socket_path() -> String {
+ if let Ok(display) = env::var("WAYLAND_DISPLAY") {
+ format!("/tmp/clear-input-coords-{}.sock", display)
+ } else {
+ "/tmp/clear-input-coords.sock".to_string()
+ }
+}
+
+pub fn get_log_path() -> String {
+ if let Ok(display) = env::var("WAYLAND_DISPLAY") {
+ format!("/tmp/clearwm-{}.log", display)
+ } else {
+ "/tmp/clearwm.log".to_string()
+ }
+}
+
+pub fn get_prev_log_path() -> String {
+ if let Ok(display) = env::var("WAYLAND_DISPLAY") {
+ format!("/tmp/clearwm-{}-prev.log", display)
+ } else {
+ "/tmp/clearwm-prev.log".to_string()
+ }
+}
+
+pub fn get_xprop_path(wid: u64) -> String {
+ if let Ok(display) = env::var("WAYLAND_DISPLAY") {
+ format!("/tmp/clearwm-xprop-{}-{}", wid, display)
+ } else {
+ format!("/tmp/clearwm-xprop-{}", wid)
+ }
+}
diff --git a/src/restart.rs b/src/restart.rs
index 4a7a5e7..1d38e12 100644
--- a/src/restart.rs
+++ b/src/restart.rs
@@ -22,12 +22,14 @@ use crate::types::WindowManager;
pub fn wm_restart() {
use std::time::Instant;
+ use crate::paths;
+
// Write to death log before fork — this is the last chance to capture
// why we're restarting, since fork+process::exit(0) silently kills the parent.
if let Ok(mut f) = std::fs::OpenOptions::new()
.create(true)
.append(true)
- .open("/tmp/clearwm-death.log")
+ .open(paths::get_death_log_path())
{
use std::io::Write;
let _ = writeln!(f, "wm_restart() called — about to fork+exit");
@@ -65,10 +67,10 @@ pub fn wm_restart() {
// Save the current log before River's launch script truncates it on restart.
// This preserves the crash/reason for the restart.
- let _ = std::fs::copy("/tmp/clearwm.log", "/tmp/clearwm-prev.log");
+ let _ = std::fs::copy(paths::get_log_path(), paths::get_prev_log_path());
// Remove the IPC socket
- let _ = std::fs::remove_file("/tmp/clearwm.sock");
+ let _ = std::fs::remove_file(paths::get_socket_path());
// Get the current executable path.
// CString is required because execl() needs a null-terminated C string;
@@ -162,7 +164,7 @@ pub fn wm_restart() {
if let Ok(mut f) = std::fs::OpenOptions::new()
.create(true)
.append(true)
- .open("/tmp/clearwm-death.log")
+ .open(paths::get_death_log_path())
{
use std::io::Write;
let _ = writeln!(
diff --git a/src/status.rs b/src/status.rs
index 49e3d9b..db9d43e 100644
--- a/src/status.rs
+++ b/src/status.rs
@@ -9,10 +9,11 @@ pub const NUM_TAGS: u32 = 4;
/// Write status files only (no pkill signaling). Safe to call inside
/// Dispatch callbacks — no fork, no blocking, just file I/O.
pub fn write_status_files(state: &WindowManager) {
+ use crate::paths;
use std::io::Write;
// /tmp/clearwm-tags: active_tags focused_tags num_tags
- if let Ok(mut f) = fs::File::create("/tmp/clearwm-tags") {
+ if let Ok(mut f) = fs::File::create(paths::get_tags_path()) {
let _ = writeln!(
f,
"{} {} {}",
@@ -30,12 +31,12 @@ pub fn write_status_files(state: &WindowManager) {
.unwrap_or("none")
};
- if let Ok(mut f) = fs::File::create("/tmp/clearwm-layout") {
+ if let Ok(mut f) = fs::File::create(paths::get_layout_path()) {
let _ = writeln!(f, "{}", mode_str);
}
// /tmp/clearwm-windows: one line per window
- if let Ok(mut f) = fs::File::create("/tmp/clearwm-windows") {
+ if let Ok(mut f) = fs::File::create(paths::get_windows_path()) {
let focused_title = state.focused_window().map(|w| w.title.clone());
for win in &state.windows {
@@ -67,7 +68,7 @@ pub fn write_status_files(state: &WindowManager) {
// /tmp/clearwm-title
if let Some(title) = focused_title {
- if let Ok(mut tf) = fs::File::create("/tmp/clearwm-title") {
+ if let Ok(mut tf) = fs::File::create(paths::get_title_path()) {
let _ = writeln!(tf, "{}", title.as_deref().unwrap_or("(null)"));
}
}
diff --git a/src/status_server.rs b/src/status_server.rs
index 9473cac..2db92f9 100644
--- a/src/status_server.rs
+++ b/src/status_server.rs
@@ -13,7 +13,7 @@ use std::os::unix::net::{UnixListener, UnixStream};
use std::sync::mpsc;
/// The socket path for the status server.
-pub const STATUS_SOCKET_PATH: &str = "/tmp/clearwm-status.sock";
+use crate::paths;
/// A status update sent from the main loop to the server thread.
#[derive(Debug, Clone)]
@@ -26,6 +26,7 @@ pub struct StatusUpdate {
pub title_text: String,
}
+
/// Subscription types that waybar scripts can request.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Subscription {
@@ -81,13 +82,14 @@ pub fn spawn_status_server() -> StatusSender {
}
fn status_server_main(rx: mpsc::Receiver<StatusUpdate>) {
+ let socket_path = paths::get_status_socket_path();
// Remove stale socket
- let _ = std::fs::remove_file(STATUS_SOCKET_PATH);
+ let _ = std::fs::remove_file(&socket_path);
- let listener = match UnixListener::bind(STATUS_SOCKET_PATH) {
+ let listener = match UnixListener::bind(&socket_path) {
Ok(l) => l,
Err(e) => {
- eprintln!("[status] failed to bind {}: {}", STATUS_SOCKET_PATH, e);
+ eprintln!("[status] failed to bind {}: {}", socket_path, e);
return;
}
};
@@ -98,7 +100,7 @@ fn status_server_main(rx: mpsc::Receiver<StatusUpdate>) {
return;
}
- eprintln!("[status] listening on {}", STATUS_SOCKET_PATH);
+ eprintln!("[status] listening on {}", socket_path);
let mut clients: Vec<Client> = Vec::new();
let mut latest: Option<StatusUpdate> = None;
@@ -152,7 +154,7 @@ fn status_server_main(rx: mpsc::Receiver<StatusUpdate>) {
Err(mpsc::TryRecvError::Empty) => break,
Err(mpsc::TryRecvError::Disconnected) => {
eprintln!("[status] channel disconnected, exiting");
- let _ = std::fs::remove_file(STATUS_SOCKET_PATH);
+ let _ = std::fs::remove_file(&socket_path);
return;
}
}
diff --git a/src/wayland.rs b/src/wayland.rs
index e427dad..725fa8c 100644
--- a/src/wayland.rs
+++ b/src/wayland.rs
@@ -397,7 +397,7 @@ impl Dispatch<RiverWindowManagerV1, ()> for AppState {
if let Ok(mut f) = std::fs::OpenOptions::new()
.create(true)
.append(true)
- .open("/tmp/clearwm-death.log")
+ .open(crate::paths::get_death_log_path())
{
use std::io::Write;
let _ = writeln!(
@@ -571,7 +571,7 @@ impl Dispatch<RiverWindowManagerV1, ()> for AppState {
continue;
}
window.xprop_check_attempts += 1;
- let path = format!("/tmp/clearwm-xprop-{}", window.id);
+ let path = crate::paths::get_xprop_path(window.id);
if let Ok(content) = std::fs::read_to_string(&path) {
let _ = std::fs::remove_file(&path);
// Each line: title|window_type_line
@@ -1111,14 +1111,15 @@ impl Dispatch<RiverWindowV1, ()> for AppState {
if !window.has_parent && window.pid > 0 {
let pid = window.pid;
let id = window.id;
+ let path = crate::paths::get_xprop_path(id);
let cmd = format!(
"for xid in $(xdotool search --pid {pid} 2>/dev/null); do \
t=$(xdotool getwindowname $xid 2>/dev/null); \
wt=$(xprop -id $xid _NET_WM_WINDOW_TYPE 2>/dev/null); \
printf '%s|%s\\n' \"$t\" \"$wt\"; \
- done > /tmp/clearwm-xprop-{id}",
+ done > {path}",
pid = pid,
- id = id
+ path = path
);
crate::config::spawn_command_bg(&cmd);
window.needs_xprop_check = true;
diff --git a/start-river.sh b/start-river.sh
index 1e725ea..81e451e 100755
--- a/start-river.sh
+++ b/start-river.sh
@@ -18,23 +18,23 @@ export XCURSOR_PATH="/home/lsgalante/.local/share/icons:/home/lsgalante/.icons:/
# Create the River init executable (clearwm launch script)
# This must exist before River starts, and /tmp is cleared on reboot.
if [ "$LOGGING" = true ]; then
- cat > /tmp/clearwm-rs-launch.sh << 'LAUNCH_EOF'
+ cat > /tmp/clearwm-rs-launch-river.sh << 'LAUNCH_EOF'
#!/bin/sh
-exec /home/lsgalante/.local/bin/clearwm 2>/tmp/clearwm.log
+exec /home/lsgalante/.local/bin/clearwm 2>/tmp/clearwm-${WAYLAND_DISPLAY}.log
LAUNCH_EOF
else
- cat > /tmp/clearwm-rs-launch.sh << 'LAUNCH_EOF'
+ cat > /tmp/clearwm-rs-launch-river.sh << 'LAUNCH_EOF'
#!/bin/sh
exec /home/lsgalante/.local/bin/clearwm
LAUNCH_EOF
fi
-chmod +x /tmp/clearwm-rs-launch.sh
+chmod +x /tmp/clearwm-rs-launch-river.sh
echo "Starting river with clearwm..."
if [ "$LOGGING" = true ]; then
- echo "Logs: /tmp/river-clearwm.log + /tmp/clearwm.log"
+ echo "Logs: /tmp/river-clearwm.log + /tmp/clearwm-${WAYLAND_DISPLAY}.log"
else
echo "Logging disabled. Use --logging to enable."
fi
-exec river -c /tmp/clearwm-rs-launch.sh 2>/tmp/river-clearwm.log
+exec river -c /tmp/clearwm-rs-launch-river.sh 2>/tmp/river-clearwm.log