Wayland compositor (wlroots)
git clone https://git.lucas.co/cce-compositor.git
refactor: depend on cce-core, not cce-ui
The compositor used cce-ui for config (paths, read, write), input.kdl
bindings, motion::enabled and the relief/droplet spec parsers — and
linked the whole toolkit for it: ash, smithay, cosmic-text, resvg, naga,
gpu-allocator. Those modules are cce-core now (cce-ui re-exports them), so
this depends on cce-core alone: Cargo.toml swaps the pin, and src renames
cce_ui::{config,input,motion,relief_spec} to cce_core::… and
scene::paint::DropletSpec to droplet::DropletSpec. Nothing else changes.
WORKSPACE.md names cce-core as the toolkit's GUI-free half and a shared
crate, and points the config/input sections at its files.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Cargo.toml | 8 +++++---
WORKSPACE.md | 15 ++++++++++-----
src/migrate_input.rs | 10 +++++-----
src/server/config.rs | 12 ++++++------
src/server/cursor.rs | 8 ++++----
src/server/layer_shell.rs | 2 +-
src/server/screenshot.rs | 4 ++--
src/server/window.rs | 24 ++++++++++++------------
src/server/window_manager.rs | 6 +++---
9 files changed, 48 insertions(+), 41 deletions(-)
diff --git a/Cargo.toml b/Cargo.toml
index 89b77b19..583e9771 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -7,14 +7,16 @@ edition = "2021"
doctest = false
[dependencies]
-cce-ui = { git = "https://github.com/lsgalante/cce-ui.git", rev = "4dcb970d84f0a6aeeef5311ac1d4cacb0c63a803" }
+# The GUI-free half of the toolkit: config, input bindings, the animations
+# switch and the relief/droplet spec parsers. It depended on cce-ui for these
+# until 2026-10-07, linking the whole Wayland/Vulkan/text stack it never used.
+cce-core = { git = "https://github.com/lsgalante/cce-core.git", rev = "39e989d6ebc7850a1e5aa96bf228fe8714e538e0" }
cce-window-manager = { git = "https://github.com/lsgalante/cce-window-manager.git", rev = "a55a5b02151f52e84d92a75994e00aa3c8f0a61f" }
xkbcommon = "0.7"
serde = { version = "1", features = ["derive"] }
nix = { version = "0.29", features = ["signal", "process", "fs", "poll", "resource"] }
libc = "0.2"
-# logind's sleep-delay inhibitor (`sleep_lock`). Already in the tree through
-# cce-ui (rfd -> ashpd), so this adds the blocking API, not a dependency.
+# logind's sleep-delay inhibitor (`sleep_lock`).
zbus = "5"
bitflags = "2"
tokio = { version = "1.35", features = ["full"] }
diff --git a/WORKSPACE.md b/WORKSPACE.md
index 875cee95..9952fb89 100644
--- a/WORKSPACE.md
+++ b/WORKSPACE.md
@@ -31,8 +31,13 @@ desktop environment written in Rust, split into two halves:
natively by the compositor — the former `cce-wallpaper` client was retired.)
The one thing tying every crate together is **`cce-ui`**, the shared GUI toolkit. Every
-client depends on it (`cce-ui = { path = "../cce-ui" }`); the compositor depends on it
-too. There is one other shared crate: **`cce-window-manager`** — the compositor's
+client depends on it (a git pin that the root `[patch]` redirects to `../cce-ui`). Its
+GUI-free half is **`cce-core`** — config, input bindings, the animations switch, units, the
+socket IPC and the DE's spec parsers — which cce-ui re-exports at the old paths
+(`cce_ui::config`, …) and which a process that draws nothing depends on directly: the
+compositor (since 2026-10-07; it linked the whole toolkit for its config before), the
+browser's launch helper and cce-window-manager. The other shared crate is
+**`cce-window-manager`** — the compositor's
pure-Rust window-management policy layer (arrange pass, `TilingMode`, saved state,
slotmap; no FFI), extracted from `cce-compositor/` and consumed only by it. The compositor
re-exports it as `crate::policy` / `crate::tiling` / `crate::slotmap`.
@@ -269,13 +274,13 @@ When adding a widget or a client, mirror an existing client (e.g.
## Configuration (shared across the whole DE)
Config is **KDL** (`kdl` crate), loaded from `~/.config/cce/` (honoring
-`XDG_CONFIG_HOME`), via `cce-ui/src/config.rs`:
+`XDG_CONFIG_HOME`), via `cce-core/src/config.rs` (re-exported as `cce_ui::config`):
- **`~/.config/cce/config.kdl`** — the shared/global config (`get_config_path()`).
- **`~/.config/cce/<app-name>/config.kdl`** — per-app override
(`get_app_config_path(app_name)`).
- **`~/.config/cce/input.kdl`** — DE-wide keybindings and pointer input settings,
- domain-scoped (`cce-ui/src/input.rs`): top-level nodes are domains
+ domain-scoped (`cce-core/src/input.rs`, re-exported as `cce_ui::input`): top-level nodes are domains
(`cce-window-manager` for compositor actions, `cce-ui` for toolkit-wide widget
defaults, `cce-<app>` for per-app bindings), children are `name "chord"`
bindings. Resolution for an app is `<app>.<name>` → `cce-ui.<name>` (the
@@ -454,7 +459,7 @@ the others. The rules:
with `ps -eo pid,ppid,cmd`, confirm it is yours via
`/proc/<pid>/cgroup` (a unit's processes name their unit), `kill` it
explicitly, then relaunch detached.
-- **Shared crates are exclusive.** Before editing `cce-ui`,
+- **Shared crates are exclusive.** Before editing `cce-ui`, `cce-core`,
`cce-window-manager`, or `cce-icons`, run `git status` there. Foreign dirt
means another session owns that crate right now — coordinate or stop; don't
edit around it. Commit your own crate's work promptly so other sessions
diff --git a/src/migrate_input.rs b/src/migrate_input.rs
index 33496d19..d3b16b40 100644
--- a/src/migrate_input.rs
+++ b/src/migrate_input.rs
@@ -14,7 +14,7 @@
// The brightness spawn binds synthesized from the `display` section are NOT
// migrated — they stay derived from the display config at load time.
-use cce_ui::input::{BindingEntry, InputConfig, UI_DOMAIN, WINDOW_MANAGER_DOMAIN};
+use cce_core::input::{BindingEntry, InputConfig, UI_DOMAIN, WINDOW_MANAGER_DOMAIN};
use cce_window_manager::api::Action;
use cce_window_manager::bindings::parse_chord;
@@ -182,7 +182,7 @@ fn backup(path: &std::path::Path) -> Option<std::path::PathBuf> {
if !path.exists() {
return None;
}
- let backups = cce_ui::config::cce_config_dir().join("backups");
+ let backups = cce_core::config::cce_config_dir().join("backups");
let _ = std::fs::create_dir_all(&backups);
for n in 1..1000 {
let candidate = backups.join(format!("input.kdl.{}.bak", n));
@@ -194,7 +194,7 @@ fn backup(path: &std::path::Path) -> Option<std::path::PathBuf> {
}
pub fn run() {
- let config_path = cce_ui::config::get_config_path();
+ let config_path = cce_core::config::get_config_path();
let content = match std::fs::read_to_string(&config_path) {
Ok(c) => c,
Err(e) => {
@@ -217,7 +217,7 @@ pub fn run() {
return;
}
- let input_path = cce_ui::input::get_input_path();
+ let input_path = cce_core::input::get_input_path();
let existing_content = std::fs::read_to_string(&input_path).unwrap_or_default();
let existing = match InputConfig::parse(&existing_content) {
Ok(c) => c,
@@ -241,7 +241,7 @@ pub fn run() {
if added == 0 {
return;
}
- match cce_ui::input::write_domain(&input_path, domain, entries) {
+ match cce_core::input::write_domain(&input_path, domain, entries) {
Ok(()) => println!("{}: migrated {} binding(s)", domain, added),
Err(e) => {
eprintln!("failed to write {}: {}", input_path.display(), e);
diff --git a/src/server/config.rs b/src/server/config.rs
index d03b4af0..1a66d951 100644
--- a/src/server/config.rs
+++ b/src/server/config.rs
@@ -1320,7 +1320,7 @@ pub fn default_config_path() -> Option<String> {
/// `<state_home>/cce/state.json`; `None` when neither `$XDG_STATE_HOME` nor
/// `$HOME` is set, rather than a path relative to wherever we were started.
pub fn default_state_path() -> Option<String> {
- let path = cce_ui::config::cce_state_dir().join("state.json");
+ let path = cce_core::config::cce_state_dir().join("state.json");
path.is_absolute().then(|| path.to_string_lossy().into_owned())
}
@@ -1782,7 +1782,7 @@ fn parse_kdl_config(content: &str) -> Result<Config, String> {
// ~/.config/cce/cce-status-interface/config.kdl, `module { spacing height }`.
// Re-read on every config (re)load, so `ccectl reload` picks up edits.
{
- let app_cfg = cce_ui::config::get_app_config_path("cce-status-interface");
+ let app_cfg = cce_core::config::get_app_config_path("cce-status-interface");
if let Ok(content) = std::fs::read_to_string(&app_cfg) {
if let Ok(app_doc) = content.parse::<kdl::KdlDocument>() {
if let Some(module) = app_doc.nodes().iter().find(|n| n.name().value() == "module") {
@@ -2236,7 +2236,7 @@ fn parse_kdl_config(content: &str) -> Result<Config, String> {
// installs the full material,
// but the scenefx chamfer has no
// custom profile to install.
- if let Some(spec) = cce_ui::relief_spec::ReliefSpec::parse(s) {
+ if let Some(spec) = cce_core::relief_spec::ReliefSpec::parse(s) {
surface.desktop_line_relief = spec.width.round() as i64;
}
}
@@ -2678,15 +2678,15 @@ pub fn parse_config(path: &str, state: &mut crate::window_manager::WindowManager
let path_buf = std::path::Path::new(path);
let input_path = path_buf.parent().unwrap_or_else(|| std::path::Path::new(".")).join("input.kdl");
- let mut wm_domain_entries: Vec<cce_ui::input::BindingEntry> = Vec::new();
+ let mut wm_domain_entries: Vec<cce_core::input::BindingEntry> = Vec::new();
if input_path.exists() {
if let Ok(input_content) = fs::read_to_string(&input_path) {
// New domain-scoped format: a `cce-window-manager { ... }` block
// of `<action_name> "<chord>"` bindings. Other domains belong to
// clients/widgets and are ignored here.
- match cce_ui::input::InputConfig::parse(&input_content) {
+ match cce_core::input::InputConfig::parse(&input_content) {
Ok(ic) => {
- wm_domain_entries = ic.domain(cce_ui::input::WINDOW_MANAGER_DOMAIN).to_vec();
+ wm_domain_entries = ic.domain(cce_core::input::WINDOW_MANAGER_DOMAIN).to_vec();
}
Err(e) => eprintln!("[WARNING] {}: {}", input_path.display(), e),
}
diff --git a/src/server/cursor.rs b/src/server/cursor.rs
index f54fe02d..0a96a386 100644
--- a/src/server/cursor.rs
+++ b/src/server/cursor.rs
@@ -2614,8 +2614,8 @@ unsafe extern "C" fn handle_button(listener: *mut ffi::wl_listener, data: *mut s
_ => "top-left",
};
let key_path = format!("layout.status_bar.{}", name);
- cce_ui::config::write_config_value(
- &cce_ui::config::get_config_path().to_string_lossy(),
+ cce_core::config::write_config_value(
+ &cce_core::config::get_config_path().to_string_lossy(),
&key_path,
&format!("\"{}\"", edge_str),
"layout"
@@ -4049,11 +4049,11 @@ unsafe extern "C" fn handle_swipe_update(listener: *mut ffi::wl_listener, data:
// reaches `swipe_repeat_peek` (default half of `swipe_peek`) at the
// repeat threshold, so a swipe that has just switched focus does
// not tug the camera toward the next window as eagerly. With
- // animations off (`cce_ui::motion`) there is no lean at all: the
+ // animations off (`cce_core::motion`) there is no lean at all: the
// camera stays put until the bind fires, then jumps (the step's
// ease is instant then, `advance_camera_animation`). A lean
// already showing when they were turned off goes back the same way.
- let peek_px = if !cce_ui::motion::enabled() {
+ let peek_px = if !cce_core::motion::enabled() {
0.0
} else if cursor.gesture_triggered {
wm.swipe_repeat_peek_px
diff --git a/src/server/layer_shell.rs b/src/server/layer_shell.rs
index 255a27c9..8c13182a 100644
--- a/src/server/layer_shell.rs
+++ b/src/server/layer_shell.rs
@@ -575,7 +575,7 @@ unsafe extern "C" fn handle_layer_surface_map(listener: *mut ffi::wl_listener, _
// furniture — wallpaper, status bar — and map once at login, where a
// fade reads as the desktop failing to draw.
if (*wlr_layer_surface).current.layer == ffi::zwlr_layer_shell_v1_layer_ZWLR_LAYER_SHELL_V1_LAYER_OVERLAY {
- let ms = if cce_ui::motion::enabled() { (*server).wm.layout.fade_in_ms } else { 0 };
+ let ms = if cce_core::motion::enabled() { (*server).wm.layout.fade_in_ms } else { 0 };
if ms > 0 {
(*layer_surface).opacity = 0.0;
}
diff --git a/src/server/screenshot.rs b/src/server/screenshot.rs
index 896ae0e0..f9d43c6a 100644
--- a/src/server/screenshot.rs
+++ b/src/server/screenshot.rs
@@ -435,11 +435,11 @@ fn spawn_encode(rgba: Vec<u8>, w: u32, h: u32, path: PathBuf) {
/// typically an isolated one (a headless shadow session, say) whose toasts
/// would land on someone else's screen. Stay quiet there.
fn notifications_enabled() -> bool {
- let Ok(content) = std::fs::read_to_string(cce_ui::config::get_config_path()) else {
+ let Ok(content) = std::fs::read_to_string(cce_core::config::get_config_path()) else {
log::debug!("screenshot: config unreadable, staying quiet about the capture");
return false;
};
- cce_ui::config::parse_kdl_to_json(&content)
+ cce_core::config::parse_kdl_to_json(&content)
.pointer("/notifications/screenshots")
.and_then(|v| v.as_bool())
.unwrap_or(true)
diff --git a/src/server/window.rs b/src/server/window.rs
index 31419725..e94dcf62 100644
--- a/src/server/window.rs
+++ b/src/server/window.rs
@@ -218,9 +218,9 @@ pub const BORDER_FADE_STEP: f32 = 0.15;
pub const BORDER_FADE_EPSILON: f32 = 0.004;
/// The hover/dim step in force: [`BORDER_FADE_STEP`], or the whole distance
-/// when animations are off (`cce_ui::motion`), which lands in one tick.
+/// when animations are off (`cce_core::motion`), which lands in one tick.
fn border_fade_step() -> f32 {
- if cce_ui::motion::enabled() { BORDER_FADE_STEP } else { 1.0 }
+ if cce_core::motion::enabled() { BORDER_FADE_STEP } else { 1.0 }
}
/// Per-tick step of the fullscreen-toggle animation, as a fraction of the
@@ -2235,8 +2235,8 @@ impl Window {
let mut loaded_edge = None;
if name == "light_source" {
let mut light_pos = 2.356194490192345_f32; // Default 135 deg in rad
- if let Ok(content) = std::fs::read_to_string(cce_ui::config::get_config_path()) {
- let val = cce_ui::config::parse_kdl_to_json(&content);
+ if let Ok(content) = std::fs::read_to_string(cce_core::config::get_config_path()) {
+ let val = cce_core::config::parse_kdl_to_json(&content);
if let Some(wm_obj) = val.get("window_manager") {
if let Some(pos_val) = wm_obj.get("light_source_position") {
if let Some(f) = pos_val.as_f64() {
@@ -2278,8 +2278,8 @@ impl Window {
StatusEdge::BottomRight
};
loaded_edge = Some(edge);
- } else if let Ok(content) = std::fs::read_to_string(cce_ui::config::get_config_path()) {
- let val = cce_ui::config::parse_kdl_to_json(&content);
+ } else if let Ok(content) = std::fs::read_to_string(cce_core::config::get_config_path()) {
+ let val = cce_core::config::parse_kdl_to_json(&content);
if let Some(layout_obj) = val.get("layout") {
if let Some(status_bar_obj) = layout_obj.get("status_bar") {
if let Some(edge_val) = status_bar_obj.get(name) {
@@ -2424,9 +2424,9 @@ impl Window {
// that failed to map never starts one. `start_map_fade` snaps rather
// than ramps when fading is off or this surface opts out (status
// segments, wallpaper), so there is no second branch here.
- // Animations off (`cce_ui::motion`) is a zero-length fade, the
+ // Animations off (`cce_core::motion`) is a zero-length fade, the
// same as `surface { fade in_ms=0 }`.
- let fade_ms = if cce_ui::motion::enabled() { (*self.server).wm.layout.fade_in_ms } else { 0 };
+ let fade_ms = if cce_core::motion::enabled() { (*self.server).wm.layout.fade_in_ms } else { 0 };
if self.wants_map_fade() && fade_ms > 0 {
self.map_fade = 0.0;
}
@@ -4254,7 +4254,7 @@ impl Window {
ffi::wlr_scene_node_set_enabled(node, false);
return;
}
- let spec = cce_ui::scene::paint::DropletSpec::parse(
+ let spec = cce_core::droplet::DropletSpec::parse(
layout.status_droplet.as_deref().unwrap_or(""),
);
if spec.refr <= 0.0 && spec.ghost <= 0.0 {
@@ -4293,7 +4293,7 @@ impl Window {
}
match (*self.server).wm.layout.status_droplet.as_deref() {
Some(raw) => {
- let spec = cce_ui::scene::paint::DropletSpec::parse(raw);
+ let spec = cce_core::droplet::DropletSpec::parse(raw);
spec.refr > 0.0 || spec.ghost > 0.0
}
None => false,
@@ -4518,7 +4518,7 @@ impl Window {
/// pass in this same cycle.
unsafe fn start_fs_anim(&mut self) {
// Animations off: the window is simply drawn at its new rect.
- if !cce_ui::motion::enabled() {
+ if !cce_core::motion::enabled() {
self.fs_anim = None;
return;
}
@@ -4555,7 +4555,7 @@ impl Window {
return false;
};
// Switched off mid-flight: land now, with the settling frame.
- if !cce_ui::motion::enabled() {
+ if !cce_core::motion::enabled() {
self.fs_anim = None;
return true;
}
diff --git a/src/server/window_manager.rs b/src/server/window_manager.rs
index 8db0639c..21d495ce 100644
--- a/src/server/window_manager.rs
+++ b/src/server/window_manager.rs
@@ -2406,10 +2406,10 @@ impl WindowManager {
/// nothing is left to animate.
unsafe fn advance_camera_animation(&mut self, dt: f64, frame_target_ns: u64) -> bool {
let mut done = true;
- // Animations off (`cce_ui::motion`): every ease below covers its
+ // Animations off (`cce_core::motion`): every ease below covers its
// whole distance in this step, the ramp lands, and a flick does not
// coast — the camera still goes where it was sent, just at once.
- let animate = cce_ui::motion::enabled();
+ let animate = cce_core::motion::enabled();
// Frame-rate independent exponential approach: the same fraction of
// the remaining distance per unit time whatever the frame pacing.
let factor = if animate { 1.0 - (-self.scroll_ease_rate() * dt).exp() } else { 1.0 };
@@ -5336,7 +5336,7 @@ impl WindowManager {
"fade-out" => {
// Animations off answers 0 like a disabled fade: the client
// exits at once and nothing ramps.
- let ms = if cce_ui::motion::enabled() { self.layout.fade_out_ms } else { 0 };
+ let ms = if cce_core::motion::enabled() { self.layout.fade_out_ms } else { 0 };
let pid = self.pending_ipc_peer_pid;
if pid <= 0 {
return "0\n".to_string();