git.lucas.co / cce-compositor
Wayland compositor (wlroots)
git clone https://git.lucas.co/cce-compositor.git

commiteec487b58a0497474497e4b5990661257fb7f867
parent3bcd22e7a8
authorLucas Galante <[email protected]>
date2026-05-28 15:42
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