Compare commits

..

1 Commits

Author SHA1 Message Date
a5d31cccdd 添加快捷键聚焦 mpv 2026-03-27 04:18:35 +08:00
99 changed files with 160 additions and 12267 deletions

View File

@@ -5,34 +5,12 @@ BasedOnStyle: Google
IndentWidth: 2
ConstructorInitializerIndentWidth: 2
ContinuationIndentWidth: 2
AlignConsecutiveAssignments:
Enabled: true
AcrossEmptyLines: false
AcrossComments: false
AlignCompound: true
PadOperators: true
AlignConsecutiveBitFields:
Enabled: true
AlignConsecutiveMacros:
Enabled: true
AlignConsecutiveShortCaseStatements:
Enabled: true
AllowShortIfStatementsOnASingleLine: AllIfsAndElse
IndentCaseLabels: false
# 多行函数每个参数单独一行
AlignAfterOpenBracket: BlockIndent
AllowAllArgumentsOnNextLine: false
AllowAllParametersOfDeclarationOnNextLine: false
BinPackArguments: false
BinPackParameters: false
# 指针样式(右对齐)
---
Language: C
DerivePointerAlignment: false
PointerAlignment: Right
---
Language: Cpp
DerivePointerAlignment: false
PointerAlignment: Right
---

View File

@@ -1,30 +0,0 @@
# -*- mode: yaml; -*-
Checks: >-
-*,
clang-analyzer-*,
-clang-analyzer-security.insecureAPI.DeprecatedOrUnsafeBufferHandling,
cert-err34-c,
bugprone-assert-side-effect,
bugprone-assignment-in-if-condition,
bugprone-branch-clone,
bugprone-infinite-loop,
bugprone-macro-parentheses,
bugprone-macro-repeated-side-effects,
bugprone-misplaced-operator-in-strlen-in-alloc,
bugprone-misplaced-pointer-arithmetic-in-alloc,
bugprone-not-null-terminated-result,
bugprone-posix-return,
bugprone-sizeof-expression,
bugprone-suspicious-memory-comparison,
bugprone-suspicious-memset-usage,
bugprone-suspicious-missing-comma,
bugprone-suspicious-semicolon,
bugprone-swapped-arguments,
bugprone-undefined-memory-manipulation,
bugprone-unsafe-functions,
bugprone-unused-return-value
WarningsAsErrors: ''
HeaderFilterRegex: '^(src|include|tests)/'
FormatStyle: file
SystemHeaders: false

View File

@@ -1,51 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
repo_root="$(git rev-parse --show-toplevel)"
cd "$repo_root"
if ! command -v clang-format >/dev/null 2>&1; then
echo "pre-commit: clang-format not found in PATH" >&2
exit 1
fi
if ! git clang-format -h >/dev/null 2>&1; then
echo "pre-commit: git clang-format is not available" >&2
exit 1
fi
mapfile -d '' staged_files < <(
git diff --cached --name-only --diff-filter=ACMR -z -- \
'*.c' '*.h' '*.cc' '*.cpp' '*.cxx' '*.hh' '*.hpp' '*.hxx'
)
if ((${#staged_files[@]} == 0)); then
exit 0
fi
if git rev-parse --verify HEAD >/dev/null 2>&1; then
base_commit=HEAD
else
empty_tree="$(git hash-object -t tree /dev/null)"
base_commit="$(
printf 'pre-commit empty base\n' | \
GIT_AUTHOR_NAME=pre-commit \
GIT_AUTHOR_EMAIL=pre-commit@example.invalid \
GIT_COMMITTER_NAME=pre-commit \
GIT_COMMITTER_EMAIL=pre-commit@example.invalid \
git commit-tree "$empty_tree"
)"
fi
set +e
git clang-format --staged --binary clang-format --quiet "$base_commit" -- \
"${staged_files[@]}"
status=$?
set -e
if [[ $status -ne 0 && $status -ne 1 ]]; then
exit "$status"
fi
git add -- "${staged_files[@]}"

1
.gitignore vendored
View File

@@ -2,4 +2,3 @@
/build/
/zdwm
/.cache/
/Testing/

View File

@@ -7,7 +7,6 @@ set(CMAKE_C_EXTENSIONS ON)
set(APP_NAME "zdwm")
set(SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/src")
set(INCLUDE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/include")
set(BUILD_DIR "${CMAKE_CURRENT_BINARY_DIR}")
add_compile_definitions(LOG_FILE="$ENV{HOME}/${APP_NAME}-log.txt")
@@ -25,11 +24,9 @@ project(${APP_NAME}
LANGUAGES C
)
option(BUILD_TESTING "Build tests" OFF)
add_executable(${APP_NAME}
${SOURCE_DIR}/wm.c
${SOURCE_DIR}/base/log.c
${SOURCE_DIR}/utils.c
${SOURCE_DIR}/buffer.c
${SOURCE_DIR}/backtrace.c
${SOURCE_DIR}/monitor.c
@@ -83,7 +80,6 @@ pkg_check_modules(deps REQUIRED
target_include_directories(${APP_NAME} SYSTEM
PRIVATE ${deps_INCLUDE_DIRS}
PRIVATE ${INCLUDE_DIR}
)
target_include_directories(${APP_NAME}
PRIVATE ${SOURCE_DIR}
@@ -95,6 +91,7 @@ target_link_libraries(${APP_NAME}
PRIVATE ${deps_LIBRARIES}
)
## 配置项
# Check for backtrace_symbols()
include(CheckSymbolExists)
@@ -141,8 +138,3 @@ add_custom_target(generate_atoms
DEPENDS ${BUILD_DIR}/atoms-extern.h ${BUILD_DIR}/atoms-intern.h
)
add_dependencies(${APP_NAME} generate_atoms)
if(BUILD_TESTING)
enable_testing()
add_subdirectory(tests)
endif()

View File

@@ -39,13 +39,4 @@ build: prepare
@cmake --build $(BUILD_DIR)
@cp $(BUILD_DIR)/$(TARGET_NAME) $(TARGET)
test:
cmake -S $(SRC_DIR) -B $(BUILD_DIR) -DBUILD_TESTING=ON
cmake --build $(BUILD_DIR)
ctest --test-dir $(BUILD_DIR)
install-hooks:
chmod +x $(MAKEFILE_DIR).githooks/pre-commit
git -C $(MAKEFILE_DIR) config core.hooksPath $(MAKEFILE_DIR).githooks
.PHONY: clean run wm prepare build install uninstall reinstall test install-hooks
.PHONY: clean run wm prepare build install uninstall reinstall

File diff suppressed because it is too large Load Diff

View File

@@ -1,650 +0,0 @@
# ZDWM 最小核心 + 外围扩展设计
* 设计原则
1. *核心只做一件事*:管理窗口、显示器、工作区
2. *所有扩展都通过插件*:状态栏、配置、快捷键、布局算法
3. *核心提供稳定的钩子*:插件可以在关键点介入
4. *零运行时开销*:扩展不增加核心复杂度
* 最小核心定义
#+BEGIN_SRC c
// src/core/zdwm.h
#include <stdbool.h>
#include <stdint.h>
// ========== 基础类型 ==========
typedef struct {
int x, y;
unsigned int width, height;
} rect_t;
typedef struct {
const char *name;
const char *instance;
} wm_class_t;
// ========== 窗口 ==========
typedef uint64_t wm_window_id_t;
typedef struct {
wm_window_id_t id;
rect_t geometry;
wm_class_t class;
bool mapped;
bool floating;
bool fullscreen;
bool focused;
// ... 其他基本状态
} wm_window_t;
// ========== 工作区 ==========
typedef uint32_t wm_workspace_id_t;
typedef struct {
wm_workspace_id_t id;
char *name;
wm_window_id_t *windows;
size_t window_count;
wm_window_id_t focused_window;
} wm_workspace_t;
// ========== 显示器 ==========
typedef uint32_t wm_output_id_t;
typedef struct {
wm_output_id_t id;
rect_t geometry;
rect_t workarea;
wm_workspace_id_t current_workspace;
} wm_output_t;
// ========== 核心状态 ==========
typedef struct {
wm_window_t *windows;
size_t window_count;
wm_workspace_t *workspaces;
size_t workspace_count;
wm_workspace_id_t current_workspace;
wm_output_t *outputs;
size_t output_count;
} wm_state_t;
// ========== 核心 API ==========
// 初始化和清理
bool wm_init(void);
void wm_shutdown(void);
// 主循环
void wm_run(void);
// 查询接口
wm_window_t* wm_find_window(wm_window_id_t id);
wm_workspace_t* wm_find_workspace(wm_workspace_id_t id);
wm_output_t* wm_find_output(wm_output_id_t id);
// 窗口操作
void wm_manage_window(wm_window_id_t id);
void wm_unmanage_window(wm_window_id_t id);
void wm_focus_window(wm_window_id_t id);
void wm_set_window_floating(wm_window_id_t id, bool floating);
// 工作区操作
void wm_switch_workspace(wm_workspace_id_t id);
void wm_send_window_to_workspace(wm_window_id_t window, wm_workspace_id_t workspace);
// 显示器操作
void wm_switch_output(wm_output_id_t id);
#+END_SRC
* 扩展接口设计
**1. 事件钩子系统**
#+BEGIN_SRC c
// src/core/hooks.h
// 钩子类型(关键事件点)
typedef enum wm_hook_type_t {
HOOK_WINDOW_MANAGE, // 窗口被管理时
HOOK_WINDOW_UNMANAGE, // 窗口取消管理时
HOOK_WINDOW_FOCUS, // 窗口焦点变化时
HOOK_WORKSPACE_SWITCH, // 工作区切换时
HOOK_LAYOUT_CALCULATE, // 布局计算时
HOOK_CONFIG_RELOAD, // 配置重载时
HOOK_KEY_PRESS, // 键盘按下时
HOOK_COUNT
} wm_hook_type_t;
// 钩子回调函数类型
typedef void (*wm_hook_window_manage_t)(wm_window_t *window, void *user_data);
typedef void (*wm_hook_window_focus_t)(wm_window_t *old_focus, wm_window_t *new_focus, void *user_data);
typedef void (*wm_hook_layout_calculate_t)(wm_workspace_t *workspace, rect_t *geometries, size_t count, void *user_data);
typedef bool (*wm_hook_key_press_t)(uint32_t keycode, uint32_t modifiers, void *user_data);
// 钩子注册
typedef struct {
wm_hook_type_t type;
void *callback;
void *user_data;
int priority; // 优先级:数字越小越先执行
} wm_hook_t;
// 核心 API注册钩子
void wm_hook_register(wm_hook_type_t type, void *callback, void *user_data, int priority);
void wm_hook_unregister(wm_hook_type_t type, void *callback);
// 核心 API触发钩子
void wm_hook_trigger_window_manage(wm_window_t *window);
void wm_hook_trigger_window_focus(wm_window_t *old_focus, wm_window_t *new_focus);
void wm_hook_trigger_layout_calculate(wm_workspace_t *workspace, rect_t *geometries, size_t count);
bool wm_hook_trigger_key_press(uint32_t keycode, uint32_t modifiers);
#+END_SRC
**2. 插件系统**
#+BEGIN_SRC c
// src/core/plugin.h
// 插件接口
typedef struct {
const char *name;
const char *version;
// 必须实现
bool (*init)(void);
void (*shutdown)(void);
// 可选实现
void (*on_event)(wm_hook_type_t event, void *event_data);
} wm_plugin_t;
// 核心 API插件加载
bool wm_plugin_load(wm_plugin_t *plugin);
bool wm_plugin_unload(const char *name);
#+END_SRC
* 扩展编写指南
**扩展1布局算法插件**
#+BEGIN_SRC c
// plugins/layout_tile.c
#include "plugins/layout.h"
#include "core/hooks.h"
// 布局算法实现
static void tile_calculate_layout(wm_workspace_t *workspace,
rect_t *geometries,
size_t count,
void *user_data) {
// 布局参数(可以通过配置获取)
int gap = 10;
int master_count = 1;
for (size_t i = 0; i < count; i++) {
if (i < master_count) {
// 主窗口:左侧,占 60%
geometries[i].x = workspace->outputs[0].x;
geometries[i].y = workspace->outputs[0].y;
geometries[i].width = workspace->outputs[0].width * 0.6 - gap;
geometries[i].height = workspace->outputs[0].height;
} else {
// 堆叠窗口:右侧,占 40%
geometries[i].x = workspace->outputs[0].x + workspace->outputs[0].width * 0.6 + gap;
geometries[i].y = workspace->outputs[0].y + (i - master_count) * 20;
geometries[i].width = workspace->outputs[0].width * 0.4 - gap;
geometries[i].height = 20;
}
}
}
// 插件初始化
static bool layout_tile_init(void) {
// 注册布局钩子
wm_hook_register(HOOK_LAYOUT_CALCULATE, tile_calculate_layout, NULL, 0);
return true;
}
static void layout_tile_shutdown(void) {
// 自动取消注册
}
// 插件定义
wm_plugin_t plugin_tile = {
.name = "tile",
.version = "1.0",
.init = layout_tile_init,
.shutdown = layout_tile_shutdown
};
#+END_SRC
**扩展2状态栏插件**
#+BEGIN_SRC c
// plugins/statusbar.c
#include "plugins/statusbar.h"
#include "core/hooks.h"
static cairo_surface_t *g_bar_surface = NULL;
// 在工作区切换时更新状态栏
static void on_workspace_switch(void *event_data) {
wm_workspace_t *workspace = event_data;
// 绘制状态栏
draw_statusbar(workspace);
}
// 在布局计算后绘制到状态栏
static void on_layout_calculate(wm_workspace_t *workspace,
rect_t *geometries,
size_t count,
void *user_data) {
// 可以在这里显示布局信息
draw_layout_info(workspace);
}
// 绘制状态栏
static void draw_statusbar(wm_workspace_t *workspace) {
// 使用 cairo 绘制
// ...
}
static bool statusbar_init(void) {
// 注册钩子
wm_hook_register(HOOK_WORKSPACE_SWITCH, on_workspace_switch, NULL, 0);
wm_hook_register(HOOK_LAYOUT_CALCULATE, on_layout_calculate, NULL, 0);
return true;
}
static void statusbar_shutdown(void) {
if (g_bar_surface) {
cairo_surface_destroy(g_bar_surface);
}
}
wm_plugin_t plugin_statusbar = {
.name = "statusbar",
.version = "1.0",
.init = statusbar_init,
.shutdown = statusbar_shutdown
};
#+END_SRC
**扩展3快捷键绑定插件**
#+BEGIN_SRC c
// plugins/keybindings.c
#include "plugins/keybindings.h"
#include "core/hooks.h"
// 快捷键配置
typedef struct {
uint32_t keycode;
uint32_t modifiers;
void (*action)(void);
} keybinding_t;
static keybinding_t g_bindings[] = {
{ .keycode = XK_Return, .modifiers = Mod4Mask, .action = action_launch_terminal },
{ .keycode = XK_q, .modifiers = Mod4Mask | ShiftMask, .action = action_close_window },
{ .keycode = XK_space, .modifiers = Mod4Mask, .action = action_next_layout },
};
static bool handle_key_press(uint32_t keycode, uint32_t modifiers, void *user_data) {
for (size_t i = 0; i < sizeof(g_bindings)/sizeof(g_bindings[0]); i++) {
if (g_bindings[i].keycode == keycode &&
g_bindings[i].modifiers == modifiers) {
g_bindings[i].action();
return true; // 事件被消费
}
}
return false; // 继续传递
}
static bool keybindings_init(void) {
wm_hook_register(HOOK_KEY_PRESS, handle_key_press, NULL, 0);
return true;
}
static void keybindings_shutdown(void) {
// 自动取消注册
}
wm_plugin_t plugin_keybindings = {
.name = "keybindings",
.version = "1.0",
.init = keybindings_init,
.shutdown = keybindings_shutdown
};
#+END_SRC
**扩展4窗口规则插件**
#+BEGIN_SRC c
// plugins/rules.c
#include "plugins/rules.h"
#include "core/hooks.h"
typedef struct {
char *class_name;
bool floating;
bool sticky;
wm_workspace_id_t target_workspace;
} rule_t;
static rule_t g_rules[] = {
{ .class_name = "floating_window", .floating = true },
{ .class_name = "sticky_terminal", .sticky = true },
};
static void on_window_manage(wm_window_t *window, void *user_data) {
// 应用规则
for (size_t i = 0; i < sizeof(g_rules)/sizeof(g_rules[0]); i++) {
if (strcmp(window->class.name, g_rules[i].class_name) == 0) {
if (g_rules[i].floating) {
wm_set_window_floating(window->id, true);
}
// ... 应用其他规则
}
}
}
static bool rules_init(void) {
wm_hook_register(HOOK_WINDOW_MANAGE, on_window_manage, NULL, 0);
return true;
}
static void rules_shutdown(void) {
// 自动取消注册
}
wm_plugin_t plugin_rules = {
.name = "rules",
.version = "1.0",
.init = rules_init,
.shutdown = rules_shutdown
};
#+END_SRC
**扩展5配置系统插件**
#+BEGIN_SRC c
// plugins/config.c
#include "plugins/config.h"
#include "core/hooks.h"
typedef struct {
char *key;
char *value;
} config_item_t;
static config_item_t g_config[] = {
{ .key = "terminal", .value = "st" },
{ .key = "browser", .value = "firefox" },
};
static const char* config_get(const char *key) {
for (size_t i = 0; i < sizeof(g_config)/sizeof(g_config[0]); i++) {
if (strcmp(g_config[i].key, key) == 0) {
return g_config[i].value;
}
}
return NULL;
}
static void on_config_reload(void *event_data) {
// 重新加载配置文件
load_config_file("~/.config/zdwm/config");
}
static bool config_init(void) {
// 加载初始配置
load_config_file("~/.config/zdwm/config");
// 监听配置重载信号
wm_hook_register(HOOK_CONFIG_RELOAD, on_config_reload, NULL, 0);
return true;
}
static void config_shutdown(void) {
// 清理配置资源
}
wm_plugin_t plugin_config = {
.name = "config",
.version = "1.0",
.init = config_init,
.shutdown = config_shutdown
};
#+END_SRC
* 核心实现(简化版)
#+BEGIN_SRC c
// src/core/zdwm.c
// 核心 API触发钩子
void wm_hook_trigger_layout_calculate(wm_workspace_t *workspace, rect_t *geometries, size_t count) {
wm_hook_t *hook = g_hooks[HOOK_LAYOUT_CALCULATE];
while (hook) {
wm_hook_layout_calculate_t callback = (wm_hook_layout_calculate_t)hook->callback;
callback(workspace, geometries, count, hook->user_data);
hook = hook->next;
}
}
bool wm_hook_trigger_key_press(uint32_t keycode, uint32_t modifiers) {
wm_hook_t *hook = g_hooks[HOOK_KEY_PRESS];
while (hook) {
wm_hook_key_press_t callback = (wm_hook_key_press_t)hook->callback;
if (callback(keycode, modifiers, hook->user_data)) {
return true; // 事件被消费
}
hook = hook->next;
}
return false;
}
// 主循环(简化版)
void wm_run(void) {
xcb_generic_event_t *event;
while (g_running) {
// 1. 获取事件
event = xcb_wait_for_event(g_conn);
// 2. 处理事件
wm_handle_event(event);
// 3. 更新布局(如果需要)
if (g_dirty_layout) {
wm_update_layout();
}
// 4. 渲染(如果有状态栏插件)
// (通过钩子让状态栏插件自己绘制)
}
}
// 布局更新
void wm_update_layout(void) {
for (size_t i = 0; i < g_state.workspace_count; i++) {
wm_workspace_t *ws = &g_state.workspaces[i];
// 分配几何数组
rect_t geometries[ws->window_count];
// 触发布局钩子(布局插件会计算几何)
wm_hook_trigger_layout_calculate(ws, geometries, ws->window_count);
// 应用几何到窗口
for (size_t j = 0; j < ws->window_count; j++) {
wm_window_t *win = wm_find_window(ws->windows[j]);
win->geometry = geometries[j];
}
}
}
#+END_SRC
* 扩展交互示例
**场景1状态栏需要显示布局信息**
#+BEGIN_SRC c
// 状态栏插件订阅布局钩子
static void on_layout_calculate(wm_workspace_t *workspace,
rect_t *geometries,
size_t count,
void *user_data) {
// 更新状态栏显示的布局信息
update_layout_indicator(workspace, geometries, count);
}
#+END_SRC
**场景2多个布局算法共存**
#+BEGIN_SRC c
// 插件1tile 布局
wm_plugin_t plugin_tile = { .name = "tile", ... };
// 插件2monocle 布局
wm_plugin_t plugin_monocle = { .name = "monocle", ... };
// 运行时切换
void switch_layout(const char *layout_name) {
// 卸载当前布局插件
wm_plugin_unload("tile");
// 加载新布局插件
wm_plugin_load(find_plugin(layout_name));
}
#+END_SRC
**场景3插件间通信**
#+BEGIN_SRC c
// 场景:网络速度插件需要通知状态栏插件更新
// 方案1通过核心状态
// network 插件更新核心状态中的某个字段
// statusbar 插件在 HOOK_LAYOUT_CALCULATE 中读取该字段
// 方案2直接函数调用如果两个插件在同一进程中
extern void statusbar_update_network_speed(uint64_t speed);
void network_on_speed_update(uint64_t speed) {
statusbar_update_network_speed(speed);
}
// 方案3自定义事件如果有事件总线插件
event_bus_publish("network.speed_changed", &speed);
#+END_SRC
* 扩展最佳实践
**✅ 推荐做法**
1. *插件职责单一*
- 一个插件做一件事
- 例如tile 布局插件只负责布局计算
2. *使用钩子优先级*
- 核心功能(如窗口管理)优先级 = 0
- 扩展功能(如状态栏)优先级 = 10
- 调试功能优先级 = 100
3. *插件间依赖最小化*
- 尽量避免插件间直接调用
- 通过核心状态中转
4. *错误处理*
- 插件 init 失败应返回 false
- 核心会跳过该插件,继续运行
**❌ 避免的做法**
1. *插件直接修改核心结构*
#+BEGIN_SRC c
// ❌ 不好:插件直接修改核心
extern wm_state_t *g_state;
g_state->current_workspace = 1;
// ✅ 好:通过核心 API
wm_switch_workspace(1);
#+END_SRC
2. *插件阻塞执行*
#+BEGIN_SRC c
// ❌ 不好:插件中有阻塞操作
void statusbar_init(void) {
sleep(1); // 阻塞主循环!
}
// ✅ 好:异步操作
void statusbar_init(void) {
pthread_create(&thread, NULL, async_init, NULL);
}
#+END_SRC
3. *插件循环依赖*
- 插件A依赖插件B
- 插件B依赖插件A
- 解决方案:通过核心状态解耦
* 与 mini_core_draft 对比
| 特性 | mini_core_draft | 最小核心 + 插件 |
|--------------+-----------------+-----------------|
| 核心代码行数 | ~3000行 | ~1500行 |
| 抽象层 | 3-4层 | 1层钩子 |
| 扩展方式 | 服务注册 | 插件+钩子 |
| 学习曲线 | 陡峭 | 平缓 |
| 性能 | 中等 | 最好 |
| 可扩展性 | 很高 | 高 |
| 复杂度 | 高 | 低 |
* 核心代码量估算
#+BEGIN_EXAMPLE
核心:
- zdwm.h (核心 API): 200 行
- zdwm.c (核心实现): 500 行
- hooks.c (钩子系统): 200 行
- plugin.c (插件加载): 150 行
------------------------------------
核心总计: ~1050 行
插件示例:
- layout_tile.c: 100 行
- statusbar.c: 300 行
- keybindings.c: 150 行
- rules.c: 200 行
------------------------------------
插件总计: ~750 行
总计: ~1800 行完整功能的窗口管理器
#+END_EXAMPLE
* 总结
*最小核心 + 插件*方案的优势:
1. *核心真正最小* - 只做窗口管理1500行代码
2. *扩展性有保证* - 钩子系统允许任何扩展
3. *性能最优* - 一层间接,零开销抽象
4. *易于理解* - 新手也能看懂核心代码
5. *渐进式开发* - 先核心,后扩展
这个设计比 ~mini_core_draft~ 简单很多,但扩展性并不差。关键是*钩子系统*设计得当,可以实现大部分扩展需求。

View File

@@ -1,407 +0,0 @@
# Zdwm 配置系统设计文档
## 概述
三层配置系统,按优先级从低到高加载:
```
内置默认配置 → XResources → 动态库配置
(后者覆盖前者)
```
---
## 配置层次
### 第一层:内置默认配置
- **位置**: 编译到二进制中
- **作用**: 提供完整的默认配置,确保无外部配置时也能正常运行
- **范围**: 所有配置项
### 第二层XResources 配置
- **位置**: `~/.Xresources``~/.config/zdwm/Xresources`
- **作用**: 覆盖外观配置(颜色、字体、边框等)
- **特点**: 无需重新编译,`xrdb` 后重启生效
- **范围**: 仅外观配置
### 第三层:动态库配置
- **位置**: `~/.config/zdwm/config.c``config.so`
- **作用**: 高级配置(快捷键、规则、自定义布局等)
- **特点**: 最大灵活性,需编译 C 代码
- **范围**: 所有配置项
---
## 配置加载流程
```
1. 加载内置默认配置
2. 尝试加载 XResources
├─ 成功 → 覆盖默认配置中的外观项
└─ 失败 → 跳过,保持默认值
3. 尝试加载动态库
├─ 存在 → 调用 zdwm_config_init(),增量修改配置
└─ 不存在 → 跳过
4. 应用最终配置
```
---
## 数据结构
### 配置优先级标记
```c
typedef struct config_source_t {
bool has_xres; // 是否加载了 XResources
bool has_user_lib; // 是否加载了用户动态库
} config_source_t;
```
### 完整配置结构
```c
typedef struct config_t {
// 外观
char *font_family;
uint32_t font_size;
uint32_t dpi;
uint16_t border_width;
uint16_t bar_y_padding;
uint16_t tag_x_padding;
color_set_t colors;
// 快捷键
const keyboard_t *key_list;
size_t key_count;
// 布局
const layout_t *layout_list;
size_t layout_count;
// 标签
const char *const *tags;
// 规则
const rule_t *rules;
size_t rules_count;
// 自动启动
const char *const *autostart_list;
// 钩子
void (*hook_manage_new)(client_t *client);
void (*hook_manage_unmanage)(client_t *client);
// 元数据
config_source_t source;
} config_t;
```
---
## API 接口(动态库)
### zdwm_api_t
```c
typedef struct zdwm_api_t {
uint32_t version;
// 外观(覆盖 XResources
void (*set_font)(const char *family, uint32_t size);
void (*set_dpi)(uint32_t dpi);
void (*set_color)(const char *name, const char *hex);
void (*set_border_width)(uint16_t width);
void (*set_padding)(uint16_t bar_y, uint16_t tag_x);
// 快捷键(增量式)
void (*add_keybinding)(uint32_t modifiers, xcb_keysym_t keysym,
void (*action)(const void *),
const void *arg);
void (*remove_keybinding)(uint32_t modifiers, xcb_keysym_t keysym);
void (*clear_keybindings)(void);
// 规则
void (*add_rule)(const char *role, const char *class,
int32_t tag_index, bool floating,
bool fullscreen, bool maximize,
bool switch_to_tag);
// 自动启动
void (*add_autostart)(const char *command);
// 布局
void (*add_layout)(const char *symbol, void (*arrange)(tag_t *tag));
// 钩子
void (*hook_manage_new)(void (*hook)(client_t *client));
void (*hook_manage_unmanage)(void (*hook)(client_t *client));
} zdwm_api_t;
```
### 用户配置初始化函数
```c
typedef struct zdwm_user_config_t {
void (*cleanup)(void);
} zdwm_user_config_t;
// 用户在 config.c 中实现此函数(可选)
zdwm_user_config_t *zdwm_config_init(zdwm_api_t *api);
```
---
## XResources 支持的配置项
### 配置项命名规范
```
Zdwm.font.family # 字体族
Zdwm.font.size # 字体大小
Zdwm.border.width # 边框宽度
Zdwm.padding.bar_y # Bar 上下内边距
Zdwm.padding.tag_x # Tag 左右内边距
Zdwm.color.bar # Bar 背景色
Zdwm.color.tag # Tag 背景色
Zdwm.color.tagActive # 活动 Tag 背景色
Zdwm.color.tagText # Tag 文字颜色
Zdwm.color.tagActiveText # 活动 Tag 文字颜色
Zdwm.color.border # 边框颜色
Zdwm.color.borderActive # 活动窗口边框颜色
```
### 数据类型映射
```
XResources 类型 → C 类型
--------------------------→-------------------
字符串 → char *
整数 → uint32_t
颜色 (#RRGGBB) → color_t (uint32_t argb)
```
---
## 实现要点
### 1. 配置加载函数
```c
// src/config.h
config_t *config_load_full(void);
void config_free(config_t *config);
config_t *config_reload(void);
```
### 2. 加载顺序
```c
// src/config.c
config_t *config_load_full(void) {
config_t *config = calloc(1, sizeof(config_t));
// 第一步:默认配置
config_apply_defaults(config);
// 第二步XResources覆盖外观
config_load_xres(config);
// 第三步:动态库(覆盖所有)
config_load_user_lib(config);
return config;
}
```
### 3. XResources 解析
```c
// src/config_xres.c
bool config_load_xres(config_t *config) {
if (!xres_init_xrm_db()) return false;
// 覆盖外观配置
xres_get_string("Zdwm.font.family", &config->font_family, default_font);
xres_get_uint32("Zdwm.font.size", &config->font_size);
// ...
xres_clean();
return true;
}
```
### 4. 动态库加载
```c
// src/config_lib.c
bool config_load_user_lib(config_t *config) {
const char *path = "~/.config/zdwm/config.so";
void *handle = dlopen(path, RTLD_LAZY);
if (!handle) return false;
zdwm_user_config_t *(*init)(zdwm_api_t *) = dlsym(handle, "zdwm_config_init");
if (!init) {
dlclose(handle);
return false; // 没有配置函数是正常的
}
zdwm_api_t api = create_api(config);
init(&api);
return true;
}
```
---
## 配置热重载
### XResources 热重载
编辑 XResources 后的操作流程:
```bash
# 1. 编辑配置文件
vim ~/.Xresources
# 2. 加载到 X Server
xrdb -merge ~/.Xresources
# 3. 重载 zdwm 配置
killall -HUP zdwm
```
### 实现要点
```c
// src/wm.c
// 重新应用配置
static void wm_reapply_config(void) {
// 重新初始化字体
text_reinit_pango_layout(wm.font_family, wm.font_size, wm.dpi);
// 重新计算 bar 高度
wm.bar_height = text_get_height() + 2 * wm.padding.bar_y;
// 刷新所有 monitor 的 bar
for (monitor_t *m = wm.monitor_list; m; m = m->next) {
monitor_draw_bar(m);
}
xcb_flush(wm.xcb_conn);
}
// SIGHUP 信号处理器
static void reload_config_handler(int sig) {
log("Received SIGHUP, reloading configuration...");
// 清理旧的 XResources 数据库
xres_clean();
// 重新加载 XResources
wm_get_xres_config();
// 重新应用配置
wm_reapply_config();
log("Configuration reloaded successfully");
}
// 在 wm_setup_signal() 中注册
void wm_setup_signal(void) {
// ... 现有信号处理 ...
signal(SIGHUP, reload_config_handler);
signal(SIGUSR1, reload_config_handler); // 备用信号
}
```
### xres 模块需要添加
```c
// src/xres.h
void xres_reload(void);
// src/xres.c
void xres_reload(void) {
if (wm.xrm) {
xcb_xrm_database_free(wm.xrm);
wm.xrm = nullptr;
}
xres_init_xrm_db();
}
```
### 完整重载流程
```
用户编辑 XResources
xrdb -merge ~/.Xresources (加载到 X Server)
killall -HUP zdwm (发送重载信号)
zdwm 收到 SIGHUP
xres_clean() (清理旧数据库)
wm_get_xres_config() (重新读取 XResources)
wm_reapply_config() (应用新配置)
monitor_draw_bar() (刷新显示)
```
---
## 文件结构
```
src/
├── config.h # 配置系统头文件
├── config.c # 配置加载主逻辑
├── config_default.c # 内置默认配置
├── config_xres.c # XResources 解析
├── config_lib.c # 动态库加载
└── config_api.c # API 实现(供动态库调用)
docs/
└── config_system.md # 本文档
```
---
## 依赖关系
```
config.c
├── config_default.c (内置默认值)
├── config_xres.c (XResources 解析)
│ └── xres.c (已有)
└── config_lib.c (动态库加载)
└── dlopen()
```
---
## 实现优先级
| 优先级 | 模块 | 说明 |
|--------|------------------|--------------------------------|
| P0 | config_default.c | 定义默认配置值 |
| P0 | config.c | 主加载逻辑 |
| P1 | config_xres.c | XResources 支持(已有基础) |
| P1 | 热重载 | SIGHUP 支持(配合 XResources |
| P1 | config_lib.c | 动态库加载 |
| P2 | config_api.c | API 实现 |
**P0**: 核心功能,必须实现
**P1**: 重要功能,提升用户体验
**P2**: 增强功能,可后续添加

View File

@@ -1,412 +0,0 @@
* ZDWM 配置系统设计
** 概述
配置系统现在只保留两类正式输入:
1. 编译进二进制的默认配置
2. 用户动态库配置(=~/.config/zdwm/config.c= 编译得到 =config.so=
不再支持 XResources也不再保留 =Xft.dpi= 作为兼容输入。字体、DPI、颜色、边框、布局、规则、快捷键和服务设置都只来自默认配置和动态库配置。
配置系统的职责也收紧为:
1. 生成 core-native 配置快照。
2. 为 runtime adapter、service 层和输入绑定层提供稳定输入。
3. 不直接操作 =wm_state_t=
4. 不再暴露 =client_t==tag_t==monitor_t= 之类的旧对象。
** 设计目标
1. 配置层继续放在核心之外。
2. 配置结果必须能无损对接新的 =runtime/policy/layout/service= 边界。
3. 配置重载应以“重建快照并显式应用”为单位,而不是任意时刻写全局内存。
4. 彻底移除 XResources 路径,避免维护两套并行语义。
** 配置来源与优先级
最终有效配置按下面顺序构建:
#+BEGIN_EXAMPLE
默认配置
动态库配置 config.so
冻结为 zdwm_config_t
#+END_EXAMPLE
动态库配置优先级高于默认配置。
** 非目标
下面这些能力不再属于配置系统设计范围:
1. =Zdwm.*= 风格的 XResources 覆盖。
2. =Xft.dpi= 的额外兼容读取。
3. =hook_manage_new(client_t *)==hook_manage_unmanage(client_t *)= 这类直接对象钩子。
4. =arrange(tag_t *)= 这类依赖旧 tag 模型的布局接口。
5. 从配置层直接访问或修改 =wm= 全局对象。
** 配置输出模型
配置系统输出一份冻结后的配置快照,而不是一组可随时调用的副作用回调。
*** 外观配置
#+BEGIN_SRC c
typedef struct zdwm_appearance_config_t {
char *font_family;
uint32_t font_size;
uint32_t dpi;
uint16_t border_width;
uint16_t bar_y_padding;
uint16_t tag_x_padding;
color_set_t colors;
} zdwm_appearance_config_t;
#+END_SRC
这部分不进入最小核心状态,而是供 render/bar/text 这类核心外服务使用。
*** workspace 配置
=wm_workspace_t= 是运行时状态结构,里面包含 =focused_window_id= 这类不属于静态配置的数据。因此配置层应单独定义 workspace 配置描述:
#+BEGIN_SRC c
typedef struct zdwm_workspace_config_t {
wm_workspace_id_t id;
wm_layout_id_t initial_layout_id;
const char *name;
const char *symbol_text;
const char *symbol_icon_path;
} zdwm_workspace_config_t;
#+END_SRC
runtime adapter 会把它拆成:
1. =wm_workspace_t= 的初始数组
2. =wm_workspace_desc_t= 描述表
*** 管理规则
#+BEGIN_SRC c
typedef struct zdwm_manage_rule_t {
const char *title;
const char *app_id;
const char *class_name;
const char *instance_name;
wm_workspace_id_t workspace_id;
wm_manage_window_init_t initial_state;
bool switch_to_workspace;
} zdwm_manage_rule_t;
#+END_SRC
规则层的职责仅限于:
1. 匹配窗口元数据。
2. 决定 =MANAGE_WINDOW= 的 =workspace_id= 。
3. 决定 =MANAGE_WINDOW= 的 =initial_state= 。
4. 可选地决定是否切到目标 workspace。
规则层不直接管理窗口对象,也不直接写 =wm_state_t= 。
*** 快捷键绑定
快捷键也需要从旧的“直接 action 回调”收敛到新边界。
#+BEGIN_SRC c
typedef enum zdwm_binding_target_t {
ZDWM_BINDING_CORE_COMMAND,
ZDWM_BINDING_SERVICE_ACTION,
} zdwm_binding_target_t;
typedef struct zdwm_service_action_t {
const char *service_name;
const char *action_name;
const char *string_arg;
int32_t int_arg;
} zdwm_service_action_t;
typedef struct zdwm_keybinding_t {
uint32_t modifiers;
xkb_keysym_t keysym;
zdwm_binding_target_t target;
union {
wm_command_t command;
zdwm_service_action_t service_action;
} as;
} zdwm_keybinding_t;
#+END_SRC
=core command= 用于工作区切换、布局切换、浮动切换、焦点切换等窗口管理行为。
=service action= 用于启动外部程序、刷新状态服务、触发核心外功能。
*** 服务设置
#+BEGIN_SRC c
typedef struct zdwm_service_setting_t {
const char *service_name;
const char *key;
const char *value;
} zdwm_service_setting_t;
#+END_SRC
服务配置由配置层生成,但仍由 service 层解释和持有。
*** 总配置快照
#+BEGIN_SRC c
typedef struct zdwm_config_source_t {
bool has_user_lib;
} zdwm_config_source_t;
typedef struct zdwm_runtime_config_t {
wm_policy_config_t policy;
zdwm_workspace_config_t *workspaces;
size_t workspace_count;
wm_layout_slot_t *layouts;
size_t layout_count;
} zdwm_runtime_config_t;
typedef struct zdwm_config_t {
zdwm_runtime_config_t runtime;
zdwm_appearance_config_t appearance;
zdwm_manage_rule_t *rules;
size_t rule_count;
zdwm_keybinding_t *keybindings;
size_t keybinding_count;
const char *const *autostart_list;
zdwm_service_setting_t *service_settings;
size_t service_setting_count;
zdwm_config_source_t source;
} zdwm_config_t;
#+END_SRC
注意这里故意不把 =outputs= 和启动时已有窗口塞进配置结果里。它们属于 backend 发现结果,不属于静态配置。
** 与最小核心的集成方式
配置层与 runtime 的拼装分为两步:
*** 第一步:构建配置快照
#+BEGIN_EXAMPLE
默认配置
config.so 覆盖与增量注册
zdwm_config_t
#+END_EXAMPLE
*** 第二步:由 adapter 组装 runtime bootstrap
#+BEGIN_EXAMPLE
zdwm_config_t
+
backend 发现的 outputs
+
backend 扫描得到的初始 MANAGE_WINDOW 命令
wm_runtime_bootstrap_t
#+END_EXAMPLE
组装规则应为:
1. =policy= 来自 =config.runtime.policy= 。
2. =workspaces= 来自 =config.runtime.workspaces[]= 的静态定义。
3. =workspace_descs= 来自 =config.runtime.workspaces[]= 中的展示字段。
4. =outputs= 来自 backend。
5. =initial_commands= 来自 backend 对已有窗口的扫描和规则匹配。
这样配置层不需要知道当前有哪些物理输出,也不需要自己扫描窗口。
** 动态库 API
动态库 API 由“builder 风格”的配置写入接口组成:
#+BEGIN_SRC c
typedef struct zdwm_api_t {
uint32_t version;
// appearance
void (*set_font)(const char *family, uint32_t size);
void (*set_dpi)(uint32_t dpi);
void (*set_color)(const char *name, const char *hex);
void (*set_border_width)(uint16_t width);
void (*set_padding)(uint16_t bar_y, uint16_t tag_x);
// runtime config
void (*set_policy)(wm_policy_config_t policy);
void (*define_workspace)(zdwm_workspace_config_t workspace);
void (*register_layout)(wm_layout_slot_t layout);
// rules
void (*add_rule)(zdwm_manage_rule_t rule);
// bindings
void (*add_keybinding)(zdwm_keybinding_t binding);
void (*clear_keybindings)(void);
// autostart and services
void (*add_autostart)(const char *command);
void (*set_service_option)(const char *service_name,
const char *key,
const char *value);
} zdwm_api_t;
#+END_SRC
用户动态库入口保持简单:
#+BEGIN_SRC c
typedef struct zdwm_user_config_t {
void (*cleanup)(void);
} zdwm_user_config_t;
zdwm_user_config_t *zdwm_config_init(zdwm_api_t *api);
#+END_SRC
=cleanup()= 只负责动态库自身资源,不负责 runtime 状态回收。
** 加载流程
#+BEGIN_SRC c
zdwm_config_t *config_load(void) {
zdwm_config_t *config = calloc(1, sizeof(*config));
config_apply_defaults(config);
config_load_user_lib(config);
config_finalize(config);
return config;
}
#+END_SRC
这里的 =config_finalize()= 建议至少做这些事情:
1. 检查 workspace id 是否唯一。
2. 检查 layout id 是否唯一。
3. 检查规则和快捷键数组是否合法。
4. 补齐未设置的默认值。
5. 冻结动态数组,避免运行时再被随意写入。
** 管理规则接入链路
管理规则不直接操作窗口,推荐链路如下:
#+BEGIN_EXAMPLE
backend 发现新窗口
同步窗口 metadata
在 rules 中匹配
生成 WM_COMMAND_MANAGE_WINDOW
- workspace_id
- initial_state
runtime / policy apply_command()
#+END_EXAMPLE
这样规则层只负责“把 metadata 翻译成命令参数”。
** 快捷键接入链路
#+BEGIN_EXAMPLE
backend 输入事件
keybinding table 匹配
若 target == ZDWM_BINDING_CORE_COMMAND:
生成 wm_command_t
若 target == ZDWM_BINDING_SERVICE_ACTION:
发送给 service 层
#+END_EXAMPLE
配置层不再保存 =void (*action)(const void *)= 这类旧式回调。
** 热重载
配置热重载仍然可以保留,但边界必须清晰。
推荐流程:
#+BEGIN_EXAMPLE
收到 SIGHUP
重建 zdwm_config_t
原子替换:
- appearance
- policy
- keybindings
- workspace descriptors
- layout registry
- service settings
通知相关 service 刷新
#+END_EXAMPLE
热重载约束:
1. 不直接改 =wm_state_t=
2. 规则变更只影响后续新管理的窗口,不回溯重算现有窗口。
3. workspace 描述、颜色、字体、DPI、bindings、policy 适合热重载。
4. 大规模布局或 service 拓扑变化,必要时可以走受控重启。
** 文件结构
#+BEGIN_EXAMPLE
src/
├── config.h # 配置系统头文件
├── config.c # 配置加载主逻辑
├── config_defaults.c # 默认配置
├── config_lib.c # 动态库加载
├── config_api.c # builder API
└── config_validate.c # 配置收尾校验(可选)
docs/
└── config_system.org # 本文档
#+END_EXAMPLE
这里不再有 =config_xres.c= 或任何通用 XResources 解析模块。
** 迁移说明
下面这些旧设计应视为废弃:
1. =默认配置 -> XResources -> 动态库配置= 三层覆盖模型。
2. =Xft.dpi= 的兼容读取。
3. =hook_manage_new(client_t *)==hook_manage_unmanage(client_t *)=
4. =add_layout(... arrange(tag_t *tag))=
5. 任何直接依赖 =client_t==tag_t==monitor_t= 的配置接口。
迁移后的对应关系:
1. tag 名称与符号 → =zdwm_workspace_config_t=
2. 布局注册 → =wm_layout_slot_t=
3. 窗口规则 → =zdwm_manage_rule_t=
4. 快捷键动作 → =wm_command_t==zdwm_service_action_t=
5. 外观设置 → =zdwm_appearance_config_t=
** 实施优先级
1. =config_defaults.c=
先保证“无用户配置”也能跑。
2. =config.c + config_lib.c=
打通默认配置和动态库配置装配。
3. =config_api.c=
落地新的 builder API替代旧 action/hook 模型。
4. runtime adapter 对接
=zdwm_config_t= 装配成 =wm_runtime_bootstrap_t= 、规则表、service 设置和 keybinding table。

View File

@@ -1,462 +0,0 @@
* ZDWM 最小核心草案
这个目录放的是一套头文件级别的最小核心 API 草案,目标是先把窗口管理器最核心的对象关系、子系统边界和主执行流程固定下来,再决定具体实现。
* 目标
1. 保持核心技术栈简洁,不引入脚本运行时、序列化协议和多进程插件宿主。
2. 先把最小核心收敛到可以管理窗口、计算布局、处理输入、提交后端副作用。
3. 将渲染、状态栏、配置、插件都放在核心之外,作为后续服务挂接。
* 最小核心子系统
1. =runtime=
负责主循环、模块装配和固定执行顺序。
2. =backend=
负责平台交互。它把平台原始事件转成 =wm_event_t= ,执行 =wm_effect_t=
并在需要时根据固定的键盘/鼠标绑定表安装平台侧被动抓键 / 抓按钮。
像 X11 的 =_NET_ACTIVE_WINDOW= 这类 =ClientMessage= 也应在这里先归一化成
request event而不是直接改 core 状态。
3. =state=
负责保存窗口、工作区、输出和全局堆叠顺序,是唯一真状态。
当前头文件级草案先直接展示结构体字段,便于讨论;
实现阶段再收敛为不透明结构体也不迟。
窗口元数据title/app_id/class/instance直接存储在 =wm_window_t= 中。
窗口 hint / capability=urgent= / =fixed_size= / =skip_taskbar=)也直接存储在
=wm_window_t= 中。
工作区名称存储在 =wm_workspace_t= 中。
4. =policy=
负责把事件路由为命令,并把命令应用到状态。
=WM_EVENT_KEY_PRESS= / =WM_EVENT_POINTER_BUTTON_PRESS= 的路由都使用 bootstrap
固定下来的输入绑定表;像 =WM_EVENT_POINTER_ENTER= 这种不经绑定的输入事实,
则由 policy 配置决定是否转换为 =FOCUS_WINDOW=。
5. =layout=
负责为某个输出上当前显示的工作区中的平铺窗口计算外框几何。
* ID 分配策略
最小核心采用"运行时固定,启动时配置"的 ID 策略:
**半静态实体(启动时分配,运行时固定)**
- =workspace= : 启动时根据配置分配 N 个(如 10 个ID = 数组索引0-N-1
- =output= : 启动时根据扫描到的显示器数量分配ID = 数组索引
- =layout= : 启动时根据注册的布局算法分配ID = 数组索引
这些实体在启动时一次性分配,运行时不再增删。
访问时直接用索引,零开销查找,无需映射表。
```c
// 半静态实体:直接访问(零开销)
wm_workspace_t *ws = &state->workspaces[workspace_id]; // O(1)
wm_output_t *output = &state->outputs[output_id]; // O(1)
```
**动态实体(运行时增删)**
- =window= : 动态数组ID 由后端生成(如 X11 的 xcb_window_t
查找时线性搜索 O(n),对于几十到几百个窗口性能足够。
(后续如成为性能热点,可改用 hashmap 优化到 O(1)
```c
// 半静态实体:直接访问(零开销)
wm_workspace_t *ws = &state->workspaces[workspace_id]; // O(1)
wm_output_t *output = &state->outputs[output_id]; // O(1)
// 动态实体:线性查找(简单且足够)
const wm_window_t *win = wm_state_window_get(state, window_id); // O(n)
```
**设计优势**
- 灵活性workspace 数量由配置决定,不硬编码
- 性能:半静态实体零开销访问,动态实体简单线性查找
- 简单性:无需复杂的映射表,代码更清晰
* 固定集合约束
以下集合一经 bootstrap 确定,运行时不再增删或重建:
1. =outputs=
2. =workspaces=
3. =layout registry=
4. 每个 workspace 的 =available_layouts=
5. =key binding table=
6. =pointer binding table=
运行时允许变化的只是:
1. 某个 output 当前显示的 =current_workspace_id=
2. 某个 workspace 当前选中的 =layout_id=
3. 窗口集合及窗口运行态
* 非核心但常见的服务
这些服务故意不放进最小核心:
1. =render=
状态栏、标题和其他装饰绘制。窗口边框不是独立服务,而是
=WM_EFFECT_CONFIGURE_WINDOW= 的一部分。
2. =status=
网速、内存、CPU、音量、时间等信息采集与聚合。
3. =config=
默认配置和用户覆盖。
4. =plugin=
动态扩展加载。
* 运行时装配与所有权
为了避免再次出现当前全局 =wm= 那样的大对象耦合,最小核心里的所有权再收紧一层:
1. =runtime= 拥有 =state==plan==command_buffer==layout_registry=
其中 workspace/output/layout registry 集合在 init 完成后保持固定。
2. =runtime= 也拥有 =policy config= 、键盘/鼠标绑定表视图、全局边框配置、
交互态和服务注册表。
3. =backend= 只负责平台事件翻译和平台副作用,不拥有控制状态,不直接拼 bar。
4. 窗口元数据title/app_id/class/instance直接存储在 =wm_window_t= 中,与控制状态一同管理。
5. 工作区名称存储在 =wm_workspace_t= 中,与控制状态一同管理。
* 启动与初始化约定
最小核心的初始化流程:
1. =wm_runtime_init()= 接收一份 bootstrap 描述,包含:
- 工作区数量和初始配置(由用户配置决定)
- 布局算法注册(由代码或配置决定)
- 策略配置focus_raises 等)
- 键盘绑定表(规范化的 =keysym + modifiers -> wm_command_t= 模板)
- 鼠标按键绑定表(规范化的 =button + modifiers + target -> wm_command_t= 模板)
- 全局统一边框宽度
- 全局边框颜色集(普通/焦点)
- 初始命令(如启动时扫描到的窗口)
2. 启动时扫描到的已有窗口,不直接塞进 =wm_state_t= ,而是翻译成一批 =MANAGE_WINDOW= 命令进入 runtime。
3. 若 backend 提供 =set_keybindings()= / =set_pointer_bindings()= runtime
在 init 时把 bootstrap 里的绑定表同步给 backend供平台侧建立被动抓键 /
抓按钮。
4. 工作区在启动时根据配置分配 N 个,运行时不再增删。
5. 输出在启动时扫描并分配对应数量的数组,运行时不再增删。
6. layout registry 在启动时注册完成,运行时不再增删。
* 显示器热插拔策略
当检测到显示器配置变化(新增/删除/几何改变)时:
1. 后端检测到 RandR/Output 变化,使 =backend.next_event()= 返回 =WM_BACKEND_NEXT_RESTART_REQUIRED=
2. Runtime 退出主循环,清理资源
3. 由启动脚本systemd/xinitrc自动重启 WM
4. WM 重启后重新扫描显示器配置,从干净状态启动
注意:
- core 不处理 =output changed/removed= 这类增量事件
- core 内不存在运行时增删 output/workspace/layout 集合的命令
**优势**
- ✅ 逻辑简单,无需复杂的运行时重配置
- ✅ 状态一致,避免增量更新的边界情况
- ✅ 实现可靠,从干净状态启动
**代价**
- 窗口会短暂闪烁(~1-2 秒)
- 但相比复杂的热插拔逻辑,这是合理的工程权衡
* Workspace-Output 配置
工作区配置可以在启动阶段根据输出数量生成;一旦 bootstrap 完成,归属关系保持固定:
**配置方式 1手动指定**
```lua
workspaces = {
{ id = 1, name = "web", output_id = 0 }, -- DP-1 的 workspace
{ id = 2, name = "code", output_id = 0 },
{ id = 3, name = "chat", output_id = 1 }, -- DP-2 的 workspace
{ id = 4, name = "media", output_id = 1 },
}
```
**配置方式 2规则式**
```lua
-- 每个 output 自动分配 N 个 workspace
workspace_distribution = {
{ output_id = 0, count = 5 }, -- DP-1: workspace 1-5
{ output_id = 1, count = 5 }, -- DP-2: workspace 6-10
}
```
**配置方式 3启动阶段脚本生成**
```lua
function configure_workspaces(outputs)
local workspaces = {}
local ws_id = 1
for _, output in ipairs(outputs) do
for i = 1, 3 do -- 每个 output 3 个 workspace
table.insert(workspaces, {
id = ws_id,
name = tostring(ws_id),
output_id = output.id
})
ws_id = ws_id + 1
end
end
return workspaces
end
```
**可见性规则**
- 普通非 =sticky= 窗口是否进入可见集,先看它的 =workspace= 是否正被归属 output 选中
- 多输出下,窗口最终是否出现在某个 output 上,还取决于 =frame_rect= 是否与该 output 的几何相交
- =sticky= 窗口只放宽 workspace 可见性,不提供 output 复制语义;它同样按最终矩形与 output 的相交关系决定可见性
- 切换 workspace 只改变归属 output 的 =current_workspace_id= ,不改变 workspace/output 的固定绑定
* 策略配置
前一版草案里有几处“建议作为可选策略”的描述,现在收敛为显式 =wm_policy_config_t=
1. =focus_raises=
焦点切换时是否隐式执行 raise。
2. =pointer_enter_focuses_window=
收到 =WM_EVENT_POINTER_ENTER= 时是否切换焦点focus-follows-mouse
3. =sticky_windows_participate_in_direction_focus=
sticky 窗口是否参与方向焦点搜索。
4. =manage_sets_focus=
新受管窗口是否默认夺取所属 workspace 的焦点。
5. =switch_workspace_restores_last_focus=
切换工作区时是否恢复该 workspace 的最近焦点。
6. =minimize_clears_focus=
最小化当前焦点窗口时是否清空或重选该 workspace 焦点。
* 边框约定
当前最小核心只保留窗口边框,不支持标题栏和其他窗口装饰。
1. =wm_window_t= 不存储 =border_width= 或 =border_color= ;边框样式不是窗口真状态。
2. =wm_runtime_bootstrap_t.border_width= 提供全局统一边框宽度。
3. =wm_runtime_bootstrap_t.border_palette= 提供全局边框颜色集(普通/焦点)。
4. =float_rect= 和 =frame_rect= 都表示“包含边框后的外框矩形”。
5. =runtime= 在提交 =WM_EFFECT_CONFIGURE_WINDOW= 时解析有效边框宽度:
- =WM_GEOMETRY_FULLSCREEN= : 边框宽度为 0
- 其余模式 : 使用全局边框宽度
6. =runtime= 在提交 =WM_EFFECT_CONFIGURE_WINDOW= 时解析有效边框颜色:
- 当前 workspace 的 focused window : =border_palette.focused_rgba=
- 其余窗口 : =border_palette.normal_rgba=
- 若有效边框宽度为 0则 backend 可忽略颜色字段
* 交互态
move/resize 这类拖拽交互不属于 =wm_state_t= 真状态但也不能继续散落在平台分支里。runtime 应维护一个单一交互会话:
1. 空闲
2. 正在移动 floating 窗口
3. 正在调整 floating 窗口大小
交互会话至少记录目标窗口、起始指针位置、起始窗口矩形和起始 output供 =policy.route_event()= 生成连续的 move/resize 命令。
* 核心外服务接入约定
既然 =render/status/bar= 被明确放在核心外,就需要一条稳定的接入边界:
1. =WM_EFFECT_RENDER_OUTPUT= 不是平台副作用,而是 runtime 发给服务层的失效通知。
2. =backend.apply_effect()= 只接收 map/unmap/configure/focus/restack 这类平台 effect。
3. =runtime= 在处理完 plan 后,把 render 和 metadata 相关事件广播给已注册服务。
4. 服务只读 =state/meta/descriptor= 快照,不直接改 =wm_state_t= 。
* 配置集成约定
=config= 仍然在核心外,但它的输出必须改成 core-native 数据,而不是旧的 =client_t/tag_t= 钩子:
1. 配置加载结果应产出 workspace 描述、policy 配置、layout 注册、规则表、
键盘/鼠标绑定到 command 的映射和服务设置。
以及全局统一边框宽度和边框颜色集。
这里的输入绑定应直接收敛为 =wm_key_binding_table_t= /
=wm_pointer_binding_table_t= 。
2. 配置层不得直接持有 =client_t= 、=tag_t= 或直接修改 =wm_state_t= 。
3. 配置重载的边界是“重建 bootstrap 和服务配置,再显式应用到 runtime”而不是任意时刻从外部写内存。
补充约定:
1. 鼠标绑定在最小核心里只区分 =root= / =window= 两类目标。
2. 状态栏、标签栏、任务栏等点击区域属于核心外服务,不进入 core 的 pointer binding 表。
* 对象关系
这套草案里的关系固定为:
1. =window -> workspace=
每个窗口只属于一个工作区。
2. =workspace -> output=
每个工作区固定归属某个输出,运行时不再改变。
3. =output -> current_workspace=
每个输出同一时刻只显示一个当前工作区(从归属的工作区中选择)。
4. =window visible on output=
这是派生结果,不是独立存储的真相。
5. =stack_order[]=
这是全局 z-order 的唯一真相,从下到上排列。
6. =window metadata=
窗口标题、类名等字符串信息直接存储在 =wm_window_t= 中。
核心算法layout/policy不应依赖这些字段仅用于匹配规则和服务层展示。
7. =workspace name=
工作区名称直接存储在 =wm_workspace_t= 中,用于状态栏显示。
核心算法不应依赖此字段。
8. =workspace layouts=
每个 workspace 维护一个可用布局列表(=available_layouts=),在启动时根据配置分配,之后固定不变。
workspace 的 =layout_id= 字段指向当前活动的布局(列表中的一个),运行时允许切换。
使用 =CYCLE_LAYOUT= / =SET_LAYOUT= 命令切换当前活动布局。
9. =window rectangles=
=frame_rect= 是窗口当前最终外框矩形;=float_rect= 是 floating 模式下记忆的外框矩形。
这两个矩形都包含边框。
* 核心不变量
1. 每个窗口必须且只能属于一个工作区。
2. 每个工作区必须且只能固定归属一个输出,运行时不再改变。
3. 每个输出必须且只能显示一个当前工作区(从归属的工作区中选择)。
4. 可见性由窗口归属、工作区归属、输出当前工作区和最小化状态共同推导。
5. =geometry_mode= 只负责几何模式,不负责 z-order其中
=WM_GEOMETRY_MINIMIZED= 统一覆盖 X11 =IconicState= 这类“最小化/图标化”状态。
6. =stack_order[]= 只负责 z-order不负责几何模式。
7. =floating= 是二值状态:窗口要么由 layout 决定基础几何,要么由 =float_rect= 决定基础几何。
8. =sticky => floating= =sticky= 只放宽 workspace 可见性,不改变窗口归属工作区。
9. floating / sticky 窗口都可以跨多个输出显示,但归属工作区仍然只有一个。
10. 多输出下 floating / sticky 窗口是否迁移工作区,由锚点和跨输出策略决定,不由覆盖面积决定。
11. layout 的 =symbol= 字段用于状态栏显示,应为 1-2 个字符的简短标识符(如 "T", "M"=name= 应保持稳定、可读(如 "tile", "monocle")。
12. 窗口标题、类名等元数据存储在 =wm_window_t= 中,与控制状态一同管理。
13. 工作区名称存储在 =wm_workspace_t= 中,核心算法不应依赖此字段。
14. 每个 workspace 的可用布局列表在启动时分配,之后固定不变;运行时只允许通过 =CYCLE_LAYOUT= 或 =SET_LAYOUT= 命令切换当前活动布局。
15. =frame_rect= 和 =float_rect= 都表示包含边框后的外框矩形。
16. =border_width= / =border_color= 都不是 =wm_window_t= 真状态;它们由 runtime
从全局配置、几何模式和焦点关系推导。
* Generation 字段
=generation= 是版本号,不是业务关系字段。
当前草案只建议保留:
1. =wm_state_t.generation=
每次成功提交状态变更后递增,用于缓存失效、增量重算和调试。
以后如果有必要,再给渲染缓存或状态服务增加更细粒度的版本号。
* 主执行流程
=runtime= 负责固定主循环顺序:
1. =backend.next_event()=
2. runtime 先处理窗口元数据更新这类辅助事件,并直接更新 =wm_window_t= 中的元数据字段
3. =policy.route_event()=
4. =policy.apply_command()=
5. 根据 =dirty_flags= 决定是否重新运行 =layout= 、解析最终外框矩形,并补全
=configure= effect 的边框宽度和边框颜色
6. 将平台 effect 发送给 =backend=
7. 将 render 失效通知发送给服务层
8. =backend.flush()=
若后端发现显示器配置变化,不进入 output 增量更新分支,而是让 =backend.next_event()=
返回 =WM_BACKEND_NEXT_RESTART_REQUIRED= ,由外层退出并整机重启。
* 文件说明
1. =wm_types.h=
通用标量类型、矩形、窗口几何模式和策略枚举。
2. =wm_state.h=
全局状态容器;当前草案同时保留公开字段和统一访问 API便于先把语义定清楚。
3. =wm_event.h=
统一后的 runtime 输入事件;不承载 stop/restart 这类控制信号。
像 =_NET_ACTIVE_WINDOW= 这类平台协议请求,也应在这一层归一化为独立的
request event。
客户端 property / hint 变化(如 =urgent= / =fixed_size= / =skip_taskbar=)也应在
这一层归一化为辅助事件,而不是伪装成 configure request。
4. =wm_binding.h=
=WM_EVENT_KEY_PRESS= / =WM_EVENT_POINTER_BUTTON_PRESS= 使用的输入绑定表和匹配规则。
5. =wm_command.h=
强类型语义命令。
命令语义补充见 [[file:WM_COMMAND_RULES.org][WM_COMMAND_RULES.org]]
=sticky= 多显示器与跨屏行为补充见 [[file:WM_STICKY_WINDOW_RULES.org][WM_STICKY_WINDOW_RULES.org]] 。
6. =wm_plan.h=
脏标记和后端副作用。
7. =wm_layout.h=
布局注册表、布局输入和布局输出。
8. =wm_backend.h=
后端子系统接口。
9. =wm_policy_config.h=
显式策略开关,替代散落在文档正文里的"可选策略"。
10. =wm_policy.h=
事件路由和命令应用接口。
11. =wm_runtime.h=
运行时上下文和生命周期接口。
12. =wm_service.h=
核心外服务的注册和事件订阅接口。
13. [[file:WM_COMMAND_RULES.org][WM_COMMAND_RULES.org]]
关键命令的前置条件、状态转移、可见性和副作用规则。
14. [[file:WM_STICKY_WINDOW_RULES.org][WM_STICKY_WINDOW_RULES.org]]
=sticky= 窗口在 workspace、taskbar、多显示器和跨 output 拖动下的补充规则。
15. [[file:WM_POLICY_APPLY_COMMAND_SKELETON.org][WM_POLICY_APPLY_COMMAND_SKELETON.org]]
=wm_policy_apply_command()= 的伪代码骨架和实现顺序建议。
16. [[file:WORKSPACE_CONFIG_EXAMPLES.org][WORKSPACE_CONFIG_EXAMPLES.org]]
Workspace-Output 配置示例和最佳实践。
* 下一步实施顺序
当前阶段不建议继续扩展抽象,应该开始把最小核心草案落成可运行骨架。推荐顺序如下:
1. 在 =src/core/= 下建立最小骨架,至少放 =wm_types/binding/state/plan/layout/policy/policy_config/runtime/service= 的头文件和空实现,先不要替换现有 =src/= 逻辑。
2. 优先实现 =state + query + plan= 这一层,把动态数组管理、=find_*= 查询函数和 =stack_order[]= 操作补齐。
3. 再实现 =wm_policy_apply_command()= 的一小批核心命令:
=MANAGE_WINDOW=
=UNMANAGE_WINDOW=
=SWITCH_WORKSPACE=
=SET_LAYOUT=
=CYCLE_LAYOUT=
=TOGGLE_FLOATING=
=SET_MAXIMIZED=
=MOVE_FLOATING_WINDOW=
=BEGIN_MOVE_FLOATING_INTERACTION=
=BEGIN_RESIZE_FLOATING_INTERACTION=
4. 给上述命令建立一个无 X11 依赖的测试入口,直接构造 =wm_command_t= ,检查 =wm_state_t==wm_plan_t= 的变化是否符合文档规则。
5.=event -> command -> state -> plan/effect= 这条链路跑通后,再编写一层很薄的 adapter把现有事件翻译为 =wm_event_t= ,并把 effect 翻译回当前 X11 操作。
其中 fullscreen / maximize / minimize 这类平台请求,应先归一化为
=WM_EVENT_WINDOW_STATE_REQUEST= ,再由路由层翻译成 =SET_*= 命令。
6. =status/render/bar= 暂时不要先动。它们属于核心外服务,应在最小核心链路稳定后再迁移。

View File

@@ -1,506 +0,0 @@
* ZDWM 最小核心命令规则
这份文档定义 =wm_policy_apply_command()= 应遵守的最小语义。目标不是覆盖所有未来扩展,而是先把最关键的状态转移规则固定下来,避免后续实现时在 =floating==sticky==geometry_mode==stack_order[]= 上出现分叉语义。
=sticky= 在多显示器、taskbar 和跨 output 拖动下的补充语义见
[[file:WM_STICKY_WINDOW_RULES.org][WM_STICKY_WINDOW_RULES.org]] 。
* 通用规则
1. 找不到目标 =window_id==workspace_id==output_id= 时,命令失败,不修改状态。
2. 命令失败时,不递增 =state.generation=
3. 命令成功且造成状态变化时,递增 =state.generation= ,并设置相应 =dirty_flags=
4. =stack_order[]= 是全局 z-order 真相,=raise/lower/focus= 只能通过修改这个数组影响堆叠。
5. =geometry_mode= 只表示几何显示模式,不表示窗口是否在最上层。
6. =sticky => floating= 。
7. =sticky= 只放宽 workspace 可见性,不改变 =workspace_id=
8. =floating= 决定基础几何来源:
=floating = false= 时,基础几何来自 layout 结果。
=floating = true= 时,基础几何来自 =float_rect=
9. output/workspace/layout registry 以及每个 workspace 的 =available_layouts=
都是 bootstrap 后固定集合;运行时不允许增删。
10. 文档里出现的可选行为,应优先固化到 =wm_policy_config_t= ,而不是继续留在 prose 里。
11. =frame_rect==float_rect= 都表示包含边框后的外框矩形。
12. =border_width= / =border_color= 都不是 =wm_window_t= 真状态runtime 在提交
=WM_EFFECT_CONFIGURE_WINDOW= 时按全局配置和焦点关系解析它们。
* 管理类命令
** =WM_COMMAND_MANAGE_WINDOW=
前置条件:
1. 目标窗口尚未出现在 =state.windows[]= 中。
2. 目标工作区存在。
状态变化:
1.=state.windows[]= 添加一个新窗口。
2. 设定其 =workspace_id=
3. 初始 =floating==sticky==urgent==geometry_mode==command.as.manage_window.initial_state= 显式给出。
4.=initial_state.sticky == true= ,则结果状态中必须满足 =floating == true=
5.=has_initial_float_rect == true= ,将其复制到 =float_rect=
6. 将窗口追加到 =stack_order[]= 顶部。
7. 是否将该窗口设为所属 workspace 的 =focused_window_id= ,由 =initial_state.set_focus==policy.manage_sets_focus= 决定。
副作用:
1. =WM_DIRTY_STATE=
2. =WM_DIRTY_LAYOUT=
3. =WM_DIRTY_STACK=
4. =WM_DIRTY_RENDER=
** =WM_COMMAND_UNMANAGE_WINDOW=
前置条件:
1. 目标窗口存在。
状态变化:
1.=state.windows[]= 中删除该窗口。
2.=stack_order[]= 中移除该窗口。
3. 如果它是某个 workspace 的 =focused_window_id= ,清空或重新选择新的 focused window。
副作用:
1. =WM_DIRTY_STATE=
2. =WM_DIRTY_LAYOUT=
3. =WM_DIRTY_STACK=
4. =WM_DIRTY_RENDER=
* 焦点和堆叠命令
** =WM_COMMAND_FOCUS_WINDOW=
状态变化:
1. 将目标窗口所属 workspace 的 =focused_window_id= 设置为该窗口。
2. 不强制改变 =stack_order[]=
可选策略:
1.=policy.focus_raises == true= ,则在实现中额外执行 =raise_window= 逻辑。
副作用:
1. =WM_DIRTY_DECORATION=
2. =WM_DIRTY_RENDER=
** =WM_COMMAND_FOCUS_DIRECTION=
状态变化:
1. 只在目标 =output_id= 当前显示的 workspace 内选择焦点。
2. 候选窗口应排除 =geometry_mode == WM_GEOMETRY_MINIMIZED= 的窗口。
3. =sticky= 窗口是否参与方向焦点切换,由 =policy.sticky_windows_participate_in_direction_focus= 决定。
副作用:
1. =WM_DIRTY_DECORATION=
2. =WM_DIRTY_RENDER=
** =WM_COMMAND_SET_WINDOW_URGENT=
状态变化:
1. 将目标窗口的 =urgent= 设为命令给定值。
2. 不修改 =focused_window_id=
3. 不修改 =stack_order[]=
副作用:
1. =WM_DIRTY_RENDER=
** =WM_COMMAND_RAISE_WINDOW=
状态变化:
1.=stack_order[]= 中找到目标窗口。
2. 将它移动到数组末尾,也就是最顶层。
副作用:
1. =WM_DIRTY_STACK=
2. =WM_DIRTY_RENDER=
** =WM_COMMAND_LOWER_WINDOW=
状态变化:
1.=stack_order[]= 中找到目标窗口。
2. 将它移动到数组开头,也就是最底层。
副作用:
1. =WM_DIRTY_STACK=
2. =WM_DIRTY_RENDER=
* 工作区命令
** =WM_COMMAND_SWITCH_WORKSPACE=
前置条件:
1. 目标 output 存在。
2. 目标 workspace 存在。
3. 目标 workspace 的 =output_id= 必须等于目标 =output_id=
状态变化:
1. 将目标输出的 =current_workspace_id= 改为指定工作区。
2. 不修改任何窗口的 =workspace_id=
3. 不修改 =stack_order[]=
副作用:
1. =WM_DIRTY_OUTPUT=
2. =WM_DIRTY_LAYOUT=
3. =WM_DIRTY_RENDER=
** =WM_COMMAND_SEND_WINDOW_TO_WORKSPACE=
状态变化:
1. 修改目标窗口的 =workspace_id=
2. 不改变 =sticky=
3. 不改变 =floating=
4. 不改变 =float_rect=
5. 如果窗口是原 workspace 的 =focused_window_id= ,需要重新选择原 workspace 焦点。
副作用:
1. =WM_DIRTY_STATE=
2. =WM_DIRTY_LAYOUT=
3. =WM_DIRTY_RENDER=
** =WM_COMMAND_SEND_WINDOW_TO_OUTPUT=
前置条件:
1. 目标窗口存在。
2. 目标 output 存在。
状态变化:
1. 这是工作区迁移命令的语法糖。
2. 取目标 output 当前的 =current_workspace_id=
3.=policy == WM_CROSS_OUTPUT_KEEP_WORKSPACE= ,则该命令本身不修改 core 真状态。
仅想保留 =workspace_id= 并跨 output 调整几何时,应使用 =WM_COMMAND_MOVE_FLOATING_WINDOW=
4.=policy == WM_CROSS_OUTPUT_MOVE_TO_TARGET_WORKSPACE= ,则把窗口 =workspace_id= 改成目标 output 当前工作区。
副作用:
1. 若最终没有修改 =workspace_id= ,则不产生状态副作用。
2. 若最终修改了 =workspace_id= ,则副作用与 =WM_COMMAND_SEND_WINDOW_TO_WORKSPACE= 一致。
* 布局命令
** =WM_COMMAND_SET_LAYOUT=
前置条件:
1. 目标 workspace 存在。
2. 目标 =layout_id= 已在 layout registry 中注册。
3. 目标 =layout_id= 必须出现在该 workspace 的 =available_layouts[]= 中。
状态变化:
1. 设置目标 workspace 的 =layout_id=
2. 不改变窗口的 =workspace_id=
3. 不改变 =stack_order[]=
4. 不影响 floating 窗口的基础几何。
5. 影响所有 =floating == false==geometry_mode == WM_GEOMETRY_NORMAL= 的窗口。
副作用:
1. =WM_DIRTY_LAYOUT=
2. =WM_DIRTY_RENDER=
** =WM_COMMAND_CYCLE_LAYOUT=
前置条件:
1. 目标 workspace 存在。
2. Workspace 的可用布局列表非空(=layout_count > 0=)。
3. 当前 =layout_id= 必须存在于 =available_layouts[]= 中。
状态变化:
1.=direction == 1= :切换到下一个布局(循环)。
2.=direction == -1= :切换到上一个布局(循环)。
3. 在 workspace 的 =available_layouts[]= 数组中找到当前 =layout_id= 的索引。
4. 按方向移动到相邻索引,循环回到数组开头或末尾。
5. 将新的布局 ID 设置为 workspace 的 =layout_id=
副作用:
1. =WM_DIRTY_LAYOUT=
2. =WM_DIRTY_RENDER=
* 模式切换命令
** =WM_COMMAND_TOGGLE_FLOATING=
前置条件:
1. 目标窗口存在。
2. 若目标窗口当前 =sticky == true= ,命令失败。
状态变化:
1. 若原来 =floating == false=
=floating= 设为 =true=
将当前 =frame_rect= 复制到 =float_rect= ,作为浮动基准外框矩形。
2. 若原来 =floating == true=
=floating= 设为 =false=
保留 =float_rect= ,供未来再次进入 floating 时恢复。
3. 不改变 =geometry_mode=
4. 若窗口处于 =WM_GEOMETRY_MINIMIZED= ,允许切换 =floating= ,但不会立刻显示出来。
副作用:
1. =WM_DIRTY_LAYOUT=
2. =WM_DIRTY_RENDER=
** =WM_COMMAND_TOGGLE_STICKY=
状态变化:
1. 若原来 =sticky == false=
若当前 =floating == false= ,先将当前 =frame_rect= 复制到 =float_rect= ,再设 =floating = true= 。
然后将 =sticky= 设为 =true=
2. 若原来 =sticky == true=
=sticky= 设为 =false=
3. 不改变 =workspace_id=
4. 关闭 =sticky= 时不自动恢复平铺;=floating= 保持原值不变。
5. 不改变 =geometry_mode=
副作用:
1. =WM_DIRTY_LAYOUT=
2. =WM_DIRTY_RENDER=
** =WM_COMMAND_SET_MAXIMIZED=
状态变化:
1.=enabled == true= 且当前 =geometry_mode != WM_GEOMETRY_MAXIMIZED=
=geometry_mode= 设为 =WM_GEOMETRY_MAXIMIZED=
这会覆盖此前的 =FULLSCREEN= / =MINIMIZED= 状态。
2. 若 =enabled == false= 且当前 =geometry_mode == WM_GEOMETRY_MAXIMIZED=
设回 =WM_GEOMETRY_NORMAL= 。
3. 其余情况不产生状态变化。
4. 不改变 =floating= 。
5. 若窗口原本是 floating退出 maximize 后回到 =float_rect= 。
6. 若窗口原本不是 floating退出 maximize 后重新参与 layout。
7. maximize 不改变全局边框策略;有效边框宽度仍由 runtime 统一解析。
副作用:
1. =WM_DIRTY_LAYOUT=
2. =WM_DIRTY_RENDER=
** =WM_COMMAND_SET_FULLSCREEN=
状态变化:
1. 若 =enabled == true= 且当前 =geometry_mode != WM_GEOMETRY_FULLSCREEN=
将 =geometry_mode= 设为 =WM_GEOMETRY_FULLSCREEN= 。
这会覆盖此前的 =MAXIMIZED= / =MINIMIZED= 状态。
2.=enabled == false= 且当前 =geometry_mode == WM_GEOMETRY_FULLSCREEN=
设回 =WM_GEOMETRY_NORMAL=
3. 其余情况不产生状态变化。
4. 不改变 =floating=
5. fullscreen 只决定几何占用 =output.geometry= ,不自动改变 =stack_order[]=
6. fullscreen 的有效边框宽度由 runtime 解析为 0 。
副作用:
1. =WM_DIRTY_LAYOUT=
2. =WM_DIRTY_RENDER=
** =WM_COMMAND_SET_MINIMIZED=
状态变化:
1.=enabled == true= 且当前 =geometry_mode != WM_GEOMETRY_MINIMIZED=
=geometry_mode= 设为 =WM_GEOMETRY_MINIMIZED=
在 X11 后端上,这同时覆盖 ICCCM =IconicState= / =WM_CHANGE_STATE(Iconic)=
语义core 不单独建模 =iconic=
2.=enabled == false= 且当前 =geometry_mode == WM_GEOMETRY_MINIMIZED=
设回 =WM_GEOMETRY_NORMAL=
3. 其余情况不产生状态变化。
4. 不改变 =floating=
5. 不改变 =sticky=
6. minimized 后窗口不再参与显示和焦点候选。
7. 若本次将目标窗口切入 minimized且它正是当前焦点是否清空或重选焦点由 =policy.minimize_clears_focus= 决定。
副作用:
1. =WM_DIRTY_LAYOUT=
2. =WM_DIRTY_RENDER=
* 浮动几何命令
** =WM_COMMAND_MOVE_FLOATING_WINDOW=
前置条件:
1. 目标窗口存在。
2. 目标窗口应为 =floating == true=
状态变化:
1.=dx/dy= 修改 =float_rect.x/y=
2.=commit_workspace_change == false= ,只修改几何,不修改 =workspace_id=
3.=commit_workspace_change == true=
依据 =anchor= 找到目标输出。
再按 =policy= 决定是否修改 =workspace_id=
该规则对普通 =floating= 窗口和 =sticky= 窗口一致适用。
4. 不改变 =geometry_mode=
5. =float_rect= 是外框矩形,因此移动的是包含边框后的最终窗口外框。
副作用:
1. =WM_DIRTY_LAYOUT=
2. =WM_DIRTY_RENDER=
** =WM_COMMAND_RESIZE_FLOATING_WINDOW=
前置条件:
1. 目标窗口存在。
2. 目标窗口应为 =floating == true=
状态变化:
1.=dw/dh= 修改 =float_rect.width/height=
2. 应对宽高做最小值约束,避免得到零或负值。
3. 不改变 =workspace_id=
4. 不改变 =geometry_mode=
5. =float_rect= 是外框矩形,因此缩放的是包含边框后的最终窗口外框。
副作用:
1. =WM_DIRTY_LAYOUT=
2. =WM_DIRTY_RENDER=
* 边框约定
1. 当前最小核心只支持全局统一边框宽度和全局边框颜色集,不支持每窗口独立配置。
2. 全局边框宽度由 =wm_runtime_bootstrap_t.border_width= 提供。
3. 全局边框颜色由 =wm_runtime_bootstrap_t.border_palette= 提供。
4. =layout==floating= 都产出包含边框后的外框矩形。
5. =WM_EFFECT_CONFIGURE_WINDOW.rect= 也是外框矩形。
6. runtime 在把最终矩形转成 =configure= effect 时解析有效边框宽度:
=WM_GEOMETRY_FULLSCREEN= -> 0
其余模式 -> 全局边框宽度
7. runtime 在把最终矩形转成 =configure= effect 时解析有效边框颜色:
focused window -> =border_palette.focused_rgba=
其余窗口 -> =border_palette.normal_rgba=
* redraw 和 runtime 控制命令
** =WM_COMMAND_REDRAW=
状态变化:
1. 不修改 =state=
副作用:
1. =WM_DIRTY_RENDER=
** =WM_COMMAND_QUIT=
状态变化:
1. 不再接收新事件,交由 =runtime= 退出主循环。
副作用:
1. 不产生 =wm_plan_t= 平台 effect。
** =WM_COMMAND_BEGIN_MOVE_FLOATING_INTERACTION=
状态变化:
1. 不修改 =wm_state_t=
2.=runtime= 在命令应用外层拦截,并把 =interaction.mode= 设为
=WM_INTERACTION_MOVE_FLOATING=
3. 只有当当前事件是 =WM_EVENT_POINTER_BUTTON_PRESS= 、目标窗口存在,且窗口
满足 =floating == true= 时,交互才真正开始。
副作用:
1. 不产生 =wm_plan_t= 平台 effect。
** =WM_COMMAND_BEGIN_RESIZE_FLOATING_INTERACTION=
状态变化:
1. 不修改 =wm_state_t=
2.=runtime= 在命令应用外层拦截,并把 =interaction.mode= 设为
=WM_INTERACTION_RESIZE_FLOATING=
3. 只有当当前事件是 =WM_EVENT_POINTER_BUTTON_PRESS= 、目标窗口存在,且窗口
满足 =floating == true= 时,交互才真正开始。
副作用:
1. 不产生 =wm_plan_t= 平台 effect。
补充约定:
1. =WM_COMMAND_QUIT= / =WM_COMMAND_BEGIN_*_INTERACTION= 属于 runtime control commands。
2. 它们应在 =wm_policy_apply_command()= 外层被 =runtime= 拦截,而不是走状态写入分支。
* 可见性推导建议
建议实现顺序如下:
#+BEGIN_SRC c
bool wm_state_window_should_be_visible(const wm_state_t *state,
const wm_window_t *window) {
if (window->geometry_mode == WM_GEOMETRY_MINIMIZED) return false;
if (window->sticky) return true;
return wm_state_workspace_is_visible(state, window->workspace_id);
}
#+END_SRC
然后再按输出判断:
#+BEGIN_SRC c
bool wm_state_window_should_be_visible_on_output(const wm_state_t *state,
const wm_window_t *window,
const wm_output_t *output) {
if (!wm_state_window_should_be_visible(state, window)) return false;
return wm_rect_intersects(window->frame_rect, output->geometry);
}
#+END_SRC
若后续存在按 output 渲染的 taskbar ,建议在下列条件同时满足时显示窗口条目:
1. =window->skip_taskbar == false=
2. =wm_state_window_should_be_visible_on_output(...) == true=
* 几何解析优先级
窗口最终矩形建议按下面的优先级解析:
这里的“最终矩形”统一指包含边框后的外框矩形。
1. =WM_GEOMETRY_MINIMIZED=
不显示。
2. =WM_GEOMETRY_FULLSCREEN=
使用 =output.geometry=
3. =WM_GEOMETRY_MAXIMIZED=
使用 =output.workarea=
4. =floating == true=
使用 =float_rect=
5. 其余情况
使用 layout 输出。

View File

@@ -1,953 +0,0 @@
* ZDWM 最小核心事件路由设计
这份文档定义 =wm_event_t==wm_command_t= 的完整路由规则,确保事件处理链路的每个环节都有明确的行为定义。
* 事件路由总体流程
#+BEGIN_SRC c
void wm_runtime_process_event(wm_runtime_t *runtime, const wm_event_t *event) {
bool consumed = false;
bool quit_requested = false;
wm_command_buffer_reset(&runtime->command_buffer);
// 1. 辅助事件特殊处理(不经过命令系统)
if (is_auxiliary_event(event)) {
runtime_handle_auxiliary_event(runtime, event);
return;
}
// 2. 交互态下的特殊路由
if (runtime->interaction.mode != WM_INTERACTION_NONE) {
consumed = route_interaction_event(runtime, event, &runtime->command_buffer);
}
// 3. 常规事件路由
if (!consumed) {
wm_policy_route_event(
&runtime->state,
&runtime->policy,
&runtime->keybindings,
&runtime->pointer_bindings,
event,
&runtime->command_buffer
);
}
// 4. 应用命令
for (size_t i = 0; i < runtime->command_buffer.count; i++) {
const wm_command_t *command = &runtime->command_buffer.items[i];
if (runtime_handle_control_command(runtime, event, command,
&quit_requested)) {
continue;
}
wm_policy_apply_command(
&runtime->state,
&runtime->policy,
&runtime->layouts,
command,
&runtime->plan
);
}
// 5. runtime 控制命令在命令应用之后统一处理
if (quit_requested) {
wm_runtime_stop(runtime);
}
}
#+END_SRC
这里的 runtime control command 至少包括:
1. =WM_COMMAND_QUIT=
2. =WM_COMMAND_BEGIN_MOVE_FLOATING_INTERACTION=
3. =WM_COMMAND_BEGIN_RESIZE_FLOATING_INTERACTION=
=QUIT= 只修改 runtime 主循环状态;
=BEGIN_*_INTERACTION= 只在当前事件是 =WM_EVENT_POINTER_BUTTON_PRESS= 且目标窗口
有效时更新 =runtime->interaction= ,不进入 =wm_policy_apply_command()=
* 辅助事件处理规则
辅助事件直接更新 runtime 或广播给服务层,不经过命令系统:
#+BEGIN_SRC c
static bool is_auxiliary_event(const wm_event_t *event) {
return event->type == WM_EVENT_WINDOW_METADATA_CHANGED ||
event->type == WM_EVENT_WINDOW_HINTS_CHANGED;
}
static void runtime_handle_auxiliary_event(wm_runtime_t *runtime,
const wm_event_t *event) {
switch (event->type) {
case WM_EVENT_WINDOW_METADATA_CHANGED:
runtime_handle_metadata_event(runtime, event);
return;
case WM_EVENT_WINDOW_HINTS_CHANGED:
runtime_handle_hints_event(runtime, event);
return;
default:
return;
}
}
#+END_SRC
**=WM_EVENT_WINDOW_METADATA_CHANGED=**
#+BEGIN_SRC c
void runtime_handle_metadata_event(wm_runtime_t *runtime, const wm_event_t *event) {
const wm_window_metadata_changed_event_t *e = &event->data.window_metadata_changed;
// 1. 查找窗口
const wm_window_t *win = wm_state_window_get(&runtime->state, e->window);
if (!win) {
return; // 窗口已被销毁
}
// 2. 通过 state 接口更新窗口元数据字段
if (e->changed_fields & WM_WINDOW_META_CHANGED_TITLE) {
wm_state_window_set_title(&runtime->state, e->window, e->title);
}
if (e->changed_fields & WM_WINDOW_META_CHANGED_APP_ID) {
wm_state_window_set_app_id(&runtime->state, e->window, e->app_id);
}
if (e->changed_fields & WM_WINDOW_META_CHANGED_CLASS) {
wm_state_window_set_class(&runtime->state, e->window, e->class_name);
}
if (e->changed_fields & WM_WINDOW_META_CHANGED_INSTANCE) {
wm_state_window_set_instance(&runtime->state, e->window, e->instance_name);
}
// 3. 标记 render 脏(状态栏需要更新)
runtime->plan.dirty_flags |= WM_DIRTY_RENDER;
// 4. 通知订阅的服务(如状态栏组件)
wm_service_event_t service_event = {
.type = WM_SERVICE_EVENT_WINDOW_METADATA_CHANGED,
.as.window_metadata_changed = {
.window_id = e->window,
.changed_fields = e->changed_fields,
},
};
wm_service_registry_emit(&runtime->services, &service_event, &runtime->state);
}
#+END_SRC
**=WM_EVENT_WINDOW_HINTS_CHANGED=**
客户端属性 / hint 更新事件,也不经过命令系统:
#+BEGIN_SRC c
void runtime_handle_hints_event(wm_runtime_t *runtime, const wm_event_t *event) {
const wm_window_hints_changed_event_t *e = &event->data.window_hints_changed;
const wm_window_t *win = wm_state_window_get(&runtime->state, e->window);
if (!win) {
return;
}
if (e->changed_fields & WM_WINDOW_HINT_CHANGED_URGENT) {
wm_state_window_set_urgent(&runtime->state, e->window, e->urgent);
}
if (e->changed_fields & WM_WINDOW_HINT_CHANGED_FIXED_SIZE) {
wm_state_window_set_fixed_size(&runtime->state, e->window, e->fixed_size);
runtime->plan.dirty_flags |= WM_DIRTY_LAYOUT;
}
if (e->changed_fields & WM_WINDOW_HINT_CHANGED_SKIP_TASKBAR) {
wm_state_window_set_skip_taskbar(&runtime->state, e->window,
e->skip_taskbar);
}
runtime->plan.dirty_flags |= WM_DIRTY_RENDER;
}
#+END_SRC
这里的边界是:
1. `window hints changed` 不是 control request不生成 command。
2. `fixed_size` 属于 capability hint不等同于 `floating`
但当前最小实现允许 state setter 在 `fixed_size == true` 时派生出
`floating == true` 这种约束。
3. `skip_taskbar` 主要影响核心外 taskbar/pager 服务的可见性过滤。
* 输入绑定系统
**键盘绑定配置格式**
#+BEGIN_SRC c
// keybindings.conf
keybindings = [
{
// 退出 WM
keysym: "SUPER+SHIFT+q",
command: {
type: WM_COMMAND_QUIT
}
},
{
// 焦点切到前一个窗口
keysym: "SUPER+j",
command: {
type: WM_COMMAND_FOCUS_DIRECTION,
output_id: 0,
direction: "prev"
}
},
{
// 循环切换布局
keysym: "SUPER+space",
command: {
type: WM_COMMAND_CYCLE_LAYOUT,
workspace_id: 0,
direction: 1
}
},
{
// 切换到 workspace 0
keysym: "SUPER+1",
command: {
type: WM_COMMAND_SWITCH_WORKSPACE,
output_id: 0,
workspace_id: 0
}
}
]
#+END_SRC
**鼠标按键绑定配置格式**
#+BEGIN_SRC c
// pointer_bindings.conf
pointer_bindings = [
{
// 点击窗口聚焦。window_id == INVALID 表示“使用当前点击的窗口”
button: "BUTTON1",
target: "window",
command: {
type: WM_COMMAND_FOCUS_WINDOW,
window_id: WM_WINDOW_ID_INVALID
}
},
{
// SUPER + 左键拖动 floating 窗口
button: "SUPER+BUTTON1",
target: "window",
command: {
type: WM_COMMAND_BEGIN_MOVE_FLOATING_INTERACTION,
window_id: WM_WINDOW_ID_INVALID
}
},
{
// SUPER + 右键调整 floating 窗口大小
button: "SUPER+BUTTON3",
target: "window",
command: {
type: WM_COMMAND_BEGIN_RESIZE_FLOATING_INTERACTION,
window_id: WM_WINDOW_ID_INVALID
}
}
]
#+END_SRC
**输入绑定匹配优先级**
1. 完全匹配keysym + 所有 modifiers
2. 通配符匹配keysym + 部分修饰符)
配置加载阶段应先把 =SUPER+m= 这类字符串解析成规范化按键组合。
这里的 =keysym= 指规范化后的逻辑主键编码,不是平台原始扫描码:
事件里的 =keycode= 只是保留 backend 的物理键位信息;
常规绑定仍以 =keysym + modifiers= 为准。
=SUPER+1= 这类行为也必须显式写成一条绑定,而不是由 core 内建生成。
鼠标按键绑定只区分 =root= / =window= 两类命中目标,不覆盖 bar/status 点击区域。
#+BEGIN_SRC c
typedef enum wm_binding_match_mode_t {
WM_BINDING_MATCH_EXACT,
WM_BINDING_MATCH_ALLOW_EXTRA_MODIFIERS,
} wm_binding_match_mode_t;
typedef struct wm_key_binding_t {
wm_keysym_t keysym;
wm_modifier_mask_t modifiers;
wm_binding_match_mode_t match_mode;
wm_command_t command;
} wm_key_binding_t;
typedef struct wm_key_binding_table_t {
const wm_key_binding_t *items;
size_t count;
} wm_key_binding_table_t;
typedef enum wm_pointer_binding_target_t {
WM_POINTER_BINDING_TARGET_ANY,
WM_POINTER_BINDING_TARGET_ROOT,
WM_POINTER_BINDING_TARGET_WINDOW,
} wm_pointer_binding_target_t;
typedef struct wm_pointer_binding_t {
wm_button_t button;
wm_modifier_mask_t modifiers;
wm_binding_match_mode_t match_mode;
wm_pointer_binding_target_t target;
wm_command_t command;
} wm_pointer_binding_t;
typedef struct wm_pointer_binding_table_t {
const wm_pointer_binding_t *items;
size_t count;
} wm_pointer_binding_table_t;
#+END_SRC
绑定表属于 bootstrap 固定输入:
#+BEGIN_SRC c
typedef struct wm_runtime_bootstrap_t {
...
wm_key_binding_table_t keybindings;
wm_pointer_binding_table_t pointer_bindings;
...
} wm_runtime_bootstrap_t;
#+END_SRC
若 backend 需要平台侧被动抓键 / 抓按钮,则通过
=wm_backend_api_t.set_keybindings()= / =set_pointer_bindings()= 接收同一组表。
**=wm_policy_route_event()= 实现**
#+BEGIN_SRC c
bool wm_policy_route_event(const wm_state_t *state,
const wm_policy_config_t *policy,
const wm_key_binding_table_t *keybindings,
const wm_pointer_binding_table_t *pointer_bindings,
const wm_event_t *event,
wm_command_buffer_t *out) {
switch (event->type) {
case WM_EVENT_KEY_PRESS:
return route_key_press(state, keybindings,
&event->data.key_press, out);
case WM_EVENT_POINTER_BUTTON_PRESS:
return route_pointer_press(state, pointer_bindings,
&event->data.pointer_button_press, out);
case WM_EVENT_POINTER_ENTER:
return route_pointer_enter(state, policy,
&event->data.pointer_enter, out);
case WM_EVENT_WINDOW_MAP_REQUEST:
return route_map_request(state, &event->data.window_map_request, out);
case WM_EVENT_WINDOW_REMOVE:
return route_window_remove(state, &event->data.window_remove, out);
case WM_EVENT_WINDOW_ACTIVATE_REQUEST:
return route_activate_request(
state,
&event->data.window_activate_request,
out
);
case WM_EVENT_WINDOW_STATE_REQUEST:
return route_window_state_request(
state,
&event->data.window_state_request,
out
);
case WM_EVENT_CONFIGURE_REQUEST:
return route_configure_request(state, &event->data.configure_request, out);
default:
return false;
}
}
// 键盘事件路由
static bool route_key_press(const wm_state_t *state,
const wm_key_binding_table_t *keybindings,
const wm_key_press_event_t *e,
wm_command_buffer_t *out) {
// 1. 查找匹配的键盘绑定
const wm_key_binding_t *binding = wm_key_binding_find(
keybindings,
e->keysym,
e->modifiers
);
if (!binding) {
return false; // 没有匹配的绑定
}
// 2. 直接复制绑定里的命令模板
return wm_command_buffer_push(out, binding->command);
}
static bool resolve_pointer_window_target(wm_command_t *cmd,
wm_window_id_t event_window) {
switch (cmd->type) {
case WM_COMMAND_UNMANAGE_WINDOW:
if (cmd->as.unmanage_window.window_id == WM_WINDOW_ID_INVALID) {
cmd->as.unmanage_window.window_id = event_window;
}
return cmd->as.unmanage_window.window_id != WM_WINDOW_ID_INVALID;
case WM_COMMAND_FOCUS_WINDOW:
if (cmd->as.focus_window.window_id == WM_WINDOW_ID_INVALID) {
cmd->as.focus_window.window_id = event_window;
}
return cmd->as.focus_window.window_id != WM_WINDOW_ID_INVALID;
case WM_COMMAND_RAISE_WINDOW:
if (cmd->as.raise_window.window_id == WM_WINDOW_ID_INVALID) {
cmd->as.raise_window.window_id = event_window;
}
return cmd->as.raise_window.window_id != WM_WINDOW_ID_INVALID;
case WM_COMMAND_LOWER_WINDOW:
if (cmd->as.lower_window.window_id == WM_WINDOW_ID_INVALID) {
cmd->as.lower_window.window_id = event_window;
}
return cmd->as.lower_window.window_id != WM_WINDOW_ID_INVALID;
case WM_COMMAND_SEND_WINDOW_TO_WORKSPACE:
if (cmd->as.send_window_to_workspace.window_id == WM_WINDOW_ID_INVALID) {
cmd->as.send_window_to_workspace.window_id = event_window;
}
return cmd->as.send_window_to_workspace.window_id !=
WM_WINDOW_ID_INVALID;
case WM_COMMAND_SEND_WINDOW_TO_OUTPUT:
if (cmd->as.send_window_to_output.window_id == WM_WINDOW_ID_INVALID) {
cmd->as.send_window_to_output.window_id = event_window;
}
return cmd->as.send_window_to_output.window_id !=
WM_WINDOW_ID_INVALID;
case WM_COMMAND_TOGGLE_FLOATING:
if (cmd->as.toggle_floating.window_id == WM_WINDOW_ID_INVALID) {
cmd->as.toggle_floating.window_id = event_window;
}
return cmd->as.toggle_floating.window_id != WM_WINDOW_ID_INVALID;
case WM_COMMAND_TOGGLE_STICKY:
if (cmd->as.toggle_sticky.window_id == WM_WINDOW_ID_INVALID) {
cmd->as.toggle_sticky.window_id = event_window;
}
return cmd->as.toggle_sticky.window_id != WM_WINDOW_ID_INVALID;
case WM_COMMAND_SET_MAXIMIZED:
if (cmd->as.set_maximized.window_id == WM_WINDOW_ID_INVALID) {
cmd->as.set_maximized.window_id = event_window;
}
return cmd->as.set_maximized.window_id != WM_WINDOW_ID_INVALID;
case WM_COMMAND_SET_FULLSCREEN:
if (cmd->as.set_fullscreen.window_id == WM_WINDOW_ID_INVALID) {
cmd->as.set_fullscreen.window_id = event_window;
}
return cmd->as.set_fullscreen.window_id != WM_WINDOW_ID_INVALID;
case WM_COMMAND_SET_MINIMIZED:
if (cmd->as.set_minimized.window_id == WM_WINDOW_ID_INVALID) {
cmd->as.set_minimized.window_id = event_window;
}
return cmd->as.set_minimized.window_id != WM_WINDOW_ID_INVALID;
case WM_COMMAND_MOVE_FLOATING_WINDOW:
if (cmd->as.move_floating_window.window_id == WM_WINDOW_ID_INVALID) {
cmd->as.move_floating_window.window_id = event_window;
}
return cmd->as.move_floating_window.window_id !=
WM_WINDOW_ID_INVALID;
case WM_COMMAND_RESIZE_FLOATING_WINDOW:
if (cmd->as.resize_floating_window.window_id == WM_WINDOW_ID_INVALID) {
cmd->as.resize_floating_window.window_id = event_window;
}
return cmd->as.resize_floating_window.window_id !=
WM_WINDOW_ID_INVALID;
case WM_COMMAND_BEGIN_MOVE_FLOATING_INTERACTION:
if (cmd->as.begin_move_floating_interaction.window_id ==
WM_WINDOW_ID_INVALID) {
cmd->as.begin_move_floating_interaction.window_id = event_window;
}
return cmd->as.begin_move_floating_interaction.window_id !=
WM_WINDOW_ID_INVALID;
case WM_COMMAND_BEGIN_RESIZE_FLOATING_INTERACTION:
if (cmd->as.begin_resize_floating_interaction.window_id ==
WM_WINDOW_ID_INVALID) {
cmd->as.begin_resize_floating_interaction.window_id = event_window;
}
return cmd->as.begin_resize_floating_interaction.window_id !=
WM_WINDOW_ID_INVALID;
default:
return true;
}
}
static bool route_pointer_press(const wm_state_t *state,
const wm_pointer_binding_table_t *pointer_bindings,
const wm_pointer_button_event_t *e,
wm_command_buffer_t *out) {
const wm_pointer_binding_t *binding = wm_pointer_binding_find(
pointer_bindings,
e->button,
e->modifiers,
e->window != WM_WINDOW_ID_INVALID
);
wm_command_t cmd;
(void)state;
if (!binding) {
return false;
}
cmd = binding->command;
if (!resolve_pointer_window_target(&cmd, e->window)) {
return false;
}
return wm_command_buffer_push(out, cmd);
}
static bool route_pointer_enter(const wm_state_t *state,
const wm_policy_config_t *policy,
const wm_pointer_enter_event_t *e,
wm_command_buffer_t *out) {
const wm_window_t *win;
const wm_workspace_t *ws;
if (!policy->pointer_enter_focuses_window) {
return false;
}
if (e->window == WM_WINDOW_ID_INVALID) {
return false;
}
win = wm_state_window_get(state, e->window);
if (!win) {
return false;
}
if (win->workspace_id >= state->workspace_count) {
return false;
}
ws = &state->workspaces[win->workspace_id];
if (ws->focused_window_id == win->id) {
return false;
}
return wm_command_buffer_push(out, (wm_command_t){
.type = WM_COMMAND_FOCUS_WINDOW,
.as.focus_window = {
.window_id = win->id,
},
});
}
// 窗口映射请求路由
static bool route_map_request(const wm_state_t *state,
const wm_window_map_request_event_t *e,
wm_command_buffer_t *out) {
// 1. 检查窗口是否已存在
if (wm_state_window_get(state, e->info.id)) {
return false; // 窗口已管理,忽略
}
// 2. 生成 MANAGE_WINDOW 命令
wm_command_t cmd = {
.type = WM_COMMAND_MANAGE_WINDOW,
.as.manage_window = {
.window_id = e->info.id,
// route 层结合 e->info / transient_for / is_dialog
// 推导 initial_state、workspace_id 和初始 float_rect
}
};
return wm_command_buffer_push(out, cmd);
}
static bool route_window_remove(const wm_state_t *state,
const wm_window_remove_event_t *e,
wm_command_buffer_t *out) {
if (!wm_state_window_get(state, e->window)) {
return false;
}
// 当前最小核心不区分 withdrawn / destroy统一转成 unmanage。
(void)e->reason;
return wm_command_buffer_push(out, (wm_command_t){
.type = WM_COMMAND_UNMANAGE_WINDOW,
.as.unmanage_window = {
.window_id = e->window,
},
});
}
static bool route_activate_request(const wm_state_t *state,
const wm_window_activate_request_event_t *e,
wm_command_buffer_t *out) {
const wm_window_t *win = wm_state_window_get(state, e->window);
const wm_workspace_t *ws;
const wm_output_t *output;
wm_command_t cmd;
if (!win) {
return false;
}
if (e->source == WM_WINDOW_ACTIVATION_SOURCE_PAGER) {
ws = &state->workspaces[win->workspace_id];
output = &state->outputs[ws->output_id];
if (output->current_workspace_id != ws->id) {
cmd = (wm_command_t){
.type = WM_COMMAND_SWITCH_WORKSPACE,
.as.switch_workspace = {
.output_id = output->id,
.workspace_id = ws->id,
},
};
if (!wm_command_buffer_push(out, cmd)) {
return false;
}
}
cmd = (wm_command_t){
.type = WM_COMMAND_FOCUS_WINDOW,
.as.focus_window = {
.window_id = win->id,
},
};
return wm_command_buffer_push(out, cmd);
}
cmd = (wm_command_t){
.type = WM_COMMAND_SET_WINDOW_URGENT,
.as.set_window_urgent = {
.window_id = win->id,
.urgent = true,
},
};
return wm_command_buffer_push(out, cmd);
}
static bool route_window_state_request(const wm_state_t *state,
const wm_window_state_request_event_t *e,
wm_command_buffer_t *out) {
const wm_window_t *win = wm_state_window_get(state, e->window);
bool current_enabled;
bool enabled;
wm_command_t cmd;
if (!win) {
return false;
}
switch (e->kind) {
case WM_WINDOW_STATE_REQUEST_FULLSCREEN:
current_enabled = win->geometry_mode == WM_GEOMETRY_FULLSCREEN;
break;
case WM_WINDOW_STATE_REQUEST_MAXIMIZED:
current_enabled = win->geometry_mode == WM_GEOMETRY_MAXIMIZED;
break;
case WM_WINDOW_STATE_REQUEST_MINIMIZED:
current_enabled = win->geometry_mode == WM_GEOMETRY_MINIMIZED;
break;
default:
return false;
}
switch (e->action) {
case WM_WINDOW_STATE_REQUEST_ACTION_ADD:
enabled = true;
break;
case WM_WINDOW_STATE_REQUEST_ACTION_REMOVE:
enabled = false;
break;
case WM_WINDOW_STATE_REQUEST_ACTION_TOGGLE:
enabled = !current_enabled;
break;
default:
return false;
}
switch (e->kind) {
case WM_WINDOW_STATE_REQUEST_FULLSCREEN:
cmd = (wm_command_t){
.type = WM_COMMAND_SET_FULLSCREEN,
.as.set_fullscreen = {
.window_id = win->id,
.enabled = enabled,
},
};
break;
case WM_WINDOW_STATE_REQUEST_MAXIMIZED:
cmd = (wm_command_t){
.type = WM_COMMAND_SET_MAXIMIZED,
.as.set_maximized = {
.window_id = win->id,
.enabled = enabled,
},
};
break;
case WM_WINDOW_STATE_REQUEST_MINIMIZED:
cmd = (wm_command_t){
.type = WM_COMMAND_SET_MINIMIZED,
.as.set_minimized = {
.window_id = win->id,
.enabled = enabled,
},
};
break;
default:
return false;
}
return wm_command_buffer_push(out, cmd);
}
#+END_SRC
这里的最小策略是:
1. `pager` 来源请求可以切到目标窗口所属 workspace并把焦点交给该窗口。
2. `application` / `legacy` 来源不直接抢焦点,而是转成 =SET_WINDOW_URGENT(true)=
3. `WM_EVENT_WINDOW_STATE_REQUEST` 保留 add/remove/toggle 语义,不在 backend 侧提前求最终布尔值。
4. route 层结合当前 =geometry_mode= ,把它翻译成 =SET_FULLSCREEN= / =SET_MAXIMIZED= / =SET_MINIMIZED=
5. 这层抽象可以同时承接 X11 的 =_NET_WM_STATE= 与 Wayland 的窗口状态请求。
6. 若以后需要更细的 focus-stealing prevention可以再下沉到 =wm_policy_config_t=
* 交互态事件路由
交互态会覆盖正常的事件路由:
**=WM_INTERACTION_MOVE_FLOATING=**
#+BEGIN_SRC c
static bool route_interaction_event(wm_runtime_t *runtime,
const wm_event_t *event,
wm_command_buffer_t *out) {
switch (runtime->interaction.mode) {
case WM_INTERACTION_MOVE_FLOATING:
return route_move_floating_event(runtime, event, out);
case WM_INTERACTION_RESIZE_FLOATING:
return route_resize_floating_event(runtime, event, out);
default:
return false;
}
}
static bool route_move_floating_event(wm_runtime_t *runtime,
const wm_event_t *event,
wm_command_buffer_t *out) {
switch (event->type) {
case WM_EVENT_POINTER_BUTTON_RELEASE:
// 结束移动交互。这是 runtime 局部状态,不需要额外命令。
runtime->interaction = (wm_interaction_state_t){0};
return true;
case WM_EVENT_POINTER_MOTION:
// 生成移动命令
wm_command_t move_cmd = {
.type = WM_COMMAND_MOVE_FLOATING_WINDOW,
.as.move_floating_window = {
.window_id = runtime->interaction.window_id,
.dx = event->data.pointer_motion.root.x -
runtime->interaction.pointer_origin.x,
.dy = event->data.pointer_motion.root.y -
runtime->interaction.pointer_origin.y
}
};
return wm_command_buffer_push(out, move_cmd);
default:
return false;
}
}
#+END_SRC
* 命令生成规则
每种事件类型对应一个或多个命令的生成规则:
**输入事件 → 命令映射**
| 事件类型 | 可能生成的命令 | 条件 |
|-------------------------+-----------------------------------------------------+----------------------------------------------------------|
| KEY_PRESS | 任意命令(通过键盘绑定) | 绑定存在 |
| POINTER_BUTTON_PRESS | 任意窗口命令 / BEGIN_*_INTERACTION通过鼠标绑定 | 绑定存在 |
| POINTER_ENTER | FOCUS_WINDOW | policy.pointer_enter_focuses_window == true 且窗口已管理 |
| WINDOW_MAP_REQUEST | MANAGE_WINDOW | 窗口未管理 |
| WINDOW_REMOVE | UNMANAGE_WINDOW | 窗口已管理 |
| WINDOW_ACTIVATE_REQUEST | SWITCH_WORKSPACE + FOCUS_WINDOW / SET_WINDOW_URGENT | source == pager / source != pager |
| WINDOW_STATE_REQUEST | SET_FULLSCREEN / SET_MAXIMIZED / SET_MINIMIZED | 窗口已管理 |
| CONFIGURE_REQUEST | (可能不生成命令) | floating 窗口 |
| POINTER_MOTION | MOVE/RESIZE 命令 | 交互态下 |
补充约定:
1. `WM_EVENT_CONFIGURE_REQUEST` 只携带稀疏几何字段,不直接镜像 X11 全量协议载荷。
2. 最小核心当前只会把这些几何请求映射到 floating 窗口的 move/resize 语义。
3. `WM_EVENT_POINTER_ENTER` 是“pointer focus 进入窗口”的输入事实,不经过 pointer binding 表。
4. 类似 `raise_or_run` 这类程序化恢复鼠标位置导致的单次 crossing 抑制,应由 runtime 维护瞬时状态后在路由前过滤,而不是下沉到 backend 业务逻辑。
5. `border_width` / `sibling` / `stack_mode` 这类协议细节留在 backend / adapter 层。
6. fullscreen / maximize / minimize 这类窗口模式切换不属于 configure request。
7. `WM_EVENT_WINDOW_STATE_REQUEST` 表达后端看到的原始 add/remove/toggle 请求;
core command 仍保持 =SET_*= 这种最终状态语义。
* 事件处理的边界条件
**1. 辅助事件边界**
辅助事件满足以下条件时直接处理,不经过命令系统:
- 事件类型是 `WM_EVENT_WINDOW_METADATA_CHANGED`
- 事件类型是 `WM_EVENT_WINDOW_HINTS_CHANGED`
**2. 交互态边界**
交互态满足以下条件时,优先路由到交互处理器:
- `runtime->interaction.mode != WM_INTERACTION_NONE`
- 事件类型与交互相关POINTER_MOTION, POINTER_BUTTON_RELEASE
**3. 显示器变化边界**
显示器配置变化不进入 core 的增量更新逻辑:
- backend 检测到显示器增删或几何变化时,使 `next_event()` 返回 `WM_BACKEND_NEXT_RESTART_REQUIRED`
- runtime 退出并由外层重启,再重新 bootstrap 固定的 output/workspace/layout 集合
**4. 窗口管理边界**
窗口管理的边界条件:
- `WM_EVENT_WINDOW_MAP_REQUEST`:窗口已存在则忽略
- `WM_EVENT_WINDOW_REMOVE`:窗口不存在则忽略
- `WM_EVENT_WINDOW_ACTIVATE_REQUEST`:窗口不存在则忽略
- `WM_EVENT_WINDOW_STATE_REQUEST`:窗口不存在则忽略
- `WM_EVENT_CONFIGURE_REQUEST`:只有 floating 窗口的几何请求才进入 core 命令链
* 事件处理顺序保证
为了保证确定性行为,事件处理必须按以下顺序进行:
1. **辅助事件优先**:元数据和状态服务更新不走命令系统
2. **交互态优先**:拖拽操作优先于常规输入绑定
3. **输入绑定匹配优先级**:完全匹配 → 通配符
4. **命令执行顺序**:命令缓冲中的命令按顺序执行
* 主执行流程实现
#+BEGIN_SRC c
void wm_runtime_run(wm_runtime_t *runtime) {
wm_event_t event;
while (runtime->running) {
// 1. 获取事件
wm_backend_next_result_t poll_result =
runtime->backend.api->next_event(&runtime->backend, &event);
if (poll_result == WM_BACKEND_NEXT_EVENT) {
// 2. 处理事件
wm_runtime_process_event(runtime, &event);
} else if (poll_result == WM_BACKEND_NEXT_RESTART_REQUIRED) {
wm_runtime_stop(runtime);
break;
} else if (poll_result == WM_BACKEND_NEXT_STOP ||
poll_result == WM_BACKEND_NEXT_ERROR) {
wm_runtime_stop(runtime);
break;
}
// 3. 如果有副作用,应用它们
if (runtime->plan.dirty_flags) {
// 3.1 重新计算布局(如果需要)
if (runtime->plan.dirty_flags & WM_DIRTY_LAYOUT) {
wm_runtime_relayout(runtime);
}
// 3.1.5 若布局或装饰发生变化,重建 configure effects。
// 这里需要同时补全 rect / border_width / border_rgba。
if (runtime->plan.dirty_flags &
(WM_DIRTY_LAYOUT | WM_DIRTY_DECORATION)) {
wm_runtime_rebuild_configure_effects(runtime);
}
// 3.2 依次提交 effect。RENDER_OUTPUT 留给服务层处理。
for (size_t i = 0; i < runtime->plan.effect_count; i++) {
const wm_effect_t *effect = &runtime->plan.effects[i];
if (effect->type == WM_EFFECT_RENDER_OUTPUT) {
wm_service_event_t service_event = {
.type = WM_SERVICE_EVENT_RENDER_OUTPUT,
.as.render_output = {
.output_id = effect->as.render_output.output_id,
},
};
wm_service_registry_emit(&runtime->services,
&service_event,
&runtime->state);
continue;
}
runtime->backend.api->apply_effect(&runtime->backend, effect);
}
// 3.3 刷新平台
if (runtime->backend.api->flush) {
runtime->backend.api->flush(&runtime->backend);
}
// 3.4 重置 plan
wm_plan_reset(&runtime->plan);
}
// 4. 处理定时器(可选,降低 CPU 使用)
usleep(1000); // 1ms
}
}
#+END_SRC
* 与现有功能的兼容性
这个事件路由设计完全兼容现有 ZDWM 功能:
1. **输入绑定系统**:通过 keybindings.conf / pointer_bindings.conf 配置
2. **规则系统**:在 MANAGE_WINDOW 命令中应用
3. **布局切换**:通过 SET_LAYOUT / CYCLE_LAYOUT 命令
4. **拖拽交互**:通过交互态管理
5. **多显示器**:显示器配置变化时通过 `WM_BACKEND_NEXT_RESTART_REQUIRED` 触发重启
* 缺失的文档总结
通过补充本文档mini_core_draft 的事件处理链路就完整了:
1. ✅ 事件定义wm_event.h
2. ✅ 命令定义wm_command.h + WM_COMMAND_RULES.org
3. ✅ 路由接口wm_policy.h
4. ✅ 命令应用WM_POLICY_APPLY_COMMAND_SKELETON.org
5. ✅ **路由规则**(本文档补充)
6. ✅ **输入绑定系统**(本文档补充)
7. ✅ **交互态处理**(本文档补充)
8. ✅ **主执行流程**(本文档补充)

View File

@@ -1,742 +0,0 @@
* ZDWM =wm_policy_apply_command()= 伪代码骨架
这份文档给出 =wm_policy_apply_command()= 的最小实现骨架。目标是把实际代码的控制流固定住,使其和 [[file:WM_COMMAND_RULES.org][WM_COMMAND_RULES.org]] 中定义的命令规则一一对应。
=sticky= 在多显示器、taskbar 和跨 output 拖动下的补充语义见
[[file:WM_STICKY_WINDOW_RULES.org][WM_STICKY_WINDOW_RULES.org]] 。
* 函数职责
=wm_policy_apply_command()= 只做四件事:
1. 校验命令输入是否合法。
2. 修改 =wm_state_t= 中的运行状态。
3.=wm_plan_t= 记录脏标记和后端副作用。
4. 在状态真的发生变化时递增 =state.generation=
补充约定:
1. =border_width= / =border_color= 不是 =wm_window_t= 真状态。
2. =apply_command()= 只负责更新 =frame_rect= / =float_rect= 和脏标记。
3. runtime 在布局求值和 effect 提交阶段,根据全局边框配置补全
=WM_EFFECT_CONFIGURE_WINDOW.border_width= /
=WM_EFFECT_CONFIGURE_WINDOW.border_rgba=
它不负责:
1. 从平台读取事件。
2. 直接调用 X11/Wayland API。
3. 直接执行渲染。
4. 直接格式化状态栏文本。
* 顶层控制流
建议骨架如下:
#+BEGIN_SRC c
bool wm_policy_apply_command(wm_state_t *state,
const wm_policy_config_t *policy,
const wm_layout_registry_t *layouts,
const wm_command_t *command,
wm_plan_t *plan) {
bool changed = false;
bool generation_relevant = false;
if (!state || !policy || !command || !plan) return false;
generation_relevant =
command->type != WM_COMMAND_REDRAW &&
command->type != WM_COMMAND_QUIT &&
command->type != WM_COMMAND_BEGIN_MOVE_FLOATING_INTERACTION &&
command->type != WM_COMMAND_BEGIN_RESIZE_FLOATING_INTERACTION;
switch (command->type) {
case WM_COMMAND_NONE:
return false;
case WM_COMMAND_MANAGE_WINDOW:
changed = apply_manage_window(state, policy, command, plan);
break;
case WM_COMMAND_UNMANAGE_WINDOW:
changed = apply_unmanage_window(state, command, plan);
break;
case WM_COMMAND_FOCUS_WINDOW:
changed = apply_focus_window(state, policy, command, plan);
break;
case WM_COMMAND_FOCUS_DIRECTION:
changed = apply_focus_direction(state, policy, command, plan);
break;
case WM_COMMAND_SET_WINDOW_URGENT:
changed = apply_set_window_urgent(state, command, plan);
break;
case WM_COMMAND_RAISE_WINDOW:
changed = apply_raise_window(state, command, plan);
break;
case WM_COMMAND_LOWER_WINDOW:
changed = apply_lower_window(state, command, plan);
break;
case WM_COMMAND_SWITCH_WORKSPACE:
changed = apply_switch_workspace(state, command, plan);
break;
case WM_COMMAND_SEND_WINDOW_TO_WORKSPACE:
changed = apply_send_window_to_workspace(state, command, plan);
break;
case WM_COMMAND_SEND_WINDOW_TO_OUTPUT:
changed = apply_send_window_to_output(state, command, plan);
break;
case WM_COMMAND_TOGGLE_FLOATING:
changed = apply_toggle_floating(state, command, plan);
break;
case WM_COMMAND_TOGGLE_STICKY:
changed = apply_toggle_sticky(state, command, plan);
break;
case WM_COMMAND_SET_MAXIMIZED:
changed = apply_set_maximized(state, command, plan);
break;
case WM_COMMAND_SET_FULLSCREEN:
changed = apply_set_fullscreen(state, command, plan);
break;
case WM_COMMAND_SET_MINIMIZED:
changed = apply_set_minimized(state, policy, command, plan);
break;
case WM_COMMAND_MOVE_FLOATING_WINDOW:
changed = apply_move_floating_window(state, command, plan);
break;
case WM_COMMAND_RESIZE_FLOATING_WINDOW:
changed = apply_resize_floating_window(state, command, plan);
break;
case WM_COMMAND_BEGIN_MOVE_FLOATING_INTERACTION:
case WM_COMMAND_BEGIN_RESIZE_FLOATING_INTERACTION:
case WM_COMMAND_QUIT:
return false;
case WM_COMMAND_SET_LAYOUT:
changed = apply_set_layout(state, layouts, command, plan);
break;
case WM_COMMAND_CYCLE_LAYOUT:
changed = apply_cycle_layout(state, command, plan);
break;
case WM_COMMAND_REDRAW:
changed = apply_redraw(state, command, plan);
break;
}
if (changed && generation_relevant) state->generation++;
return changed;
}
#+END_SRC
* 辅助函数建议
建议先把这些低层辅助函数写出来,避免在每个命令分支中重复拼逻辑:
#+BEGIN_SRC c
static wm_window_t *require_window(wm_state_t *state, wm_window_id_t id);
static wm_workspace_t *require_workspace(wm_state_t *state, wm_workspace_id_t id);
static wm_output_t *require_output(wm_state_t *state, wm_output_id_t id);
static bool state_append_window(wm_state_t *state, wm_window_t window);
static bool state_remove_window(wm_state_t *state, wm_window_id_t id);
static bool stack_raise(wm_state_t *state, wm_window_id_t id);
static bool stack_lower(wm_state_t *state, wm_window_id_t id);
static bool stack_remove(wm_state_t *state, wm_window_id_t id);
static bool stack_append(wm_state_t *state, wm_window_id_t id);
static bool workspace_has_layout(const wm_workspace_t *workspace,
wm_layout_id_t id);
static void mark_layout_dirty(wm_plan_t *plan);
static void mark_stack_dirty(wm_plan_t *plan);
static void mark_render_dirty(wm_plan_t *plan);
static void mark_output_dirty(wm_plan_t *plan);
static void push_render_all_outputs(const wm_state_t *state, wm_plan_t *plan);
static void push_restack_effect(const wm_state_t *state, wm_plan_t *plan);
static void maybe_refocus_workspace(wm_state_t *state, wm_workspace_t *workspace);
static const wm_output_t *find_output_by_point(const wm_state_t *state,
wm_point_t point);
#+END_SRC
* 命令分支骨架
** =apply_manage_window()=
#+BEGIN_SRC c
static bool apply_manage_window(wm_state_t *state,
const wm_policy_config_t *policy,
const wm_command_t *command,
wm_plan_t *plan) {
wm_window_id_t id = command->as.manage_window.window_id;
wm_workspace_id_t ws_id = command->as.manage_window.workspace_id;
if (wm_state_window_get(state, id)) return false;
wm_workspace_t *ws = require_workspace(state, ws_id);
if (!ws) return false;
wm_window_t win = {
.id = id,
.workspace_id = ws_id,
.geometry_mode = command->as.manage_window.initial_state.geometry_mode,
.floating = command->as.manage_window.initial_state.floating,
.sticky = command->as.manage_window.initial_state.sticky,
.urgent = command->as.manage_window.initial_state.urgent,
};
if (win.sticky) {
win.floating = true;
}
if (command->as.manage_window.has_initial_float_rect) {
win.float_rect = command->as.manage_window.initial_float_rect;
}
if (!state_append_window(state, win)) return false;
if (!stack_append(state, id)) return false;
if (command->as.manage_window.initial_state.set_focus ||
policy->manage_sets_focus) {
ws->focused_window_id = id;
}
plan->dirty_flags |= WM_DIRTY_STATE | WM_DIRTY_LAYOUT |
WM_DIRTY_STACK | WM_DIRTY_RENDER;
push_restack_effect(state, plan);
push_render_all_outputs(state, plan);
return true;
}
#+END_SRC
** =apply_unmanage_window()=
#+BEGIN_SRC c
static bool apply_unmanage_window(wm_state_t *state,
const wm_command_t *command,
wm_plan_t *plan) {
wm_window_id_t id = command->as.unmanage_window.window_id;
wm_window_t *win = require_window(state, id);
if (!win) return false;
wm_workspace_t *ws = require_workspace(state, win->workspace_id);
if (!ws) return false;
if (!stack_remove(state, id)) return false;
if (!state_remove_window(state, id)) return false;
if (ws->focused_window_id == id) maybe_refocus_workspace(state, ws);
plan->dirty_flags |= WM_DIRTY_STATE | WM_DIRTY_LAYOUT |
WM_DIRTY_STACK | WM_DIRTY_RENDER;
push_restack_effect(state, plan);
push_render_all_outputs(state, plan);
return true;
}
#+END_SRC
** =apply_focus_window()=
#+BEGIN_SRC c
static bool apply_focus_window(wm_state_t *state,
const wm_policy_config_t *policy,
const wm_command_t *command,
wm_plan_t *plan) {
wm_window_id_t id = command->as.focus_window.window_id;
wm_window_t *win = require_window(state, id);
if (!win) return false;
wm_workspace_t *ws = require_workspace(state, win->workspace_id);
if (!ws) return false;
if (ws->focused_window_id == id) return false;
ws->focused_window_id = id;
if (policy->focus_raises && stack_raise(state, id)) {
plan->dirty_flags |= WM_DIRTY_STACK;
push_restack_effect(state, plan);
}
plan->dirty_flags |= WM_DIRTY_DECORATION | WM_DIRTY_RENDER;
return true;
}
#+END_SRC
** =apply_toggle_sticky()=
#+BEGIN_SRC c
static bool apply_toggle_sticky(wm_state_t *state,
const wm_command_t *command,
wm_plan_t *plan) {
wm_window_t *win = require_window(state, command->as.toggle_sticky.window_id);
if (!win) return false;
if (!win->sticky) {
if (!win->floating) {
win->float_rect = win->frame_rect;
win->floating = true;
}
win->sticky = true;
} else {
win->sticky = false;
/* 关闭 sticky 时保持 floating 不变,不自动恢复平铺。 */
}
plan->dirty_flags |= WM_DIRTY_LAYOUT | WM_DIRTY_RENDER;
push_render_all_outputs(state, plan);
return true;
}
#+END_SRC
实际实现中,若关闭 =sticky= 后窗口对当前正在查看的 workspace 不再可见,
且它曾是相关 workspace 的焦点窗口,则应重选该 workspace 的焦点。
** =apply_focus_direction()=
#+BEGIN_SRC c
static bool apply_focus_direction(wm_state_t *state,
const wm_policy_config_t *policy,
const wm_command_t *command,
wm_plan_t *plan) {
wm_output_id_t output_id = command->as.focus_direction.output_id;
wm_output_t *out = require_output(state, output_id);
if (!out) return false;
wm_workspace_t *ws = require_workspace(state, out->current_workspace_id);
if (!ws) return false;
wm_window_id_t next =
find_focus_candidate_in_workspace(state, ws,
command->as.focus_direction.direction,
policy->sticky_windows_participate_in_direction_focus);
if (!next || ws->focused_window_id == next) return false;
ws->focused_window_id = next;
plan->dirty_flags |= WM_DIRTY_DECORATION | WM_DIRTY_RENDER;
return true;
}
#+END_SRC
** =apply_set_window_urgent()=
#+BEGIN_SRC c
static bool apply_set_window_urgent(wm_state_t *state,
const wm_command_t *command,
wm_plan_t *plan) {
wm_window_t *win =
require_window(state, command->as.set_window_urgent.window_id);
bool urgent = command->as.set_window_urgent.urgent;
if (!win) return false;
if (win->urgent == urgent) return false;
win->urgent = urgent;
plan->dirty_flags |= WM_DIRTY_RENDER;
push_render_all_outputs(state, plan);
return true;
}
#+END_SRC
** =apply_raise_window()= 和 =apply_lower_window()=
#+BEGIN_SRC c
static bool apply_raise_window(wm_state_t *state,
const wm_command_t *command,
wm_plan_t *plan) {
if (!stack_raise(state, command->as.raise_window.window_id)) return false;
plan->dirty_flags |= WM_DIRTY_STACK | WM_DIRTY_RENDER;
push_restack_effect(state, plan);
return true;
}
static bool apply_lower_window(wm_state_t *state,
const wm_command_t *command,
wm_plan_t *plan) {
if (!stack_lower(state, command->as.lower_window.window_id)) return false;
plan->dirty_flags |= WM_DIRTY_STACK | WM_DIRTY_RENDER;
push_restack_effect(state, plan);
return true;
}
#+END_SRC
** =apply_switch_workspace()=
#+BEGIN_SRC c
static bool apply_switch_workspace(wm_state_t *state,
const wm_command_t *command,
wm_plan_t *plan) {
wm_output_t *out =
require_output(state, command->as.switch_workspace.output_id);
if (!out) return false;
wm_workspace_t *ws =
require_workspace(state, command->as.switch_workspace.workspace_id);
if (!ws) return false;
if (ws->output_id != out->id) return false;
if (out->current_workspace_id == ws->id) {
return false;
}
out->current_workspace_id = ws->id;
plan->dirty_flags |= WM_DIRTY_OUTPUT | WM_DIRTY_LAYOUT | WM_DIRTY_RENDER;
push_render_all_outputs(state, plan);
return true;
}
#+END_SRC
** =apply_send_window_to_workspace()=
#+BEGIN_SRC c
static bool apply_send_window_to_workspace(wm_state_t *state,
const wm_command_t *command,
wm_plan_t *plan) {
wm_window_t *win =
require_window(state, command->as.send_window_to_workspace.window_id);
if (!win) return false;
wm_workspace_id_t dst_id = command->as.send_window_to_workspace.workspace_id;
if (!require_workspace(state, dst_id)) return false;
if (win->workspace_id == dst_id) return false;
wm_workspace_t *src = require_workspace(state, win->workspace_id);
win->workspace_id = dst_id;
if (src && src->focused_window_id == win->id) maybe_refocus_workspace(state, src);
plan->dirty_flags |= WM_DIRTY_STATE | WM_DIRTY_LAYOUT | WM_DIRTY_RENDER;
push_render_all_outputs(state, plan);
return true;
}
#+END_SRC
** =apply_send_window_to_output()=
#+BEGIN_SRC c
static bool apply_send_window_to_output(wm_state_t *state,
const wm_command_t *command,
wm_plan_t *plan) {
wm_window_t *win =
require_window(state, command->as.send_window_to_output.window_id);
wm_output_t *out =
require_output(state, command->as.send_window_to_output.output_id);
if (!win || !out) return false;
if (command->as.send_window_to_output.policy ==
WM_CROSS_OUTPUT_KEEP_WORKSPACE) {
/*
* 保留 workspace 但跨 output 调整几何,不属于这个命令的职责;
* 调用方应使用 MOVE_FLOATING_WINDOW 只改 float_rect。
*/
return false;
}
wm_command_t forwarded = {
.type = WM_COMMAND_SEND_WINDOW_TO_WORKSPACE,
.as.send_window_to_workspace = {
.window_id = win->id,
.workspace_id = out->current_workspace_id,
},
};
return apply_send_window_to_workspace(state, &forwarded, plan);
}
#+END_SRC
** =apply_toggle_floating()=
#+BEGIN_SRC c
static bool apply_toggle_floating(wm_state_t *state,
const wm_command_t *command,
wm_plan_t *plan) {
wm_window_t *win = require_window(state, command->as.toggle_floating.window_id);
if (!win) return false;
if (win->sticky) return false;
if (!win->floating) {
win->floating = true;
win->float_rect = win->frame_rect;
} else {
win->floating = false;
}
plan->dirty_flags |= WM_DIRTY_LAYOUT | WM_DIRTY_RENDER;
push_render_all_outputs(state, plan);
return true;
}
#+END_SRC
** =apply_set_maximized()=
#+BEGIN_SRC c
static bool apply_set_maximized(wm_state_t *state,
const wm_command_t *command,
wm_plan_t *plan) {
wm_window_t *win = require_window(state, command->as.set_maximized.window_id);
if (!win) return false;
wm_window_geometry_mode_t next_mode = win->geometry_mode;
if (command->as.set_maximized.enabled) {
next_mode = WM_GEOMETRY_MAXIMIZED;
} else if (win->geometry_mode == WM_GEOMETRY_MAXIMIZED) {
next_mode = WM_GEOMETRY_NORMAL;
}
if (next_mode == win->geometry_mode) return false;
win->geometry_mode = next_mode;
plan->dirty_flags |= WM_DIRTY_LAYOUT | WM_DIRTY_RENDER;
push_render_all_outputs(state, plan);
return true;
}
#+END_SRC
实际实现中应按 [[file:WM_COMMAND_RULES.org][WM_COMMAND_RULES.org]] 处理 =enabled=
的幂等性,以及与 =FULLSCREEN= / =MINIMIZED= 的互斥切换关系。
** =apply_set_fullscreen()=
#+BEGIN_SRC c
static bool apply_set_fullscreen(wm_state_t *state,
const wm_command_t *command,
wm_plan_t *plan) {
wm_window_t *win =
require_window(state, command->as.set_fullscreen.window_id);
if (!win) return false;
wm_window_geometry_mode_t next_mode = win->geometry_mode;
if (command->as.set_fullscreen.enabled) {
next_mode = WM_GEOMETRY_FULLSCREEN;
} else if (win->geometry_mode == WM_GEOMETRY_FULLSCREEN) {
next_mode = WM_GEOMETRY_NORMAL;
}
if (next_mode == win->geometry_mode) return false;
win->geometry_mode = next_mode;
plan->dirty_flags |= WM_DIRTY_LAYOUT | WM_DIRTY_RENDER;
push_render_all_outputs(state, plan);
return true;
}
#+END_SRC
** =apply_set_minimized()=
#+BEGIN_SRC c
static bool apply_set_minimized(wm_state_t *state,
const wm_policy_config_t *policy,
const wm_command_t *command,
wm_plan_t *plan) {
wm_window_t *win =
require_window(state, command->as.set_minimized.window_id);
if (!win) return false;
wm_workspace_t *ws = require_workspace(state, win->workspace_id);
wm_window_geometry_mode_t next_mode = win->geometry_mode;
if (command->as.set_minimized.enabled) {
next_mode = WM_GEOMETRY_MINIMIZED;
} else if (win->geometry_mode == WM_GEOMETRY_MINIMIZED) {
next_mode = WM_GEOMETRY_NORMAL;
}
if (next_mode == win->geometry_mode) return false;
win->geometry_mode = next_mode;
if (command->as.set_minimized.enabled &&
policy->minimize_clears_focus &&
ws &&
ws->focused_window_id == win->id) {
maybe_refocus_workspace(state, ws);
}
plan->dirty_flags |= WM_DIRTY_LAYOUT | WM_DIRTY_RENDER;
push_render_all_outputs(state, plan);
return true;
}
#+END_SRC
** =apply_move_floating_window()=
#+BEGIN_SRC c
static bool apply_move_floating_window(wm_state_t *state,
const wm_command_t *command,
wm_plan_t *plan) {
wm_window_t *win =
require_window(state, command->as.move_floating_window.window_id);
if (!win || !win->floating) return false;
wm_workspace_id_t old_ws_id = win->workspace_id;
win->float_rect.x += command->as.move_floating_window.dx;
win->float_rect.y += command->as.move_floating_window.dy;
if (command->as.move_floating_window.commit_workspace_change) {
const wm_output_t *out =
find_output_by_point(state, command->as.move_floating_window.anchor);
if (out &&
command->as.move_floating_window.policy ==
WM_CROSS_OUTPUT_MOVE_TO_TARGET_WORKSPACE) {
win->workspace_id = out->current_workspace_id;
if (win->workspace_id != old_ws_id) {
wm_workspace_t *src = require_workspace(state, old_ws_id);
if (src && src->focused_window_id == win->id) {
maybe_refocus_workspace(state, src);
}
}
}
}
plan->dirty_flags |= WM_DIRTY_LAYOUT | WM_DIRTY_RENDER;
push_render_all_outputs(state, plan);
return true;
}
#+END_SRC
** =apply_resize_floating_window()=
#+BEGIN_SRC c
static bool apply_resize_floating_window(wm_state_t *state,
const wm_command_t *command,
wm_plan_t *plan) {
wm_window_t *win =
require_window(state, command->as.resize_floating_window.window_id);
if (!win || !win->floating) return false;
win->float_rect.width =
clamp_i32(win->float_rect.width + command->as.resize_floating_window.dw,
WM_MIN_WINDOW_WIDTH, INT32_MAX);
win->float_rect.height =
clamp_i32(win->float_rect.height + command->as.resize_floating_window.dh,
WM_MIN_WINDOW_HEIGHT, INT32_MAX);
plan->dirty_flags |= WM_DIRTY_LAYOUT | WM_DIRTY_RENDER;
push_render_all_outputs(state, plan);
return true;
}
#+END_SRC
** =apply_set_layout()=
#+BEGIN_SRC c
static bool apply_set_layout(wm_state_t *state,
const wm_layout_registry_t *layouts,
const wm_command_t *command,
wm_plan_t *plan) {
wm_workspace_t *ws =
require_workspace(state, command->as.set_layout.workspace_id);
if (!ws) return false;
if (!wm_layout_slot_get(layouts, command->as.set_layout.layout_id)) {
return false;
}
if (!workspace_has_layout(ws, command->as.set_layout.layout_id)) {
return false;
}
if (ws->layout_id == command->as.set_layout.layout_id) return false;
ws->layout_id = command->as.set_layout.layout_id;
plan->dirty_flags |= WM_DIRTY_LAYOUT | WM_DIRTY_RENDER;
push_render_all_outputs(state, plan);
return true;
}
#+END_SRC
** =apply_cycle_layout()=
#+BEGIN_SRC c
static bool apply_cycle_layout(wm_state_t *state,
const wm_command_t *command,
wm_plan_t *plan) {
wm_workspace_t *ws =
require_workspace(state, command->as.cycle_layout.workspace_id);
if (!ws) return false;
if (ws->layout_count == 0) return false;
// 找到当前布局在列表中的索引
size_t current_idx = 0;
bool found_current = false;
for (size_t i = 0; i < ws->layout_count; i++) {
if (ws->available_layouts[i] == ws->layout_id) {
current_idx = i;
found_current = true;
break;
}
}
if (!found_current) return false;
// 计算下一个索引(支持前后循环)
size_t next_idx;
if (command->as.cycle_layout.direction > 0) {
next_idx = (current_idx + 1) % ws->layout_count;
} else {
next_idx = (current_idx == 0) ? (ws->layout_count - 1) : (current_idx - 1);
}
// 如果新旧布局相同,直接返回
wm_layout_id_t next_layout = ws->available_layouts[next_idx];
if (next_layout == ws->layout_id) return false;
ws->layout_id = next_layout;
plan->dirty_flags |= WM_DIRTY_LAYOUT | WM_DIRTY_RENDER;
push_render_all_outputs(state, plan);
return true;
}
#+END_SRC
** =apply_redraw()=
#+BEGIN_SRC c
static bool apply_redraw(wm_state_t *state,
const wm_command_t *command,
wm_plan_t *plan) {
(void)state;
(void)command;
plan->dirty_flags |= WM_DIRTY_RENDER;
push_render_all_outputs(state, plan);
return true;
}
#+END_SRC
通常情况下 =WM_COMMAND_QUIT= / =WM_COMMAND_BEGIN_*_INTERACTION= 都会由
=runtime= 在命令应用外层直接拦截,因此 =wm_policy_apply_command()= 不应真正看见
这些 runtime control commands。
* 推荐的后处理步骤
=apply_*()= 返回后,建议由上层统一做这些事,而不是在每个分支里直接跑布局:
1. 如果有 =WM_DIRTY_LAYOUT= ,则对每个 output 的当前 workspace 重新计算 layout。
2. 若有 =WM_DIRTY_LAYOUT==WM_DIRTY_DECORATION= ,则根据窗口的
=geometry_mode==floating= 、焦点关系和全局边框配置解析最终矩形、
有效边框宽度与边框颜色。
=sticky= 窗口由于满足 =sticky => floating= ,因此同样走 =float_rect= 路径。
3. 生成 =WM_EFFECT_CONFIGURE_WINDOW=
4. 若有 =WM_DIRTY_STACK= ,生成 =WM_EFFECT_RESTACK_WINDOWS=
5. 若有 =WM_DIRTY_RENDER= ,生成 =WM_EFFECT_RENDER_OUTPUT=
6. =WM_EFFECT_RENDER_OUTPUT= 由 runtime 分发给服务层,而不是直接交给 backend 。
* 为什么要拆成多个 =apply_*()=
1. 便于单元测试,每个命令可以单独测试。
2. 便于和 =WM_COMMAND_RULES.org= 对照审查。
3. 便于以后替换部分策略,例如 =focus raises= 或 sticky 参与焦点候选的规则。
4. 便于将复杂逻辑下沉到更小的辅助函数,而不是把 =wm_policy_apply_command()= 写成一个超长 switch。

View File

@@ -1,200 +0,0 @@
* Sticky 窗口规则
这份文档补充 =sticky= 窗口在最小核心中的约束,重点覆盖以下问题:
1. =sticky==floating= 的关系
2. =sticky= 窗口是否参与自动布局
3. =sticky= 窗口在 taskbar 中的显示规则
4. 多显示器下 =sticky= 窗口的可见性与跨屏移动语义
* 核心结论
1. =sticky => floating=
2. =sticky= 窗口不参与任何自动布局
3. =sticky= 只放宽 workspace 可见性,不改变窗口的唯一归属 =workspace_id=
4. =sticky= 窗口不复制到每个 output是否出现在某个 output 上,只由其最终矩形是否覆盖该 output 决定
5. 拖动 =sticky= 或普通 =floating= 窗口跨 output 时,默认只修改 =float_rect= ,不隐式修改 =workspace_id=
6. 只有显式提交跨 output 迁移时,才允许把窗口归属 workspace 改到目标 output 当前显示的 workspace
* 状态约束
** =sticky= 与 =floating=
最小核心中,=sticky= 不是独立于 =floating= 的另一套几何模式,而是附加在 floating 语义之上的可见性扩展。
因此规定:
1. 若窗口进入 =sticky= 状态,则必须满足 =floating == true=
2. 若窗口原本是平铺窗口,开启 =sticky= 时,应先把当前最终外框矩形写入 =float_rect= ,再设 =floating = true= ,最后设 =sticky = true=
3. 关闭 =sticky= 时,只清除 =sticky= 标记,不自动恢复到平铺模式
不自动恢复平铺的原因是:否则核心必须额外保存“进入 sticky 前是否为 floating”的历史状态复杂度不值得。
** 唯一归属 workspace
每个窗口始终只有一个归属 =workspace_id=
=sticky= 的作用只是:
- 让窗口在非归属 workspace 上也可见
而不是:
- 把窗口复制到多个 workspace
- 让窗口同时归属于多个 workspace
因此:
1. =workspace_id= 仍然是窗口的唯一归属
2. layout、focus 历史、命令目标和后续迁移规则都仍以该归属 workspace 为准
* 布局规则
** 不参与自动布局
=sticky= 窗口不参与任何自动布局。
这条规则在单显示器和多显示器下都成立:
1. 不进入任何 workspace 的平铺输入
2. 不进入任何 output 的布局输入
3. 几何始终来自 =float_rect=
原因:
1. 自动布局应是 workspace-local / output-local 的
2. =sticky= 如果参与布局,会让同一个窗口对多个 workspace 或多个 output 的布局结果产生耦合
3. 同一个窗口不能同时接受多份独立 layout 结果
** =fn == NULL= 的 floating 布局槽位
若当前 workspace 的活动布局槽位满足 =fn == NULL= ,则该布局表示 =floating= 布局。
这只表示当前 workspace 上的普通窗口不进行平铺计算,不改变 =sticky= 窗口的特殊规则:
1. 普通 floating 窗口:几何来自各自的 =float_rect=
2. =sticky= 窗口:同样几何来自 =float_rect=
3. 二者都不参与 layout 函数计算
* Taskbar 规则
** 基本规则
如果 taskbar 表达的是“当前 workspace 上用户可见的窗口列表”,则 =sticky= 窗口应被视为可见窗口,因此应出现在 taskbar 中。
推荐的基础判断是:
1. =skip_taskbar == false=
2. 窗口在当前上下文中实际可见
** 单显示器
单显示器下,若当前 workspace 与窗口归属 workspace 不同,只要窗口是 =sticky= 且当前几何可见,仍应出现在 taskbar 中。
** 多显示器 / 每 output 一条 taskbar
若系统为每个 output 提供独立 taskbar ,推荐规则是:
1. 只在窗口实际可见的 output 上显示该窗口
2. 不在所有 output 的 taskbar 中无条件复制显示
也就是说:
1. 窗口只落在左屏,则只显示在左屏 taskbar
2. 窗口只落在右屏,则只显示在右屏 taskbar
3. 窗口跨越两个 output ,则可同时出现在两个 output 的 taskbar
这样 taskbar 与屏幕上的真实可见内容保持一致。
* 多显示器规则
** 坐标空间
=float_rect==frame_rect= 使用全局桌面坐标,而不是某个 output 的局部坐标。
因此:
1. 一个 floating/sticky 窗口可以自然跨越多个 output
2. output 可见性可以通过“窗口最终矩形是否与 output 矩形相交”来判断
** 可见性
=sticky= 只放宽 workspace 可见性,不提供 output 复制语义。
因此:
1. 窗口不会因为 =sticky= 自动在所有 output 上都出现
2. 它只会出现在最终矩形实际覆盖到的 output 上
3. 若窗口完全移到另一块屏幕,则旧屏幕上不再可见
* 跨 output 移动规则
** 拖动过程中
无论窗口是普通 =floating= 还是 =sticky= ,拖动过程中都只更新 =float_rect=
不应在拖动过程中频繁修改 =workspace_id= ,否则归属语义会抖动,也不利于测试。
** 拖动结束后的归属处理
拖动结束后,是否迁移窗口归属 workspace应由显式策略控制而不是隐式发生。
推荐沿用以下策略:
1. =WM_CROSS_OUTPUT_KEEP_WORKSPACE=
- 只更新 =float_rect=
- 不修改 =workspace_id=
2. =WM_CROSS_OUTPUT_MOVE_TO_TARGET_WORKSPACE=
- 使用稳定锚点确定目标 output
- 若锚点落在另一个 output 上,则把 =workspace_id= 改为该 output 当前显示的 workspace
推荐锚点:
1. 窗口中心点
2. 或命令中显式提供的 =anchor=
不建议按覆盖面积决定归属,因为该规则在窗口跨屏时不稳定,也不直观。
** =sticky= 下的跨 output 移动
=sticky= 窗口跨屏拖动时,默认也不自动修改 =workspace_id=
原因:
1. =sticky= 本来就可以跨 workspace 可见
2. 若移动几何时顺便修改归属,会让“移动窗口”和“迁移窗口”两个动作发生耦合
因此推荐:
1. 默认只更新 =float_rect=
2. 若用户明确要求迁移,则再执行归属 workspace 切换
* 关闭 Sticky 的语义
关闭 =sticky= 时:
1. 只清除 =sticky= 标记
2. 不自动修改 =workspace_id=
3. 不自动修改 =floating=
4. 不自动恢复平铺
若当前查看的 workspace 不是窗口归属的 =workspace_id= ,则关闭 =sticky= 后窗口应立即从当前视图消失。
如果该窗口在关闭 =sticky= 前是当前焦点窗口,则需要重新选择当前 workspace 的焦点窗口。
* 设计原则总结
这套规则的目标是把几个概念彻底拆开:
1. =floating= 管几何来源
2. =sticky= 管跨 workspace 可见性
3. =workspace_id= 管唯一归属
4. 跨 output 拖动只改几何
5. workspace 迁移必须显式提交
这样可以避免以下几类混乱语义:
1. 拖动窗口时归属 workspace 频繁跳变
2. =sticky= 窗口同时参与多个布局
3. taskbar 显示内容与实际可见窗口不一致
4. 关闭 =sticky= 时隐式触发额外状态回滚

View File

@@ -1,286 +0,0 @@
* Workspace-Output 配置示例
本文档展示在不同场景下如何配置 workspace 和 output 的归属关系。
** 单显示器配置
最简单的配置:所有 workspace 归属到唯一输出。
#+BEGIN_SRC lua
-- zdwm.lua
outputs = {
{ id = 0, name = "DP-1", primary = true },
}
-- 所有 workspace 都归属 DP-1
workspaces = {
{ id = 1, name = "1", output_id = 0 },
{ id = 2, name = "2", output_id = 0 },
{ id = 3, name = "3", output_id = 0 },
{ id = 4, name = "4", output_id = 0 },
{ id = 5, name = "5", output_id = 0 },
{ id = 6, name = "6", output_id = 0 },
{ id = 7, name = "7", output_id = 0 },
{ id = 8, name = "8", output_id = 0 },
{ id = 9, name = "9", output_id = 0 },
{ id = 10, name = "10", output_id = 0 },
}
-- 或使用自动生成
workspaces = auto_generate_workspaces({
output_id = 0,
count = 10,
name_pattern = "number" -- 1, 2, 3, ...
})
#+END_SRC
** 双显示器配置
*** 场景 1平分 workspace
#+BEGIN_SRC lua
-- 左侧显示器 1-5右侧显示器 6-10
outputs = {
{ id = 0, name = "DP-1", primary = true },
{ id = 1, name = "DP-2" },
}
workspaces = {
-- DP-1 的 workspace
{ id = 1, name = "1", output_id = 0 },
{ id = 2, name = "2", output_id = 0 },
{ id = 3, name = "3", output_id = 0 },
{ id = 4, name = "4", output_id = 0 },
{ id = 5, name = "5", output_id = 0 },
-- DP-2 的 workspace
{ id = 6, name = "6", output_id = 1 },
{ id = 7, name = "7", output_id = 1 },
{ id = 8, name = "8", output_id = 1 },
{ id = 9, name = "9", output_id = 1 },
{ id = 10, name = "10", output_id = 1 },
}
#+END_SRC
*** 场景 2偶数/奇数分配
#+BEGIN_SRC lua
-- 奇数在左,偶数在右
workspaces = {}
for i = 1, 10 do
table.insert(workspaces, {
id = i,
name = tostring(i),
output_id = (i % 2 == 1) and 0 or 1 -- 奇数->0偶数->1
})
end
#+END_SRC
*** 场景 3主题化 workspace
#+BEGIN_SRC lua
-- 左侧开发相关,右侧娱乐相关
outputs = {
{ id = 0, name = "DP-1", primary = true },
{ id = 1, name = "HDMI-1" },
}
workspaces = {
-- DP-1 (开发)
{ id = 1, name = "code", output_id = 0 },
{ id = 2, name = "term", output_id = 0 },
{ id = 3, name = "docs", output_id = 0 },
{ id = 4, name = "debug", output_id = 0 },
-- HDMI-1 (娱乐)
{ id = 5, name = "chat", output_id = 1 },
{ id = 6, name = "web", output_id = 1 },
{ id = 7, name = "media", output_id = 1 },
{ id = 8, name = "game", output_id = 1 },
}
#+END_SRC
** 三显示器配置
笔记本 + 外接显示器的常见配置:
#+BEGIN_SRC lua
-- eDP-1: 日常工作 (1-3)
-- DP-1: 代码 (4-6)
-- HDMI-1: 娱乐 (7-9)
outputs = {
{ id = 0, name = "eDP-1", primary = true },
{ id = 1, name = "DP-1" },
{ id = 2, name = "HDMI-1" },
}
workspaces = {
-- eDP-1 (笔记本)
{ id = 1, name = "mail", output_id = 0 },
{ id = 2, name = "chat", output_id = 0 },
{ id = 3, name = "docs", output_id = 0 },
-- DP-1 (外接显示器 - 左)
{ id = 4, name = "code", output_id = 1 },
{ id = 5, name = "term", output_id = 1 },
{ id = 6, name = "debug", output_id = 1 },
-- HDMI-1 (外接显示器 - 右)
{ id = 7, name = "web", output_id = 2 },
{ id = 8, name = "media", output_id = 2 },
{ id = 9, name = "game", output_id = 2 },
}
#+END_SRC
** 启动阶段脚本生成
根据启动阶段检测到的输出数量生成:
#+BEGIN_SRC lua
function configure_workspaces(outputs)
local workspaces = {}
local ws_id = 1
-- 每个 output 分配 3 个 workspace
for _, output in ipairs(outputs) do
for i = 1, 3 do
table.insert(workspaces, {
id = ws_id,
name = string.format("%d:%s", ws_id, output.name),
output_id = output.id,
layout_id = 0, -- 默认布局
})
ws_id = ws_id + 1
end
end
return workspaces
end
#+END_SRC
** 配置加载流程
#+BEGIN_SRC c
// config_loader.c
typedef struct workspace_config_t {
wm_workspace_id_t id;
const char *name;
wm_output_id_t output_id;
wm_layout_id_t layout_id;
} workspace_config_t;
bool load_workspace_config(const char *path,
workspace_config_t **out_workspaces,
size_t *out_count) {
// 1. 扫描输出
output_config_t *outputs;
size_t output_count;
scan_outputs(&outputs, &output_count);
// 2. 加载 workspace 配置
workspace_config_t *workspaces;
size_t workspace_count;
parse_config_file(path, &workspaces, &workspace_count);
// 3. 验证:每个 workspace 的 output_id 必须有效
for (size_t i = 0; i < workspace_count; i++) {
if (workspaces[i].output_id >= output_count) {
fprintf(stderr, "Workspace %d: invalid output_id %d\n",
workspaces[i].id, workspaces[i].output_id);
return false;
}
}
*out_workspaces = workspaces;
*out_count = workspace_count;
return true;
}
#+END_SRC
** 切换行为示例
#+BEGIN_SRC lua
-- 切换到 workspace 4在 DP-1 上)
-- 效果:只有 DP-1 切换DP-2 保持当前 workspace
-- 将窗口从 workspace 1 移动到 workspace 4
-- 效果:窗口从 DP-1 移动到 DP-1但在不同的 workspace
-- 如果 workspace 4 当前在 DP-2 显示,窗口会在 DP-2 出现
#+END_SRC
** 布局列表配置
每个 workspace 可以配置自己的可用布局列表。
*** 基础配置
#+BEGIN_SRC lua
-- 声明布局注册表
-- layout_id 按注册顺序稳定分配tile=0, monocle=1, floating=2, ...
layouts = {
{ name = "tile", symbol = "T", fn = layout_tile },
{ name = "monocle", symbol = "M", fn = layout_monocle },
{ name = "floating", symbol = "F", fn = layout_floating },
{ name = "stack", symbol = "S", fn = layout_stack },
{ name = "grid", symbol = "G", fn = layout_grid },
}
-- 单显示器:每个 workspace 可用所有布局
workspaces = {
{ id = 1, name = "1", output_id = 0, layouts = {0, 1, 2, 3, 4}, default_layout = 0 },
{ id = 2, name = "2", output_id = 0, layouts = {0, 1, 2, 3, 4}, default_layout = 0 },
{ id = 3, name = "3", output_id = 0, layouts = {0, 1, 2, 3, 4}, default_layout = 0 },
}
#+END_SRC
*** 主题化布局配置
不同用途的 workspace 配置不同的布局集合:
#+BEGIN_SRC lua
workspaces = {
-- 代码 workspace平铺、单列、堆叠适合代码+终端)
{ id = 1, name = "code", output_id = 0,
layouts = {0, 3, 4}, default_layout = 0 }, -- T, S, G
-- 浏览器 workspace单列、浮动、全屏适合阅读
{ id = 2, name = "web", output_id = 0,
layouts = {1, 2}, default_layout = 1 }, -- M, F
-- 游戏 workspace仅浮动游戏窗口通常需要浮动
{ id = 3, name = "game", output_id = 1,
layouts = {2}, default_layout = 2 }, -- F only
-- 聊天 workspace堆叠、单列
{ id = 4, name = "chat", output_id = 1,
layouts = {3, 1}, default_layout = 3 }, -- S, M
}
#+END_SRC
*** 默认布局配置
如果不指定 layouts 字段,使用全局默认布局列表:
#+BEGIN_SRC lua
-- 全局默认
default_layouts = {0, 1, 2, 3, 4} -- 所有布局可用
-- workspace 省略 layouts 时使用全局默认
workspaces = {
{ id = 1, name = "1", output_id = 0 }, -- 使用全局默认
{ id = 2, name = "2", output_id = 0,
layouts = {0, 1}, default_layout = 0 }, -- 自定义
}
#+END_SRC
** 注意事项
1. *固定归属*workspace.output_id 在创建后不应改变
2. *输出变化*显示器配置变化时WM 会重启,重新扫描和配置
3. *可见性*:普通非 =sticky= 窗口先受 workspace 可见性约束,再按最终矩形与 output 的相交关系决定具体显示在哪些 output 上;=sticky= 窗口只放宽 workspace 可见性
4. *独立性*:切换不同 output 的 workspace 互不影响
5. *布局列表*workspace.layouts 在启动时分配,运行时固定
6. *布局切换*:使用 =CYCLE_LAYOUT==SET_LAYOUT= 命令在可用布局集合内切换当前布局

View File

@@ -1,61 +0,0 @@
#pragma once
#include "wm_binding.h"
#include "wm_event.h"
#include "wm_plan.h"
typedef struct wm_backend_t wm_backend_t;
typedef enum wm_backend_next_result_t {
// 成功产出一个 runtime 输入事件out 有效
WM_BACKEND_NEXT_EVENT,
// backend 请求 runtime 正常停止(例如连接关闭)
WM_BACKEND_NEXT_STOP,
// backend 检测到必须重建 bootstrap 的平台变化(例如显示器配置变化)
WM_BACKEND_NEXT_RESTART_REQUIRED,
// backend 遇到无法恢复的错误
WM_BACKEND_NEXT_ERROR,
} wm_backend_next_result_t;
typedef struct wm_backend_api_t {
bool (*init)(wm_backend_t *backend);
/*
* 安装 bootstrap 固定下来的键盘绑定表。
* X11 这类需要被动抓键的平台可以在这里建立平台侧 grab
* 总能收到所有键盘事件的 backend 可以把它留空。
*/
bool (*set_keybindings)(
wm_backend_t *backend,
const wm_key_binding_table_t *keybindings
);
/*
* 安装 bootstrap 固定下来的鼠标按键绑定表。
* X11 这类需要被动抓按钮的平台可以把它缓存下来,并在 root 或新受管窗口
* 上按 target 规则建立 grab总能收到所有 pointer button 事件的 backend
* 可以把它留空。
*/
bool (*set_pointer_bindings)(
wm_backend_t *backend,
const wm_pointer_binding_table_t *pointer_bindings
);
/*
* 返回一个 backend 轮询结果。
* 只有返回 WM_BACKEND_NEXT_EVENT 时out 才包含一个完整的 runtime 输入事件。
*/
wm_backend_next_result_t (*next_event)(
wm_backend_t *backend,
wm_event_t *out
);
/*
* Runtime filters out service-side effects such as WM_EFFECT_RENDER_OUTPUT.
* Backends only receive platform effects here.
*/
bool (*apply_effect)(wm_backend_t *backend, const wm_effect_t *effect);
void (*flush)(wm_backend_t *backend);
void (*shutdown)(wm_backend_t *backend);
} wm_backend_api_t;
struct wm_backend_t {
const wm_backend_api_t *api;
void *impl;
};

View File

@@ -1,92 +0,0 @@
#pragma once
#include <stdbool.h>
#include <stddef.h>
#include "wm_command.h"
#include "wm_types.h"
/*
* 输入绑定只匹配 backend 已归一化后的输入事件。
* backend 负责把平台原始输入翻译为 wm_event_t并决定是否折叠 auto-repeat。
*/
typedef enum wm_binding_match_mode_t {
// modifiers 必须与事件完全一致
WM_BINDING_MATCH_EXACT,
// 事件允许带有更多修饰键,但必须至少包含 binding.modifiers
WM_BINDING_MATCH_ALLOW_EXTRA_MODIFIERS,
} wm_binding_match_mode_t;
typedef struct wm_key_binding_t {
wm_keysym_t keysym;
wm_modifier_mask_t modifiers;
wm_binding_match_mode_t match_mode;
// 命中的绑定会直接复制这条命令模板进入 command buffer
wm_command_t command;
} wm_key_binding_t;
typedef struct wm_key_binding_table_t {
const wm_key_binding_t *items;
size_t count;
} wm_key_binding_table_t;
/*
* 核心只关心 root / window 两类命中目标:
* - bar / status / widget 等点击区域属于核心外服务,不进入最小核心绑定系统
* - 当事件未命中受管窗口时wm_event_t.window == WM_WINDOW_ID_INVALID
*/
typedef enum wm_pointer_binding_target_t {
WM_POINTER_BINDING_TARGET_ANY,
WM_POINTER_BINDING_TARGET_ROOT,
WM_POINTER_BINDING_TARGET_WINDOW,
} wm_pointer_binding_target_t;
/*
* 最小核心当前只定义“按下触发”的鼠标绑定;
* button release 主要保留给 move/resize 交互结束逻辑。
*
* 若 command 是窗口目标命令,且模板中的 window_id 为
* WM_WINDOW_ID_INVALID则 route_pointer_press() 应把它替换为当前点击的
* event->window。
*/
typedef struct wm_pointer_binding_t {
wm_button_t button;
wm_modifier_mask_t modifiers;
wm_binding_match_mode_t match_mode;
wm_pointer_binding_target_t target;
wm_command_t command;
} wm_pointer_binding_t;
typedef struct wm_pointer_binding_table_t {
const wm_pointer_binding_t *items;
size_t count;
} wm_pointer_binding_table_t;
/*
* 查找与当前按键事件匹配的绑定。
* 匹配顺序固定为:
* 1. 所有 exact 绑定,按数组顺序扫描
* 2. 所有 allow-extra-modifiers 绑定,按数组顺序扫描
*
* 当前最小核心不内建“默认绑定生成规则”;像 SUPER+1 这类行为也必须显式
* 出现在 key binding table 里。
*/
const wm_key_binding_t *wm_key_binding_find(
const wm_key_binding_table_t *table,
wm_keysym_t keysym,
wm_modifier_mask_t modifiers
);
/*
* 查找与当前按钮按下事件匹配的鼠标绑定。
* hits_window == true 表示事件命中了某个受管窗口;
* hits_window == false 表示事件落在 root / 空白区域。
*
* 匹配顺序与键盘绑定一致:先 exact再 allow-extra-modifiers。
*/
const wm_pointer_binding_t *wm_pointer_binding_find(
const wm_pointer_binding_table_t *table,
wm_button_t button,
wm_modifier_mask_t modifiers,
bool hits_window
);

View File

@@ -1,149 +0,0 @@
#pragma once
#include "wm_types.h"
typedef struct wm_manage_window_init_t {
wm_window_geometry_mode_t geometry_mode;
bool floating;
bool sticky;
bool urgent;
bool set_focus;
} wm_manage_window_init_t;
typedef enum wm_command_type_t {
WM_COMMAND_NONE,
WM_COMMAND_MANAGE_WINDOW,
WM_COMMAND_UNMANAGE_WINDOW,
WM_COMMAND_FOCUS_WINDOW,
WM_COMMAND_FOCUS_DIRECTION,
WM_COMMAND_SET_WINDOW_URGENT,
WM_COMMAND_RAISE_WINDOW,
WM_COMMAND_LOWER_WINDOW,
WM_COMMAND_SWITCH_WORKSPACE,
WM_COMMAND_SEND_WINDOW_TO_WORKSPACE,
WM_COMMAND_SEND_WINDOW_TO_OUTPUT,
WM_COMMAND_TOGGLE_FLOATING,
WM_COMMAND_TOGGLE_STICKY,
WM_COMMAND_SET_MAXIMIZED,
WM_COMMAND_SET_FULLSCREEN,
WM_COMMAND_SET_MINIMIZED,
WM_COMMAND_MOVE_FLOATING_WINDOW,
WM_COMMAND_RESIZE_FLOATING_WINDOW,
WM_COMMAND_BEGIN_MOVE_FLOATING_INTERACTION,
WM_COMMAND_BEGIN_RESIZE_FLOATING_INTERACTION,
WM_COMMAND_SET_LAYOUT,
WM_COMMAND_CYCLE_LAYOUT,
WM_COMMAND_REDRAW,
WM_COMMAND_QUIT,
} wm_command_type_t;
typedef struct wm_command_t {
wm_command_type_t type;
union {
struct {
wm_window_id_t window_id;
wm_workspace_id_t workspace_id;
wm_manage_window_init_t initial_state;
bool has_initial_float_rect;
wm_rect_t initial_float_rect;
} manage_window;
struct {
wm_window_id_t window_id;
} unmanage_window;
struct {
wm_window_id_t window_id;
} focus_window;
struct {
wm_output_id_t output_id;
wm_focus_direction_t direction;
} focus_direction;
struct {
wm_window_id_t window_id;
bool urgent;
} set_window_urgent;
struct {
wm_window_id_t window_id;
} raise_window;
struct {
wm_window_id_t window_id;
} lower_window;
struct {
wm_output_id_t output_id;
wm_workspace_id_t workspace_id;
} switch_workspace;
struct {
wm_window_id_t window_id;
wm_workspace_id_t workspace_id;
} send_window_to_workspace;
struct {
wm_window_id_t window_id;
wm_output_id_t output_id;
wm_cross_output_policy_t policy;
} send_window_to_output;
struct {
wm_window_id_t window_id;
} toggle_floating;
struct {
wm_window_id_t window_id;
} toggle_sticky;
struct {
wm_window_id_t window_id;
bool enabled;
} set_maximized;
struct {
wm_window_id_t window_id;
bool enabled;
} set_fullscreen;
struct {
wm_window_id_t window_id;
bool enabled;
} set_minimized;
struct {
wm_window_id_t window_id;
int16_t dx;
int16_t dy;
bool commit_workspace_change;
wm_point_t anchor;
wm_cross_output_policy_t policy;
} move_floating_window;
struct {
wm_window_id_t window_id;
int16_t dw;
int16_t dh;
} resize_floating_window;
struct {
wm_window_id_t window_id;
} begin_move_floating_interaction;
struct {
wm_window_id_t window_id;
} begin_resize_floating_interaction;
struct {
wm_workspace_id_t workspace_id;
wm_layout_id_t layout_id;
} set_layout;
struct {
wm_workspace_id_t workspace_id;
int direction; // 1: 下一个布局, -1: 上一个布局
} cycle_layout;
} as;
} wm_command_t;

View File

@@ -1,36 +0,0 @@
#pragma once
#include "wm_types.h"
typedef struct wm_output_info_t {
char *name;
wm_rect_t geometry;
} wm_output_info_t;
/*
* 由 backend 提供的窗口基础信息。
*
* 这些字符串由调用方提供state 如需长期持有,应自行复制。
*/
typedef struct wm_window_info_t {
wm_window_id_t id;
wm_rect_t frame_rect;
const char *title;
const char *app_id;
const char *class_name;
const char *instance_name;
wm_window_geometry_mode_t geometry_mode;
bool urgent;
bool fixed_size;
bool skip_taskbar;
} wm_window_info_t;
typedef struct wm_workspace_desc_t {
size_t output_index;
const char *name;
const wm_layout_id_t *layout_ids;
size_t layout_count;
wm_layout_id_t initial_layout_id;
} wm_workspace_desc_t;

View File

@@ -1,228 +0,0 @@
#pragma once
#include "wm_desc.h"
/*
* wm_event_t 表示“runtime 真正关心的输入事实”:
* - 它不是平台原始事件的逐字段照抄
* - 它允许 backend 对平台事件做最小必要的归一化 / 过滤 / 聚合
* - 它不承载 runtime 控制语义stop / restart / error 由 backend.next_event()
* 的返回结果表达,而不是伪装成一个事件类型
*/
typedef enum wm_event_type_t {
// 最小核心里的快捷键只由 key press 触发,不单独暴露 key release 事件
WM_EVENT_KEY_PRESS,
// 鼠标绑定只匹配 button pressbutton release 主要供交互态结束拖拽使用
WM_EVENT_POINTER_BUTTON_PRESS,
WM_EVENT_POINTER_BUTTON_RELEASE,
WM_EVENT_POINTER_MOTION,
// 指针焦点进入某个窗口;对应 X11 EnterNotify / Wayland wl_pointer.enter
WM_EVENT_POINTER_ENTER,
WM_EVENT_WINDOW_MAP_REQUEST,
WM_EVENT_WINDOW_REMOVE,
WM_EVENT_WINDOW_METADATA_CHANGED,
WM_EVENT_WINDOW_HINTS_CHANGED,
WM_EVENT_WINDOW_ACTIVATE_REQUEST,
WM_EVENT_WINDOW_STATE_REQUEST,
WM_EVENT_CONFIGURE_REQUEST,
} wm_event_type_t;
typedef struct wm_key_press_event_t {
/*
* 规范化后的逻辑主键编码xkb keysym
* backend 应先结合 keymap / xkb state 把原始按键解析成逻辑键,再写入这里。
* 它适合与 "SUPER+m" 这类配置字符串解析结果做匹配。
*/
wm_keysym_t keysym;
// backend 归一化后的修饰键掩码
wm_modifier_mask_t modifiers;
/*
* backend 原始物理键编码。
* 它保留平台侧按键信息,便于调试、日志或未来按物理键位绑定;
* 常规快捷键匹配不应直接使用它。
*/
uint32_t keycode;
} wm_key_press_event_t;
typedef struct wm_pointer_button_event_t {
wm_button_t button;
wm_modifier_mask_t modifiers;
// backend 原始按钮码,仅供调试或未来平台特化逻辑使用
uint32_t raw_button;
// 全局桌面坐标
wm_point_t root;
// 相对 window 的坐标;若 window == WM_WINDOW_ID_INVALID则该值未定义
wm_point_t local;
// 当前命中的窗口;若未命中窗口,则为 WM_WINDOW_ID_INVALID
wm_window_id_t window;
} wm_pointer_button_event_t;
typedef struct wm_pointer_motion_event_t {
// 全局桌面坐标
wm_point_t root;
// 相对 window 的坐标;若 window == WM_WINDOW_ID_INVALID则该值未定义
wm_point_t local;
// 当前命中的窗口;若未命中窗口,则为 WM_WINDOW_ID_INVALID
wm_window_id_t window;
} wm_pointer_motion_event_t;
typedef struct wm_pointer_enter_event_t {
// 全局桌面坐标
wm_point_t root;
// 相对 window 的坐标;若 window == WM_WINDOW_ID_INVALID则该值未定义
wm_point_t local;
// 当前进入并获得 pointer focus 的窗口;若未命中窗口,则为
// WM_WINDOW_ID_INVALID
wm_window_id_t window;
} wm_pointer_enter_event_t;
typedef struct wm_window_map_request_event_t {
wm_window_info_t info;
// 仅用于 manage 路由判断,不进入持久 state
wm_window_id_t transient_for;
// 仅用于 manage 路由判断,不进入持久 state
bool is_dialog;
} wm_window_map_request_event_t;
/*
* 归一化后的窗口移除事件。
* backend 把 withdraw / destroy 这类平台差异折叠成单一 remove 事件,
* core 只按 reason 区分“为何离开 managed 集合”。
*/
typedef enum wm_window_remove_reason_t {
WM_WINDOW_REMOVE_WITHDRAWN,
WM_WINDOW_REMOVE_DESTROY,
} wm_window_remove_reason_t;
typedef struct wm_window_remove_event_t {
wm_window_id_t window;
wm_window_remove_reason_t reason;
} wm_window_remove_event_t;
typedef enum wm_window_meta_changed_flags_t {
WM_WINDOW_META_CHANGED_NONE = 0,
WM_WINDOW_META_CHANGED_TITLE = 1u << 0,
WM_WINDOW_META_CHANGED_APP_ID = 1u << 1,
WM_WINDOW_META_CHANGED_CLASS = 1u << 2,
WM_WINDOW_META_CHANGED_INSTANCE = 1u << 3,
} wm_window_meta_changed_flags_t;
typedef struct wm_window_metadata_changed_event_t {
wm_window_id_t window;
uint32_t changed_fields;
/*
* These strings are borrowed from the backend adapter and are valid until
* wm_runtime_process_event() returns. changed_fields decides which ones are
* meaningful for the current event.
* runtime 会复制这些字符串到 wm_window_t 中的对应字段。
*/
const char *title;
const char *app_id;
const char *class_name;
const char *instance_name;
} wm_window_metadata_changed_event_t;
typedef enum wm_window_hint_changed_flags_t {
WM_WINDOW_HINT_CHANGED_NONE = 0,
WM_WINDOW_HINT_CHANGED_URGENT = 1u << 0,
WM_WINDOW_HINT_CHANGED_FIXED_SIZE = 1u << 1,
WM_WINDOW_HINT_CHANGED_SKIP_TASKBAR = 1u << 2,
} wm_window_hint_changed_flags_t;
/*
* 客户端属性 / hint 变化的归一化结果。
* 这类事件不是控制请求,不经过 command 路由;
* runtime 直接把结果同步到 wm_window_t 中的 hint/capability 字段。
*/
typedef struct wm_window_hints_changed_event_t {
wm_window_id_t window;
uint32_t changed_fields;
bool urgent;
bool fixed_size;
bool skip_taskbar;
} wm_window_hints_changed_event_t;
/*
* 归一化后的窗口激活请求来源。
* 取值对齐 EWMH _NET_ACTIVE_WINDOW 的 source indication
* - 0: 旧客户端 / 未指明来源
* - 1: 应用自身发起
* - 2: pager / taskbar / window switcher 发起
*/
typedef enum wm_window_activation_source_t {
WM_WINDOW_ACTIVATION_SOURCE_LEGACY = 0,
WM_WINDOW_ACTIVATION_SOURCE_APPLICATION = 1,
WM_WINDOW_ACTIVATION_SOURCE_PAGER = 2,
} wm_window_activation_source_t;
typedef struct wm_window_activate_request_event_t {
wm_window_id_t window;
wm_window_activation_source_t source;
} wm_window_activate_request_event_t;
/*
* 归一化后的窗口模式切换请求。
* backend 保留 add/remove/toggle 这类原始协议语义;
* route_event() 再结合当前 wm_state_t 翻译成最终的 SET_* 命令。
*/
typedef enum wm_window_state_request_kind_t {
WM_WINDOW_STATE_REQUEST_FULLSCREEN,
WM_WINDOW_STATE_REQUEST_MAXIMIZED,
WM_WINDOW_STATE_REQUEST_MINIMIZED,
} wm_window_state_request_kind_t;
typedef enum wm_window_state_request_action_t {
WM_WINDOW_STATE_REQUEST_ACTION_ADD,
WM_WINDOW_STATE_REQUEST_ACTION_REMOVE,
WM_WINDOW_STATE_REQUEST_ACTION_TOGGLE,
} wm_window_state_request_action_t;
typedef struct wm_window_state_request_event_t {
wm_window_id_t window;
wm_window_state_request_kind_t kind;
wm_window_state_request_action_t action;
} wm_window_state_request_event_t;
typedef enum wm_configure_request_field_t {
WM_CONFIGURE_REQUEST_FIELD_NONE = 0,
WM_CONFIGURE_REQUEST_FIELD_X = 1u << 0,
WM_CONFIGURE_REQUEST_FIELD_Y = 1u << 1,
WM_CONFIGURE_REQUEST_FIELD_WIDTH = 1u << 2,
WM_CONFIGURE_REQUEST_FIELD_HEIGHT = 1u << 3,
} wm_configure_request_field_t;
/*
* 归一化后的窗口配置请求。
* 这类事件只表达稀疏几何请求,不承载 fullscreen/maximize/minimize
* 之类窗口模式切换;后者应进入独立的 state request event。
*
* x / y / width / height 中只有 changed_fields 指示的成员才有意义。
*/
typedef struct wm_configure_request_event_t {
wm_window_id_t window;
uint32_t changed_fields;
int32_t x;
int32_t y;
int32_t width;
int32_t height;
} wm_configure_request_event_t;
typedef struct wm_event_t {
wm_event_type_t type;
// backend 归一化后的事件时间值(毫秒),不是 wall-clock 时间戳
wm_time_ms_t time_ms;
union {
wm_key_press_event_t key_press;
wm_pointer_button_event_t pointer_button_press;
wm_pointer_button_event_t pointer_button_release;
wm_pointer_motion_event_t pointer_motion;
wm_pointer_enter_event_t pointer_enter;
wm_window_map_request_event_t window_map_request;
wm_window_remove_event_t window_remove;
wm_window_metadata_changed_event_t window_metadata_changed;
wm_window_hints_changed_event_t window_hints_changed;
wm_window_activate_request_event_t window_activate_request;
wm_window_state_request_event_t window_state_request;
wm_configure_request_event_t configure_request;
} data;
} wm_event_t;

View File

@@ -1,88 +0,0 @@
#pragma once
#include "wm_types.h"
typedef struct wm_layout_window_ref_t {
wm_window_id_t window_id;
wm_window_geometry_mode_t geometry_mode;
} wm_layout_window_ref_t;
typedef struct wm_layout_ctx_t {
wm_output_id_t output_id;
wm_workspace_id_t workspace_id;
wm_window_id_t focused_window_id;
wm_rect_t workarea;
const wm_layout_window_ref_t *windows;
size_t window_count;
} wm_layout_ctx_t;
typedef struct wm_layout_item_t {
wm_window_id_t window_id;
wm_rect_t rect; // 平铺窗口的目标外框矩形(包含边框)
} wm_layout_item_t;
typedef struct wm_layout_result_t {
wm_layout_item_t *items;
size_t item_count;
size_t item_capacity;
} wm_layout_result_t;
typedef bool (*wm_layout_fn)(
const wm_layout_ctx_t *ctx,
wm_layout_result_t *out
);
typedef struct wm_layout_slot_t {
wm_layout_id_t id;
/*
* 布局稳定名称,用于:
* - 配置引用
* - 日志输出
* - 调试输出
*
* 推荐使用可读名字,如 "tile"、"monocle"、"floating"。
*/
const char *name;
/*
* 布局符号,用于:
* - 状态栏显示(应简短,如 "T", "M", "F"
* - 紧凑场景下的快速识别
*
* 推荐使用 1-2 个字符的简短标识符。
*/
const char *symbol;
wm_layout_fn fn;
} wm_layout_slot_t;
typedef struct wm_layout_registry_t {
wm_layout_slot_t *slots;
size_t slot_count;
size_t slot_capacity;
} wm_layout_registry_t;
void wm_layout_result_init(wm_layout_result_t *result);
void wm_layout_result_reset(wm_layout_result_t *result);
void wm_layout_result_shutdown(wm_layout_result_t *result);
bool wm_layout_result_push(wm_layout_result_t *result, wm_layout_item_t item);
void wm_layout_registry_init(wm_layout_registry_t *registry);
void wm_layout_registry_shutdown(wm_layout_registry_t *registry);
size_t wm_layout_registry_count(const wm_layout_registry_t *registry);
const wm_layout_slot_t *
wm_layout_registry_at(const wm_layout_registry_t *registry, size_t index);
wm_layout_id_t wm_layout_register(
wm_layout_registry_t *registry,
const char *name,
const char *symbol,
wm_layout_fn fn
);
/*
* layout registry 只允许在 bootstrap / 初始化阶段注册。
* 运行时布局集合保持固定,只允许切换 workspace 当前选中的 layout_id。
*/
wm_layout_fn
wm_layout_lookup(const wm_layout_registry_t *registry, wm_layout_id_t id);
const wm_layout_slot_t *
wm_layout_slot_get(const wm_layout_registry_t *registry, wm_layout_id_t id);

View File

@@ -1,70 +0,0 @@
#pragma once
#include "wm_types.h"
typedef enum wm_dirty_flags_t {
WM_DIRTY_NONE = 0,
WM_DIRTY_STATE = 1u << 0,
WM_DIRTY_LAYOUT = 1u << 1,
WM_DIRTY_STACK = 1u << 2,
WM_DIRTY_OUTPUT = 1u << 3,
// 只影响窗口装饰,不改变 layout 结果;例如焦点切换导致边框颜色变化
WM_DIRTY_DECORATION = 1u << 4,
WM_DIRTY_RENDER = 1u << 5,
} wm_dirty_flags_t;
typedef enum wm_effect_type_t {
WM_EFFECT_NONE,
WM_EFFECT_MAP_WINDOW,
WM_EFFECT_UNMAP_WINDOW,
WM_EFFECT_CONFIGURE_WINDOW,
WM_EFFECT_FOCUS_WINDOW,
WM_EFFECT_RESTACK_WINDOWS,
WM_EFFECT_RENDER_OUTPUT,
} wm_effect_type_t;
typedef struct wm_effect_t {
wm_effect_type_t type;
union {
struct {
wm_window_id_t window_id;
} map_window;
struct {
wm_window_id_t window_id;
} unmap_window;
struct {
wm_window_id_t window_id;
wm_rect_t rect; // 最终外框矩形(包含边框)
uint16_t border_width; // runtime 解析出的有效边框宽度
wm_rgba32_t border_rgba; // runtime 解析出的有效边框颜色
} configure_window;
struct {
wm_window_id_t window_id;
} focus_window;
struct {
const wm_window_id_t *stack_order;
size_t stack_count;
} restack_windows;
struct {
wm_output_id_t output_id;
} render_output;
} as;
} wm_effect_t;
typedef struct wm_plan_t {
wm_effect_t *effects;
size_t effect_count;
size_t effect_capacity;
uint32_t dirty_flags;
} wm_plan_t;
void wm_plan_init(wm_plan_t *plan);
void wm_plan_reset(wm_plan_t *plan);
void wm_plan_shutdown(wm_plan_t *plan);
bool wm_plan_push_effect(wm_plan_t *plan, wm_effect_t effect);

View File

@@ -1,38 +0,0 @@
#pragma once
#include "wm_binding.h"
#include "wm_command.h"
#include "wm_event.h"
#include "wm_layout.h"
#include "wm_plan.h"
#include "wm_policy_config.h"
#include "wm_state.h"
typedef struct wm_command_buffer_t {
wm_command_t *items;
size_t count;
size_t capacity;
} wm_command_buffer_t;
void wm_command_buffer_init(wm_command_buffer_t *buffer);
void wm_command_buffer_reset(wm_command_buffer_t *buffer);
void wm_command_buffer_shutdown(wm_command_buffer_t *buffer);
bool wm_command_buffer_push(wm_command_buffer_t *buffer, wm_command_t command);
bool wm_policy_route_event(
const wm_state_t *state,
const wm_policy_config_t *policy,
const wm_key_binding_table_t *keybindings,
const wm_pointer_binding_table_t *pointer_bindings,
const wm_event_t *event,
wm_command_buffer_t *out
);
bool wm_policy_apply_command(
wm_state_t *state,
const wm_policy_config_t *policy,
const wm_layout_registry_t *layouts,
const wm_command_t *command,
wm_plan_t *plan
);

View File

@@ -1,14 +0,0 @@
#pragma once
#include <stdbool.h>
typedef struct wm_policy_config_t {
bool focus_raises;
bool pointer_enter_focuses_window;
bool sticky_windows_participate_in_direction_focus;
bool manage_sets_focus;
bool switch_workspace_restores_last_focus;
bool minimize_clears_focus;
} wm_policy_config_t;
void wm_policy_config_init_default(wm_policy_config_t *config);

View File

@@ -1,136 +0,0 @@
#pragma once
#include <stdint.h>
#include "wm_backend.h"
#include "wm_binding.h"
#include "wm_layout.h"
#include "wm_plan.h"
#include "wm_policy.h"
#include "wm_policy_config.h"
#include "wm_service.h"
#include "wm_state.h"
typedef enum wm_interaction_mode_t {
WM_INTERACTION_NONE,
WM_INTERACTION_MOVE_FLOATING,
WM_INTERACTION_RESIZE_FLOATING,
} wm_interaction_mode_t;
typedef struct wm_interaction_state_t {
wm_interaction_mode_t mode;
wm_window_id_t window_id;
wm_output_id_t origin_output_id;
wm_point_t pointer_origin;
wm_rect_t window_origin_rect;
} wm_interaction_state_t;
typedef struct wm_border_palette_t {
wm_rgba32_t normal_rgba;
wm_rgba32_t focused_rgba;
} wm_border_palette_t;
typedef struct wm_runtime_bootstrap_t {
/*
* 输出配置(启动时扫描显示器后填充)。
* 这是 bootstrap 后固定的 output 集合。
*/
const wm_output_t *outputs;
size_t output_count;
/*
* 工作区配置(已绑定 output
* workspace.output_id 指向 outputs 中的某个输出
* 这是 bootstrap 后固定的 workspace 集合。
*/
const wm_workspace_t *workspaces;
size_t workspace_count;
/*
* 布局算法注册。
* layout registry 在 bootstrap 后固定;运行时只切换 workspace.layout_id。
*/
struct {
wm_layout_fn fn;
const char *name;
const char *symbol;
} *layouts;
size_t layout_count;
/*
* 策略配置
*/
wm_policy_config_t policy;
/*
* 不可变键盘绑定表。
* 当前草案把它视为 bootstrap 提供的只读视图items 的生命周期必须覆盖
* runtime。
*/
wm_key_binding_table_t keybindings;
/*
* 不可变鼠标按键绑定表。
* bar / status 点击属于核心外服务,因此这里只覆盖 root / window 目标。
*/
wm_pointer_binding_table_t pointer_bindings;
/*
* 全局统一边框宽度。
* 它不是窗口真状态。runtime 在提交 configure effect 时:
* - fullscreen -> 0
* - 其余模式 -> border_width
*/
uint16_t border_width;
/*
* 全局边框颜色配置。
* 它不是窗口真状态。runtime 在提交 configure effect 时根据当前焦点关系解析:
* - 当前 workspace 的 focused window -> focused_rgba
* - 其余窗口 -> normal_rgba
*/
wm_border_palette_t border_palette;
/*
* Existing windows discovered during startup should be translated into these
* commands instead of being injected into wm_state_t directly.
*/
const wm_command_t *initial_commands;
size_t initial_command_count;
} wm_runtime_bootstrap_t;
// 当前草案直接展示 runtime 结构,便于讨论;实现阶段可再封装
typedef struct wm_runtime_t {
bool running;
wm_state_t state;
wm_plan_t plan;
wm_command_buffer_t command_buffer;
wm_layout_registry_t layouts;
wm_policy_config_t policy;
wm_key_binding_table_t keybindings;
wm_pointer_binding_table_t pointer_bindings;
uint16_t border_width;
wm_border_palette_t border_palette;
wm_interaction_state_t interaction;
wm_backend_t backend;
wm_service_registry_t services;
} wm_runtime_t;
bool wm_runtime_init(
wm_runtime_t *runtime,
wm_backend_t backend,
const wm_runtime_bootstrap_t *bootstrap
);
bool wm_runtime_register_service(wm_runtime_t *runtime, wm_service_t service);
/*
* 处理单个事件,方便做无平台依赖的单元测试。
* 元数据事件允许 runtime 直接更新 wm_window_t 中的元数据字段;
* 控制状态变更仍经由 route_event() + apply_command()。
*/
bool wm_runtime_process_event(wm_runtime_t *runtime, const wm_event_t *event);
void wm_runtime_run(wm_runtime_t *runtime);
void wm_runtime_stop(wm_runtime_t *runtime);
void wm_runtime_shutdown(wm_runtime_t *runtime);

View File

@@ -1,63 +0,0 @@
#pragma once
#include <stdint.h>
#include "wm_state.h"
typedef enum wm_service_event_type_t {
WM_SERVICE_EVENT_NONE,
WM_SERVICE_EVENT_RUNTIME_STARTED,
WM_SERVICE_EVENT_RUNTIME_STOPPING,
WM_SERVICE_EVENT_WINDOW_METADATA_CHANGED,
WM_SERVICE_EVENT_RENDER_OUTPUT,
} wm_service_event_type_t;
typedef struct wm_service_event_t {
wm_service_event_type_t type;
union {
struct {
wm_window_id_t window_id;
uint32_t changed_fields;
} window_metadata_changed;
struct {
wm_output_id_t output_id;
} render_output;
} as;
} wm_service_event_t;
typedef struct wm_service_t wm_service_t;
typedef struct wm_service_api_t {
bool (*init)(wm_service_t *service);
void (*handle_event)(
wm_service_t *service,
const wm_service_event_t *event,
const wm_state_t *state
);
void (*shutdown)(wm_service_t *service);
} wm_service_api_t;
struct wm_service_t {
const wm_service_api_t *api;
void *impl;
};
typedef struct wm_service_registry_t {
wm_service_t *items;
size_t count;
size_t capacity;
} wm_service_registry_t;
void wm_service_registry_init(wm_service_registry_t *registry);
void wm_service_registry_shutdown(wm_service_registry_t *registry);
bool wm_service_registry_register(
wm_service_registry_t *registry,
wm_service_t service
);
void wm_service_registry_emit(
wm_service_registry_t *registry,
const wm_service_event_t *event,
const wm_state_t *state
);

View File

@@ -1,266 +0,0 @@
#pragma once
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include "wm_types.h"
// ========== 核心实体定义 ==========
typedef struct wm_window_t {
wm_window_id_t id;
wm_workspace_id_t workspace_id;
// 几何模式
wm_window_geometry_mode_t geometry_mode;
bool floating;
bool sticky;
bool urgent;
bool fixed_size;
// 几何信息(均为包含边框后的外框矩形)
wm_rect_t float_rect; // floating 模式下记忆的外框矩形
wm_rect_t frame_rect; // 当前最终外框矩形(由 layout 或 float_rect 解析)
// 元数据(核心算法不依赖,仅用于规则匹配和服务层展示)
char *title;
char *app_id;
char *class_name;
char *instance_name;
// 任务栏可见性提示
bool skip_taskbar;
} wm_window_t;
typedef struct wm_workspace_t {
wm_workspace_id_t id;
wm_output_id_t output_id; // 固定归属某个输出
wm_layout_id_t layout_id; // 当前活动布局(可用布局列表中的一个)
wm_window_id_t focused_window_id;
// 可用布局列表(启动时根据配置分配,之后固定不变)
const wm_layout_id_t *available_layouts;
size_t layout_count;
// 名称(核心算法不依赖,仅用于状态栏显示)
char *name;
} wm_workspace_t;
typedef struct wm_output_t {
wm_output_id_t id;
wm_rect_t geometry; // 输出完整几何bootstrap 后固定)
wm_rect_t workarea; // 可用区域排除面板等bootstrap 后固定)
wm_workspace_id_t current_workspace_id;
} wm_output_t;
// ========== 全局状态容器 ==========
typedef struct wm_state_t {
// 头文件级草案当前直接公开字段,便于讨论;实现阶段可再封装
wm_workspace_t *workspaces;
size_t workspace_count;
wm_output_t *outputs;
size_t output_count;
wm_window_t *windows;
size_t window_count;
size_t window_capacity;
// 公开的字段
wm_window_id_t *stack_order;
size_t stack_count;
size_t stack_capacity;
uint64_t generation;
bool initialized;
} wm_state_t;
// ========== API 函数 ==========
// 初始化/清理
// workspace/output 集合大小在 init 时一次性确定,之后不再变化。
void wm_state_init(
wm_state_t *state,
size_t workspace_count,
size_t output_count
);
void wm_state_shutdown(wm_state_t *state);
// ========== Workspace 访问 ==========
wm_workspace_t *wm_state_workspace(wm_state_t *state, wm_workspace_id_t id);
wm_workspace_t *wm_state_workspace_at(wm_state_t *state, size_t index);
size_t wm_state_workspace_count(const wm_state_t *state);
bool wm_state_workspace_valid(wm_state_t *state, wm_workspace_id_t id);
void wm_state_workspace_set_focused_window(
wm_state_t *state,
wm_workspace_id_t workspace_id,
wm_window_id_t window_id
);
// 查询某个 output 的所有 workspace只读视图
size_t wm_state_workspace_get_by_output(
const wm_state_t *state,
wm_output_id_t output_id,
wm_workspace_t **out_workspaces
);
// ========== Workspace 布局操作 ==========
/*
* 设置 workspace 的可用布局列表。
* 仅允许在 bootstrap / 初始化阶段调用;运行时不应修改。
*/
void wm_workspace_set_layouts(
wm_workspace_t *workspace,
const wm_layout_id_t *layouts,
size_t count
);
/*
* 获取 workspace 的可用布局列表。
* 该列表在 bootstrap 完成后保持不变。
*/
const wm_layout_id_t *
wm_workspace_get_layouts(const wm_workspace_t *workspace, size_t *out_count);
/*
* 切换到下一个布局(循环)
*/
bool wm_workspace_cycle_layout(wm_workspace_t *workspace);
/*
* 切换到指定索引的布局
*/
bool wm_workspace_set_layout_by_index(wm_workspace_t *workspace, size_t index);
/*
* 切换到指定 ID 的布局(如果该布局在可用列表中)
*/
bool wm_workspace_set_layout_by_id(
wm_workspace_t *workspace,
wm_layout_id_t id
);
// ========== Output 访问 ==========
wm_output_t *wm_state_output(wm_state_t *state, wm_output_id_t id);
wm_output_t *wm_state_output_at(wm_state_t *state, size_t index);
size_t wm_state_output_count(const wm_state_t *state);
bool wm_state_output_valid(wm_state_t *state, wm_output_id_t id);
/*
* 切换某个 output 当前显示的 workspace。
* 这不会修改 output 集合本身,也不会改变 workspace->output_id 绑定。
* 调用方必须保证目标 workspace 固定归属于该 output。
*/
void wm_state_output_set_current_workspace(
wm_state_t *state,
wm_output_id_t output_id,
wm_workspace_id_t workspace_id
);
// ========== Window 操作 ==========
wm_window_t *wm_state_window_add(wm_state_t *state, wm_window_id_t id);
wm_window_t *wm_state_window_get(wm_state_t *state, wm_window_id_t id);
wm_window_t *wm_state_window_at(wm_state_t *state, size_t index);
void wm_state_window_remove(wm_state_t *state, wm_window_id_t id);
size_t wm_state_window_count(const wm_state_t *state);
void wm_state_window_set_workspace(
wm_state_t *state,
wm_window_id_t window_id,
wm_workspace_id_t workspace_id
);
void wm_state_window_set_geometry_mode(
wm_state_t *state,
wm_window_id_t window_id,
wm_window_geometry_mode_t geometry_mode
);
void wm_state_window_set_floating(
wm_state_t *state,
wm_window_id_t window_id,
bool floating
);
void wm_state_window_set_sticky(
wm_state_t *state,
wm_window_id_t window_id,
bool sticky
);
void wm_state_window_set_urgent(
wm_state_t *state,
wm_window_id_t window_id,
bool urgent
);
void wm_state_window_set_fixed_size(
wm_state_t *state,
wm_window_id_t window_id,
bool fixed_size
);
void wm_state_window_set_skip_taskbar(
wm_state_t *state,
wm_window_id_t window_id,
bool skip_taskbar
);
void wm_state_window_set_float_rect(
wm_state_t *state,
wm_window_id_t window_id,
wm_rect_t float_rect
);
void wm_state_window_set_frame_rect(
wm_state_t *state,
wm_window_id_t window_id,
wm_rect_t frame_rect
);
bool wm_state_window_set_title(
wm_state_t *state,
wm_window_id_t window_id,
const char *title
);
bool wm_state_window_set_app_id(
wm_state_t *state,
wm_window_id_t window_id,
const char *app_id
);
bool wm_state_window_set_class(
wm_state_t *state,
wm_window_id_t window_id,
const char *class_name
);
bool wm_state_window_set_instance(
wm_state_t *state,
wm_window_id_t window_id,
const char *instance_name
);
// ========== 查询辅助函数 ==========
/*
* 判断工作区是否可见(某个 output 正在显示它)
*/
bool wm_state_workspace_is_visible(
const wm_state_t *state,
wm_workspace_id_t workspace_id
);
/*
* 判断窗口是否可见
* 窗口可见 iff
* 1. 若窗口是 sticky则只要求窗口未最小化
* 2. 否则,窗口的 workspace 固定归属某个 output
* 3. 且该 output 当前正显示这个 workspace
*/
bool wm_state_window_should_be_visible(
const wm_state_t *state,
const wm_window_t *window
);
/*
* 判断窗口是否在特定 output 上可见
*/
bool wm_state_window_should_be_visible_on_output(
const wm_state_t *state,
const wm_window_t *window,
const wm_output_t *output
);

View File

@@ -1,105 +0,0 @@
#pragma once
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
// ID 类型定义
// workspace/output/layout 在启动时分配固定数量,运行时不再增删
// window 由后端管理,使用后端原生 ID
typedef uint32_t wm_window_id_t;
typedef uint16_t wm_workspace_id_t;
typedef uint16_t wm_output_id_t;
typedef uint32_t wm_layout_id_t;
/*
* backend 归一化后的毫秒时间值。
* 它对齐 X11 / Wayland 常见的 32-bit ms 时间域,用于事件排序和短时差计算;
* 不是 wall-clock / Unix epoch基准也不保证可见因此允许回绕。
*/
typedef uint32_t wm_time_ms_t;
/*
* 逻辑键编码,使用 xkb keysym 值空间。
* 它表示“当前 keymap 下,这次按键对应的逻辑主键”,而不是平台原始扫描码。
*/
typedef uint32_t wm_keysym_t;
typedef uint32_t wm_modifier_mask_t;
/*
* 通用 32-bit RGBA 颜色值,编码为 0xRRGGBBAA。
* backend 若不支持 alpha可忽略最低 8 bit 或在提交平台 effect 时自行量化。
*/
typedef uint32_t wm_rgba32_t;
// 无效 ID 标记
#define WM_WINDOW_ID_INVALID ((wm_window_id_t)0)
#define WM_WORKSPACE_ID_INVALID ((wm_workspace_id_t) - 1)
#define WM_OUTPUT_ID_INVALID ((wm_output_id_t) - 1)
#define WM_LAYOUT_ID_INVALID ((wm_layout_id_t) - 1)
#define WM_KEYSYM_INVALID ((wm_keysym_t)0)
/*
* 归一化后的修饰键掩码:
* - backend 应在产出 wm_event_t 之前合并 left/right 变体
* - CapsLock / NumLock 这类锁定位不应进入该掩码
*/
typedef enum wm_modifier_bit_t {
WM_MOD_NONE = 0,
WM_MOD_SHIFT = 1u << 0,
WM_MOD_CTRL = 1u << 1,
WM_MOD_ALT = 1u << 2,
WM_MOD_SUPER = 1u << 3,
} wm_modifier_bit_t;
/*
* 归一化后的指针按键语义。
* backend 应把平台原始按钮码映射到这些稳定枚举值;
* 若需要保留平台原始值,可放在事件里的 raw_button 字段。
*/
typedef enum wm_button_t {
WM_BUTTON_NONE = 0,
WM_BUTTON_LEFT,
WM_BUTTON_MIDDLE,
WM_BUTTON_RIGHT,
WM_BUTTON_WHEEL_UP,
WM_BUTTON_WHEEL_DOWN,
WM_BUTTON_WHEEL_LEFT,
WM_BUTTON_WHEEL_RIGHT,
WM_BUTTON_BACK,
WM_BUTTON_FORWARD,
} wm_button_t;
typedef struct wm_point_t {
int32_t x;
int32_t y;
} wm_point_t;
typedef struct wm_rect_t {
int32_t x;
int32_t y;
int32_t width;
int32_t height;
} wm_rect_t;
/*
* 核心统一使用 geometry_mode 表示窗口的显示几何模式。
* 其中 WM_GEOMETRY_MINIMIZED 覆盖所有“被最小化/被图标化”的不可见状态:
* - 在 X11 上,它对应 ICCCM 的 IconicState
* - 来自 WM_CHANGE_STATE(Iconic) 的请求也应折叠到这个模式
*
* 最小核心不单独暴露 "iconic" 枚举,避免与 minimized 形成两套并行语义。
*/
typedef enum wm_window_geometry_mode_t {
WM_GEOMETRY_NORMAL,
WM_GEOMETRY_MAXIMIZED,
WM_GEOMETRY_FULLSCREEN,
WM_GEOMETRY_MINIMIZED,
} wm_window_geometry_mode_t;
typedef enum wm_focus_direction_t {
WM_FOCUS_PREV,
WM_FOCUS_NEXT,
} wm_focus_direction_t;
typedef enum wm_cross_output_policy_t {
WM_CROSS_OUTPUT_KEEP_WORKSPACE,
WM_CROSS_OUTPUT_MOVE_TO_TARGET_WORKSPACE,
} wm_cross_output_policy_t;

View File

@@ -1,104 +0,0 @@
#ifndef ZDWM_CONFIG_H
#define ZDWM_CONFIG_H
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <zdwm/layout.h>
#include <zdwm/rules.h>
#include <zdwm/types.h>
#if defined(__cplusplus)
extern "C" {
#endif
#define ZDWM_CONFIG_ABI_VERSION 1u
typedef struct zdwm_config_builder_t zdwm_config_builder_t;
typedef struct zdwm_builtin_layouts_t {
zdwm_layout_fn tile;
zdwm_layout_fn monocle;
} zdwm_builtin_layouts_t;
typedef struct zdwm_api_t {
uint32_t abi_version;
zdwm_builtin_layouts_t builtin_layouts;
/**
* 用户调用这个函数注册布局算法
* @param builder 配置构建上下文
* @param name 布局算法名称,不能为 nullptr
* @param symbol 布局算法标志符号,用于在状态栏中显示,不能为 nullptr
* @param description 布局算法描述,可以为 nullptr
* @param fn 布局算法,可以为 nullptr ,如果为 nullptr 则代表不自动布局
* @returns 返回布局算法对应的布局 id ,如果注册失败,返回
* ZDWM_LAYOUT_ID_INVALID
*/
zdwm_layout_id_t (*register_layout)(
zdwm_config_builder_t *builder,
const char *name,
const char *symbol,
const char *description,
zdwm_layout_fn fn
);
/**
* 用户调用这个函数添加 output 中的工作空间
* @param builder 配置构建上下文
* @param output_index 该工作空间归属的 output 索引
* @param name 工作空间名称,用于状态栏显示,不能为 nullptr
* @param layout_ids 该工作空间支持的布局算法列表
* @param layout_count 支持的布局算法总数
* @param initial_layout_id 初始布局算法 id
* @returns 返回工作空间 id 。如果注册失败,返回 ZDWM_WORKSPACE_ID_INVALID
*/
zdwm_workspace_id_t (*define_workspace)(
zdwm_config_builder_t *builder,
size_t output_index,
const char *name,
const zdwm_layout_id_t *layout_ids,
size_t layout_count,
zdwm_layout_id_t initial_layout_id
);
/**
* 用户调用这个函数添加窗口规则
* @param builder 配置构建上下文
* @param match 窗口匹配条件,不能为 nullptr
* @param action 匹配后执行的动作,不能为 nullptr
* @returns 添加成功返回 true ,不合法时返回 false
* @details 不合法的情况match 所有字段全为 nullptr
* 或 action 无任何动作;或 action 指定的 workspace 不存在
*/
bool (*add_rule)(
zdwm_config_builder_t *builder,
const zdwm_rule_match_t *match,
const zdwm_rule_action_t *action
);
} zdwm_api_t;
/**
* 用户配置入口函数类型定义
* @param api 用户自定义时可以使用的 api 接口及数据api
* 中的函数和信息只能在函数内部使用,如果用户将其保存后在函数外使用是未定义行为
* @param builder 配置构建上下文
* @param outputs 屏幕信息列表
* @param output_count 屏幕个数
* @returns 用户配置成功返回 true ,配置失败返回 false
*/
typedef bool zdwm_config_setup_fn(
const zdwm_api_t *api,
zdwm_config_builder_t *builder,
const zdwm_output_info_t *outputs,
size_t output_count
);
/* 用户在自己的动态链接库配置中必须实现该函数 */
extern zdwm_config_setup_fn setup;
#if defined(__cplusplus)
}
#endif
#endif /* ZDWM_CONFIG_H */

View File

@@ -1,64 +0,0 @@
#ifndef ZDWM_LAYOUT_H
#define ZDWM_LAYOUT_H
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <stdlib.h>
#include <zdwm/types.h>
#if defined(__cplusplus)
extern "C" {
#endif
typedef struct zdwm_layout_ctx_t {
zdwm_workspace_id_t workspace_id;
zdwm_window_id_t focused_window_id;
zdwm_rect_t workarea;
const zdwm_window_id_t *window_ids;
size_t window_count;
} zdwm_layout_ctx_t;
typedef struct zdwm_layout_item_t {
zdwm_window_id_t window_id;
zdwm_rect_t rect;
} zdwm_layout_item_t;
typedef struct zdwm_layout_result_t {
zdwm_layout_item_t *items;
size_t item_count;
size_t item_capacity;
} zdwm_layout_result_t;
typedef bool (*zdwm_layout_fn)(
const zdwm_layout_ctx_t *ctx,
zdwm_layout_result_t *out
);
static inline void zdwm_layout_result_reset(zdwm_layout_result_t *result) {
if (!result) abort();
result->item_count = 0;
}
static inline void
zdwm_layout_result_push(zdwm_layout_result_t *result, zdwm_layout_item_t item) {
if (!result) abort();
if (result->item_count == result->item_capacity) {
size_t capacity = result->item_capacity ? result->item_capacity * 2u : 4u;
zdwm_layout_item_t *items =
realloc(result->items, capacity * sizeof(*result->items));
if (!items) abort();
result->items = items;
result->item_capacity = capacity;
}
result->items[result->item_count++] = item;
}
#if defined(__cplusplus)
}
#endif
#endif /* ZDWM_LAYOUT_H */

View File

@@ -1,38 +0,0 @@
#ifndef ZDWM_RULES_H
#define ZDWM_RULES_H
#include <zdwm/types.h>
#if defined(__cplusplus)
extern "C" {
#endif
/**
* @brief 窗口匹配条件
* @details 仅匹配不为 nullptr 的字段,多个字段按且规则匹配
*/
typedef struct zdwm_rule_match_t {
const char *app_id;
const char *role;
const char *class_name;
const char *instance_name;
} zdwm_rule_match_t;
typedef struct zdwm_rule_action_t {
/**
* 目标 workspace id
* - 若规则不想指定 workspace ,则将其设为 ZDWM_WORKSPACE_ID_INVALID
*/
zdwm_workspace_id_t workspace;
bool switch_to_workspace; /* 自动切换到目标 workspace仅 true 时生效) */
bool fullscreen; /* 强制全屏(仅 true 时生效) */
bool maximize; /* 强制最大化(仅 true 时生效) */
bool floating; /* 强制浮动(仅 true 时生效) */
} zdwm_rule_action_t;
#if defined(__cplusplus)
}
#endif
#endif /* ZDWM_RULES_H */

View File

@@ -1,34 +0,0 @@
#ifndef ZDWM_TYPES_H
#define ZDWM_TYPES_H
#include <stdint.h>
#if defined(__cplusplus)
extern "C" {
#endif
typedef uint32_t zdwm_layout_id_t;
typedef uint32_t zdwm_window_id_t;
typedef uint32_t zdwm_workspace_id_t;
#define ZDWM_LAYOUT_ID_INVALID ((zdwm_layout_id_t)UINT32_MAX)
#define ZDWM_WINDOW_ID_INVALID ((zdwm_window_id_t)0)
#define ZDWM_WORKSPACE_ID_INVALID ((zdwm_workspace_id_t)UINT32_MAX)
typedef struct zdwm_rect_t {
int32_t x;
int32_t y;
int32_t width;
int32_t height;
} zdwm_rect_t;
typedef struct zdwm_output_info_t {
const char *name;
zdwm_rect_t geometry;
} zdwm_output_info_t;
#if defined(__cplusplus)
}
#endif
#endif /* ZDWM_TYPES_H */

View File

@@ -1,16 +0,0 @@
# -*- mode: yaml; -*-
---
BasedOnStyle: Google
IndentWidth: 2
ConstructorInitializerIndentWidth: 2
ContinuationIndentWidth: 2
---
Language: C
DerivePointerAlignment: false
PointerAlignment: Right
---
Language: Cpp
DerivePointerAlignment: false
PointerAlignment: Right
---

View File

@@ -78,9 +78,9 @@ void raise_or_run(const user_action_arg_t *arg) {
bool monitor_changed = client->monitor != wm.current_monitor;
bool tag_changed = client->tags != client->monitor->selected_tag->mask;
if (monitor_changed || tag_changed) {
point_t point = monitor_changed
? monitor_get_restore_cursor_point(client->monitor)
: xcursor_query_pointer_position();
point_t point = monitor_changed ? monitor_get_restore_cursor_point(
client->monitor)
: xcursor_query_pointer_position();
wm_ignore_enter_notify_at_point(point);
}
wm_set_current_monitor(client->monitor, true);

View File

@@ -1 +0,0 @@
../../.clang-format

View File

@@ -1,55 +0,0 @@
#include "backend/output_utils.h"
#include <stddef.h>
#include <stdint.h>
#include "base/memory.h"
#include "core/backend.h"
#include "core/types.h"
static inline int32_t right(rect_t a) { return a.x + a.width; }
static inline int32_t bottom(rect_t a) { return a.y + a.height; }
static inline bool fully_contains(rect_t outer, rect_t inner) {
return inner.x >= outer.x && inner.y >= outer.y &&
right(inner) <= right(outer) && bottom(inner) <= bottom(outer);
}
backend_detect_t *
output_remove_duplication(const output_info_t *output, const size_t count) {
if (!output || count < 1) return nullptr;
bool *remove = p_new(bool, count);
for (int32_t i = (int32_t)count - 1; i >= 0; i--) {
if (remove[i]) continue;
for (int32_t j = i - 1; j >= 0; j--) {
if (remove[j]) continue;
if (fully_contains(output[j].geometry, output[i].geometry)) {
remove[i] = true;
} else if (fully_contains(output[i].geometry, output[j].geometry)) {
remove[j] = true;
}
}
}
int32_t amount = 0;
for (size_t i = 0; i < count; i++) {
if (!remove[i]) amount++;
}
if (!amount) {
p_delete(&remove);
return nullptr;
}
output_info_t *list = p_new(output_info_t, amount);
amount = 0;
for (size_t i = 0; i < count; i++) {
if (remove[i]) continue;
list[amount++] = output[i];
}
p_delete(&remove);
backend_detect_t *detect = p_new(backend_detect_t, 1);
detect->outputs = list;
detect->output_count = amount;
return detect;
}

View File

@@ -1,9 +0,0 @@
#pragma once
#include <stddef.h>
#include "core/backend.h"
#include "core/types.h"
backend_detect_t *
output_remove_duplication(const output_info_t *output, const size_t count);

View File

@@ -1,309 +0,0 @@
#include "core/backend.h"
#include <stddef.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <xcb/randr.h>
#include <xcb/xcb.h>
#include <xcb/xcb_aux.h>
#include <xcb/xfixes.h>
#include <xcb/xinerama.h>
#include <xcb/xproto.h>
#include "backend/output_utils.h"
#include "base/log.h"
#include "base/macros.h"
#include "base/memory.h"
#include "core/types.h"
#include "internal.h"
typedef struct atom_item_t {
const char *name;
size_t len;
xcb_atom_t *atom;
} atom_item_t;
static void atoms_init(backend_t *backend) {
xcb_connection_t *conn = backend->conn;
atoms_t *atoms = &backend->atoms;
#define ATOM_ITEM(name) {#name, sizeof(#name) - 1, &atoms->name},
atom_item_t atom_list[] = {ATOM_LIST(ATOM_ITEM)};
#undef ATOM_ITEM
xcb_intern_atom_cookie_t cookies[countof(atom_list)];
for (size_t i = 0; i < countof(atom_list); ++i) {
const atom_item_t *atom = &atom_list[i];
cookies[i] = xcb_intern_atom_unchecked(conn, false, atom->len, atom->name);
}
xcb_intern_atom_reply_t *reply = nullptr;
for (size_t i = 0; i < countof(atom_list); ++i) {
reply = xcb_intern_atom_reply(conn, cookies[i], nullptr);
if (!reply) continue;
atom_item_t *atom = &atom_list[i];
*atom->atom = reply->atom;
p_delete(&reply);
}
}
backend_t *backend_create(const char *display_name) {
int screen_num = 0;
xcb_connection_t *conn = xcb_connect(display_name, &screen_num);
int xcb_conn_error = xcb_connection_has_error(conn);
if (xcb_conn_error) {
fatal("cannot open display %s, error %d", display_name, xcb_conn_error);
}
xcb_screen_t *screen = xcb_aux_get_screen(conn, screen_num);
if (!screen) fatal("cannot get screen info");
xcb_window_t root = screen->root;
uint32_t mask = XCB_CW_EVENT_MASK;
const xcb_params_cw_t params = {
.event_mask = XCB_EVENT_MASK_SUBSTRUCTURE_REDIRECT,
};
xcb_void_cookie_t cookie =
xcb_aux_change_window_attributes_checked(conn, root, mask, &params);
if (xcb_request_check(conn, cookie)) {
fatal(
"another window manager is already running (cannot select "
"SubstructureRedirect)"
);
}
backend_t *backend = p_new(backend_t, 1);
backend->conn = conn;
backend->screen = screen;
backend->screenp = screen_num;
xcb_prefetch_extension_data(conn, &xcb_xfixes_id);
const xcb_query_extension_reply_t *query =
xcb_get_extension_data(conn, &xcb_xfixes_id);
if (query && query->present) {
xcb_xfixes_query_version_cookie_t cookie = xcb_xfixes_query_version(
conn,
XCB_XFIXES_MAJOR_VERSION,
XCB_XFIXES_MINOR_VERSION
);
xcb_xfixes_query_version_reply_t *reply =
xcb_xfixes_query_version_reply(conn, cookie, nullptr);
if (reply) {
backend->have_xfixes = true;
p_delete(&reply);
}
} else {
backend->have_xfixes = false;
}
atoms_init(backend);
return backend;
}
void backend_destroy(backend_t *backend) {
if (!backend) return;
xcb_disconnect(backend->conn);
backend->conn = nullptr;
free(backend);
}
static bool detect_monitor_by_randr(
const backend_t *backend,
output_info_t **outputs,
size_t *count
) {
xcb_prefetch_extension_data(backend->conn, &xcb_randr_id);
const xcb_query_extension_reply_t *query =
xcb_get_extension_data(backend->conn, &xcb_randr_id);
if (!query || !query->present) return false;
xcb_randr_query_version_cookie_t cookie = xcb_randr_query_version(
backend->conn,
XCB_RANDR_MAJOR_VERSION,
XCB_RANDR_MINOR_VERSION
);
xcb_randr_query_version_reply_t *reply =
xcb_randr_query_version_reply(backend->conn, cookie, nullptr);
if (!reply) return false;
/* xcb_randr_get_monitors 接口是 1.5 引入的 */
uint32_t major_version = reply->major_version;
uint32_t minor_version = reply->minor_version;
if (major_version < 1 || (major_version == 1 && minor_version < 5)) {
p_delete(&reply);
return false;
}
p_delete(&reply);
xcb_randr_get_monitors_cookie_t monitors_cookie =
xcb_randr_get_monitors(backend->conn, backend->screen->root, 1);
xcb_randr_get_monitors_reply_t *monitors_reply =
xcb_randr_get_monitors_reply(backend->conn, monitors_cookie, nullptr);
if (!monitors_reply) {
warn("RandR get monitor failed");
return false;
}
int len = xcb_randr_get_monitors_monitors_length(monitors_reply);
if (len <= 0) {
p_delete(&monitors_reply);
return false;
}
output_info_t *output_list = p_new(output_info_t, len);
xcb_randr_monitor_info_iterator_t iter =
xcb_randr_get_monitors_monitors_iterator(monitors_reply);
for (int i = 0; iter.rem; xcb_randr_monitor_info_next(&iter), i++) {
output_info_t *output = &output_list[i];
output->geometry.x = iter.data->x;
output->geometry.y = iter.data->y;
output->geometry.width = iter.data->width;
output->geometry.height = iter.data->height;
xcb_get_atom_name_cookie_t name_cookie =
xcb_get_atom_name_unchecked(backend->conn, iter.data->name);
xcb_get_atom_name_reply_t *name_reply =
xcb_get_atom_name_reply(backend->conn, name_cookie, nullptr);
if (name_reply) {
char *name = xcb_get_atom_name_name(name_reply);
int length = xcb_get_atom_name_name_length(name_reply);
output->name = p_strndup(name, length);
p_delete(&name_reply);
}
}
p_delete(&monitors_reply);
if (count) *count = (size_t)len;
if (outputs) {
*outputs = output_list;
} else {
for (size_t i = 0; i < (size_t)len; i++) p_delete(&output_list[i].name);
p_delete(&output_list);
}
return true;
}
static bool detect_monitor_by_xinerama(
const backend_t *backend,
output_info_t **outputs,
size_t *count
) {
xcb_prefetch_extension_data(backend->conn, &xcb_xinerama_id);
const xcb_query_extension_reply_t *query =
xcb_get_extension_data(backend->conn, &xcb_xinerama_id);
if (!query || !query->present) return false;
xcb_xinerama_query_version_cookie_t cookie = xcb_xinerama_query_version(
backend->conn,
XCB_XINERAMA_MAJOR_VERSION,
XCB_XINERAMA_MINOR_VERSION
);
xcb_xinerama_query_version_reply_t *version_reply =
xcb_xinerama_query_version_reply(backend->conn, cookie, nullptr);
if (!version_reply) return false;
p_delete(&version_reply);
xcb_xinerama_is_active_cookie_t active_cookie =
xcb_xinerama_is_active(backend->conn);
xcb_xinerama_is_active_reply_t *active_reply =
xcb_xinerama_is_active_reply(backend->conn, active_cookie, nullptr);
bool active = active_reply && active_reply->state;
if (active_reply) p_delete(&active_reply);
if (!active) return false;
xcb_xinerama_query_screens_cookie_t screens_cookie =
xcb_xinerama_query_screens(backend->conn);
xcb_xinerama_query_screens_reply_t *screens_reply =
xcb_xinerama_query_screens_reply(backend->conn, screens_cookie, nullptr);
if (!screens_reply) return false;
int len = xcb_xinerama_query_screens_screen_info_length(screens_reply);
if (len <= 0) {
p_delete(&screens_reply);
return false;
}
xcb_xinerama_screen_info_t *screen_info =
xcb_xinerama_query_screens_screen_info(screens_reply);
if (!screen_info) {
p_delete(&screens_reply);
return false;
}
output_info_t *output_list = p_new(output_info_t, len);
for (int i = 0; i < len; i++) {
output_info_t *output = &output_list[i];
output->geometry.x = screen_info[i].x_org;
output->geometry.y = screen_info[i].y_org;
output->geometry.width = screen_info[i].width;
output->geometry.height = screen_info[i].height;
}
p_delete(&screens_reply);
if (count) *count = (size_t)len;
if (outputs) {
*outputs = output_list;
} else {
for (size_t i = 0; i < (size_t)len; i++) p_delete(&output_list[i].name);
p_delete(&output_list);
}
return true;
}
backend_detect_t *backend_detect(backend_t *backend) {
output_info_t *outputs = nullptr;
size_t count = 0;
if (detect_monitor_by_randr(backend, &outputs, &count)) {
backend_detect_t *detect = output_remove_duplication(outputs, count);
p_delete(&outputs);
return detect;
}
if (detect_monitor_by_xinerama(backend, &outputs, &count)) {
backend_detect_t *detect = output_remove_duplication(outputs, count);
p_delete(&outputs);
return detect;
}
output_info_t *output = p_new(output_info_t, 1);
output->geometry.x = 0;
output->geometry.y = 0;
output->geometry.width = backend->screen->width_in_pixels;
output->geometry.height = backend->screen->height_in_pixels;
backend_detect_t *detect = p_new(backend_detect_t, 1);
detect->outputs = output;
detect->output_count = 1;
return detect;
}
void backend_detect_destroy(backend_detect_t *detect) {
if (!detect) return;
for (size_t i = 0; i < detect->output_count; i++) {
p_delete(&detect->outputs[i].name);
}
p_delete(&detect->outputs);
free(detect);
}
bool backend_apply_effect(
backend_t *backend,
const effect_t *effects,
size_t effect_count
) {
/* TODO: */
return true;
}

View File

@@ -1,218 +0,0 @@
#include "core/event.h"
#include <stdio.h>
#include <xcb/xcb.h>
#include <xcb/xcb_event.h>
#include <xcb/xcb_icccm.h>
#include <xcb/xproto.h>
#include "backend/x11/window.h"
#include "base/memory.h"
#include "core/backend.h"
#include "core/types.h"
#include "core/window.h"
#include "internal.h"
static window_state_t *derive_window_states(
const atoms_t *atoms,
const xcb_atom_t *raw,
uint32_t count,
size_t *out_count
) {
if (!raw || count == 0) {
*out_count = 0;
return nullptr;
}
window_state_t *buf = p_new(window_state_t, count);
size_t valid = 0;
for (uint32_t i = 0; i < count; i++) {
window_state_t s = atom_to_window_state(atoms, raw[i]);
if (s != (window_state_t)-1) buf[valid++] = s;
}
if (valid == 0) {
p_delete(&buf);
*out_count = 0;
return nullptr;
}
*out_count = valid;
return buf;
}
static bool derive_skip_taskbar(
const atoms_t *atoms,
const xcb_atom_t *raw,
uint32_t count
) {
for (uint32_t i = 0; i < count; i++)
if (raw[i] == atoms->_NET_WM_STATE_SKIP_TASKBAR) return true;
return false;
}
static window_geometry_mode_t geometry_mode_from_hints(
const xcb_icccm_wm_hints_t *hints
) {
if (hints->initial_state == XCB_ICCCM_WM_STATE_ICONIC)
return ZDWM_GEOMETRY_MINIMIZED;
return ZDWM_GEOMETRY_NORMAL;
}
static window_geometry_mode_t geometry_mode_from_states(
const atoms_t *atoms,
const xcb_atom_t *state_atoms,
uint32_t state_count
) {
for (uint32_t i = 0; i < state_count; i++) {
if (state_atoms[i] == atoms->_NET_WM_STATE_FULLSCREEN)
return ZDWM_GEOMETRY_FULLSCREEN;
if (state_atoms[i] == atoms->_NET_WM_STATE_MAXIMIZED_VERT ||
state_atoms[i] == atoms->_NET_WM_STATE_MAXIMIZED_HORZ)
return ZDWM_GEOMETRY_MAXIMIZED;
}
return ZDWM_GEOMETRY_NORMAL;
}
static bool handle_map_request(
backend_t *backend,
event_t *event,
const xcb_map_request_event_t *xcb_event
) {
xcb_window_t window = xcb_event->window;
event_reset(event);
event->type = ZDWM_EVENT_WINDOW_MAP_REQUEST;
window_map_request_event_t *ev = &event->as.window_map_request;
ev->window = (window_id_t)window;
ev->transient_for = window_get_transient_for(backend, window);
ev->geometry_mode = ZDWM_GEOMETRY_NORMAL;
xcb_get_window_attributes_reply_t *wa =
window_get_attributes(backend, window);
if (!wa) return false;
ev->override_redirect = (bool)wa->override_redirect;
p_delete(&wa);
xcb_icccm_wm_hints_t wm_hints = {0};
if (window_get_wm_hints(backend, window, &wm_hints)) {
ev->urgent = (bool)xcb_icccm_wm_hints_get_urgency(&wm_hints);
ev->geometry_mode = geometry_mode_from_hints(&wm_hints);
}
if (!window_get_fixed_size(backend, window, &ev->fixed_size)) return false;
if (!window_get_geometry(backend, window, &ev->rect)) return false;
const atoms_t *atoms = &backend->atoms;
xcb_atom_t *state_atoms = nullptr;
uint32_t state_count = 0;
if (!window_get_atom_array(
backend,
window,
atoms->_NET_WM_STATE,
&state_atoms,
&state_count
)) {
return false;
}
if (ev->geometry_mode == ZDWM_GEOMETRY_NORMAL && state_atoms) {
ev->geometry_mode =
geometry_mode_from_states(atoms, state_atoms, state_count);
}
if (state_atoms) {
ev->skip_taskbar = derive_skip_taskbar(atoms, state_atoms, state_count);
ev->props.states = derive_window_states(
atoms,
state_atoms,
state_count,
&ev->props.state_count
);
}
p_delete(&state_atoms);
window_layer_props_t *props = &ev->props;
if (!window_get_types(backend, window, &props->types, &props->type_count)) {
p_delete(&props->states);
return false;
}
ev->metadata.role = window_get_role(backend, window);
ev->metadata.title = window_get_title(backend, window);
window_get_class(
backend,
window,
&ev->metadata.class_name,
&ev->metadata.instance_name
);
return true;
}
static bool handle_unmap_notify(
backend_t *backend,
event_t *event,
const xcb_unmap_notify_event_t *xcb_event
) {
event_reset(event);
event->type = ZDWM_EVENT_WINDOW_REMOVE;
/* TODO: */
return true;
}
static bool handle_destroy_notify(
backend_t *backend,
event_t *event,
const xcb_destroy_notify_event_t *xcb_event
) {
event_reset(event);
event->type = ZDWM_EVENT_WINDOW_REMOVE;
/* TODO: */
return true;
}
static bool handle_configure_request(
backend_t *backend,
event_t *event,
const xcb_configure_request_event_t *xcb_event
) {
event_reset(event);
event->type = ZDWM_EVENT_CONFIGURE_REQUEST;
/* TODO: */
return true;
}
bool backend_next_event(backend_t *backend, event_t *event) {
if (!backend || !backend->conn || !event) return false;
for (;;) {
xcb_generic_event_t *raw_event = xcb_wait_for_event(backend->conn);
if (!raw_event) return false;
uint8_t response_type = XCB_EVENT_RESPONSE_TYPE(raw_event);
bool handled = false;
printf("xcb event type: %u\n", response_type);
switch (response_type) {
#define EVENT(type, handler) \
case type: \
handled = handler(backend, event, (void *)raw_event); \
break
EVENT(XCB_MAP_REQUEST, handle_map_request);
EVENT(XCB_UNMAP_NOTIFY, handle_unmap_notify);
EVENT(XCB_DESTROY_NOTIFY, handle_destroy_notify);
EVENT(XCB_CONFIGURE_REQUEST, handle_configure_request);
#undef EVENT
default:
break;
}
p_delete(&raw_event);
if (handled) return true;
event_reset(event);
}
}

View File

@@ -1,52 +0,0 @@
#pragma once
#include <xcb/xcb.h>
#include <xcb/xproto.h>
#define ATOM_LIST(X) \
X(COMPOUND_TEXT) \
X(UTF8_STRING) \
\
X(WM_WINDOW_ROLE) \
X(WM_NAME) \
X(_NET_WM_NAME) \
\
X(_NET_WM_STATE) \
X(_NET_WM_STATE_FULLSCREEN) \
X(_NET_WM_STATE_ABOVE) \
X(_NET_WM_STATE_STICKY) \
X(_NET_WM_STATE_MODAL) \
X(_NET_WM_STATE_SKIP_TASKBAR) \
X(_NET_WM_STATE_MAXIMIZED_VERT) \
X(_NET_WM_STATE_MAXIMIZED_HORZ) \
\
X(_NET_WM_WINDOW_TYPE) \
X(_NET_WM_WINDOW_TYPE_NORMAL) \
X(_NET_WM_WINDOW_TYPE_DESKTOP) \
X(_NET_WM_WINDOW_TYPE_DOCK) \
X(_NET_WM_WINDOW_TYPE_TOOLBAR) \
X(_NET_WM_WINDOW_TYPE_DIALOG) \
X(_NET_WM_WINDOW_TYPE_UTILITY) \
X(_NET_WM_WINDOW_TYPE_SPLASH) \
X(_NET_WM_WINDOW_TYPE_MENU) \
X(_NET_WM_WINDOW_TYPE_DROPDOWN_MENU) \
X(_NET_WM_WINDOW_TYPE_POPUP_MENU) \
X(_NET_WM_WINDOW_TYPE_TOOLTIP) \
X(_NET_WM_WINDOW_TYPE_COMBO) \
X(_NET_WM_WINDOW_TYPE_DND) \
X(_NET_WM_WINDOW_TYPE_NOTIFICATION)
typedef struct atoms_t {
#define DECLARATION_ATOM(name) xcb_atom_t name;
ATOM_LIST(DECLARATION_ATOM);
#undef DECLARATION_ATOM
} atoms_t;
struct backend_t {
xcb_connection_t *conn;
xcb_screen_t *screen;
int screenp;
bool have_xfixes;
atoms_t atoms;
};

View File

@@ -1,245 +0,0 @@
#include "window.h"
#include <stddef.h>
#include <stdint.h>
#include <xcb/xcb.h>
#include <xcb/xcb_icccm.h>
#include <xcb/xproto.h>
#include "base/memory.h"
#include "core/backend.h"
#include "internal.h"
static char *window_get_text_property(
backend_t *backend,
xcb_window_t window,
xcb_atom_t property
) {
xcb_get_property_cookie_t cookie = xcb_get_property_unchecked(
backend->conn,
false,
window,
property,
XCB_ATOM_ANY,
0,
UINT32_MAX
);
xcb_get_property_reply_t *reply =
xcb_get_property_reply(backend->conn, cookie, nullptr);
if (!reply) return nullptr;
int length = xcb_get_property_value_length(reply);
char *value = (char *)xcb_get_property_value(reply);
char *text = nullptr;
if (length && reply->format == 8 &&
(reply->type == XCB_ATOM_STRING ||
reply->type == backend->atoms.UTF8_STRING ||
reply->type == backend->atoms.COMPOUND_TEXT)) {
text = p_strndup(value, (size_t)length);
}
p_delete(&reply);
return text;
}
char *window_get_role(backend_t *backend, xcb_window_t window) {
xcb_atom_t property = backend->atoms.WM_WINDOW_ROLE;
return window_get_text_property(backend, window, property);
}
char *window_get_title(backend_t *backend, xcb_window_t window) {
xcb_atom_t property = backend->atoms._NET_WM_NAME;
char *name = window_get_text_property(backend, window, property);
if (!name) {
property = backend->atoms.WM_NAME;
name = window_get_text_property(backend, window, property);
}
return name;
}
void window_get_class(
backend_t *backend,
xcb_window_t window,
char **class_out,
char **instance_out
) {
xcb_icccm_get_wm_class_reply_t prop;
xcb_get_property_cookie_t cookie =
xcb_icccm_get_wm_class_unchecked(backend->conn, window);
if (!xcb_icccm_get_wm_class_reply(backend->conn, cookie, &prop, nullptr)) {
return;
}
if (class_out) *class_out = p_strdup_nullable(prop.class_name);
if (instance_out) *instance_out = p_strdup_nullable(prop.instance_name);
xcb_icccm_get_wm_class_reply_wipe(&prop);
}
xcb_window_t window_get_transient_for(backend_t *backend, xcb_window_t window) {
xcb_connection_t *conn = backend->conn;
xcb_get_property_cookie_t cookie =
xcb_icccm_get_wm_transient_for_unchecked(conn, window);
xcb_window_t trans_for = XCB_WINDOW_NONE;
if (xcb_icccm_get_wm_transient_for_reply(conn, cookie, &trans_for, nullptr)) {
return trans_for;
}
return XCB_WINDOW_NONE;
}
xcb_get_window_attributes_reply_t *
window_get_attributes(backend_t *backend, xcb_window_t window) {
xcb_get_window_attributes_cookie_t cookie =
xcb_get_window_attributes(backend->conn, window);
return xcb_get_window_attributes_reply(backend->conn, cookie, nullptr);
}
static window_type_t
atom_to_window_type(const atoms_t *atoms, xcb_atom_t atom) {
if (atom == atoms->_NET_WM_WINDOW_TYPE_DESKTOP)
return ZDWM_WINDOW_TYPE_DESKTOP;
if (atom == atoms->_NET_WM_WINDOW_TYPE_DOCK) return ZDWM_WINDOW_TYPE_DOCK;
if (atom == atoms->_NET_WM_WINDOW_TYPE_TOOLBAR)
return ZDWM_WINDOW_TYPE_TOOLBAR;
if (atom == atoms->_NET_WM_WINDOW_TYPE_DIALOG) return ZDWM_WINDOW_TYPE_DIALOG;
if (atom == atoms->_NET_WM_WINDOW_TYPE_UTILITY)
return ZDWM_WINDOW_TYPE_UTILITY;
if (atom == atoms->_NET_WM_WINDOW_TYPE_SPLASH) return ZDWM_WINDOW_TYPE_SPLASH;
if (atom == atoms->_NET_WM_WINDOW_TYPE_MENU) return ZDWM_WINDOW_TYPE_MENU;
if (atom == atoms->_NET_WM_WINDOW_TYPE_DROPDOWN_MENU)
return ZDWM_WINDOW_TYPE_DROPDOWN_MENU;
if (atom == atoms->_NET_WM_WINDOW_TYPE_POPUP_MENU)
return ZDWM_WINDOW_TYPE_POPUP_MENU;
if (atom == atoms->_NET_WM_WINDOW_TYPE_TOOLTIP)
return ZDWM_WINDOW_TYPE_TOOLTIP;
if (atom == atoms->_NET_WM_WINDOW_TYPE_COMBO) return ZDWM_WINDOW_TYPE_COMBO;
if (atom == atoms->_NET_WM_WINDOW_TYPE_DND) return ZDWM_WINDOW_TYPE_DND;
if (atom == atoms->_NET_WM_WINDOW_TYPE_NOTIFICATION)
return ZDWM_WINDOW_TYPE_NOTIFICATION;
return ZDWM_WINDOW_TYPE_NORMAL;
}
bool window_get_types(
backend_t *backend,
xcb_window_t window,
window_type_t **types,
size_t *count
) {
uint32_t atom_count = 0;
xcb_atom_t *atoms = nullptr;
if (!window_get_atom_array(
backend,
window,
backend->atoms._NET_WM_WINDOW_TYPE,
&atoms,
&atom_count
)) {
return false;
}
if (!atoms) {
*types = nullptr;
*count = 0;
return true;
}
*count = atom_count;
*types = p_new(window_type_t, atom_count);
for (uint32_t i = 0; i < atom_count; i++) {
(*types)[i] = atom_to_window_type(&backend->atoms, atoms[i]);
}
p_delete(&atoms);
return true;
}
bool window_get_fixed_size(backend_t *backend, xcb_window_t window, bool *out) {
xcb_size_hints_t hints = {0};
xcb_connection_t *conn = backend->conn;
xcb_get_property_cookie_t cookie =
xcb_icccm_get_wm_normal_hints_unchecked(conn, window);
if (!xcb_icccm_get_wm_normal_hints_reply(conn, cookie, &hints, nullptr)) {
return false;
}
if ((hints.flags & XCB_ICCCM_SIZE_HINT_P_MIN_SIZE) &&
(hints.flags & XCB_ICCCM_SIZE_HINT_P_MAX_SIZE)) {
*out = hints.min_width == hints.max_width &&
hints.min_height == hints.max_height;
} else {
*out = false;
}
return true;
}
bool window_get_geometry(backend_t *backend, xcb_window_t window, rect_t *out) {
xcb_get_geometry_cookie_t cookie = xcb_get_geometry(backend->conn, window);
xcb_get_geometry_reply_t *reply =
xcb_get_geometry_reply(backend->conn, cookie, nullptr);
if (!reply) return false;
out->x = reply->x;
out->y = reply->y;
out->width = reply->width;
out->height = reply->height;
p_delete(&reply);
return true;
}
bool window_get_atom_array(
backend_t *backend,
xcb_window_t window,
xcb_atom_t property,
xcb_atom_t **out_atoms,
uint32_t *out_len
) {
xcb_get_property_cookie_t cookie = xcb_get_property_unchecked(
backend->conn,
false,
window,
property,
XCB_ATOM_ATOM,
0,
UINT32_MAX
);
xcb_get_property_reply_t *reply =
xcb_get_property_reply(backend->conn, cookie, nullptr);
if (!reply) return false;
if (reply->type == XCB_ATOM_ATOM && reply->format == 32 && reply->value_len) {
*out_len = reply->value_len;
*out_atoms =
p_copy((xcb_atom_t *)xcb_get_property_value(reply), reply->value_len);
} else {
*out_len = 0;
*out_atoms = nullptr;
}
p_delete(&reply);
return true;
}
bool window_get_wm_hints(
backend_t *backend,
xcb_window_t window,
xcb_icccm_wm_hints_t *out
) {
xcb_get_property_cookie_t cookie =
xcb_icccm_get_wm_hints_unchecked(backend->conn, window);
if (!xcb_icccm_get_wm_hints_reply(backend->conn, cookie, out, nullptr)) {
p_clear(out, 1);
return false;
}
return true;
}
window_state_t atom_to_window_state(const atoms_t *atoms, xcb_atom_t atom) {
if (atom == atoms->_NET_WM_STATE_FULLSCREEN)
return ZDWM_WINDOW_STATE_FULLSCREEN;
if (atom == atoms->_NET_WM_STATE_ABOVE) return ZDWM_WINDOW_STATE_ABOVE;
if (atom == atoms->_NET_WM_STATE_STICKY) return ZDWM_WINDOW_STATE_STICKY;
if (atom == atoms->_NET_WM_STATE_MODAL) return ZDWM_WINDOW_STATE_MODAL;
return (window_state_t)-1;
}

View File

@@ -1,67 +0,0 @@
#pragma once
#include <stdbool.h>
#include <stddef.h>
#include <xcb/xcb_icccm.h>
#include <xcb/xproto.h>
#include "core/backend.h"
#include "core/window.h"
typedef struct atoms_t atoms_t;
/**
* @brief 获取窗口的 role 属性
* @param backend 后端实例指针
* @param window 目标窗口
* @return 返回窗口的 role 属性字符串,调用者持有其内存并负责调用 free 释放
*/
char *window_get_role(backend_t *backend, xcb_window_t window);
/**
* @brief 获取窗口标题
* @details 获取窗口的 _NET_WM_NAME/WM_NAME 属性作为其 title
* @param backend 后端实例指针
* @param window 目标窗口
* @return 返回窗口标题字符串,由调用者持有其内存并负责调用 free 释放
*/
char *window_get_title(backend_t *backend, xcb_window_t window);
/**
* @brief 获取窗口的 class 和 instance 信息
* @param backend 后端实例指针
* @param window 目标窗口
* @param class_out class 信息返回指针,调用者持有其内存并负责调用 free 释放
* @param instance_out instance 信息返回指针,调用者持有其内存并负责调用 free
* 释放
*/
void window_get_class(
backend_t *backend,
xcb_window_t window,
char **class_out,
char **instance_out
);
xcb_window_t window_get_transient_for(backend_t *backend, xcb_window_t window);
xcb_get_window_attributes_reply_t *
window_get_attributes(backend_t *backend, xcb_window_t window);
bool window_get_types(
backend_t *backend,
xcb_window_t window,
window_type_t **types,
size_t *count
);
bool window_get_fixed_size(backend_t *backend, xcb_window_t window, bool *out);
bool window_get_geometry(backend_t *backend, xcb_window_t window, rect_t *out);
bool window_get_atom_array(
backend_t *backend,
xcb_window_t window,
xcb_atom_t property,
xcb_atom_t **out_atoms,
uint32_t *out_len
);
bool window_get_wm_hints(
backend_t *backend,
xcb_window_t window,
xcb_icccm_wm_hints_t *out
);
window_state_t atom_to_window_state(const atoms_t *atoms, xcb_atom_t atom);

2
src/bar.h Normal file
View File

@@ -0,0 +1,2 @@
#pragma once

View File

@@ -1 +0,0 @@
../../.clang-format

View File

@@ -1,107 +0,0 @@
#pragma once
#include <stddef.h>
#include <string.h>
#include "base/memory.h"
static constexpr size_t INIT_CAPACITY = 4;
static inline size_t next_capacity(size_t capacity) {
if (capacity) return capacity * 2;
return INIT_CAPACITY;
}
/**
* @file array.h
* @brief 动态数组基础操作。
*/
/**
* @brief 确保数组容量至少为 need。
*
* @param items 数组指针地址
* @param item_size 单个元素大小
* @param capacity 当前容量地址
* @param need 目标容量
*/
static inline void array_reserve_impl(
void **items,
size_t item_size,
size_t *capacity,
size_t need
) {
if (*capacity >= need) return;
size_t new_capacity = next_capacity(*capacity);
while (new_capacity < need) {
new_capacity = next_capacity(new_capacity);
}
if (*items) {
xrealloc(items, item_size * new_capacity);
} else {
*items = xmalloc(item_size * new_capacity);
}
*capacity = new_capacity;
}
/**
* @brief 追加一个槽位并返回其地址。
*
* @param items 数组指针地址
* @param item_size 单个元素大小
* @param count 元素个数地址
* @param capacity 容量地址
*
* @return void* 新槽位地址
*/
static inline void *array_push_impl(
void **items,
size_t item_size,
size_t *count,
size_t *capacity
) {
array_reserve_impl(items, item_size, capacity, *count + 1);
void *slot = (char *)*items + *count * item_size;
(*count)++;
return slot;
}
/**
* @brief 删除指定下标元素,并将后续元素左移。
*
* @param items 数组首地址
* @param item_size 单个元素大小
* @param count 元素个数地址
* @param index 要删除的下标
*
* @return true 删除成功
* @return false 下标越界
*/
static inline bool
array_erase_impl(void *items, size_t item_size, size_t *count, size_t index) {
if (index >= *count) return false;
size_t tail_count = *count - index - 1;
if (tail_count > 0) {
char *dest = (char *)items + index * item_size;
char *src = dest + item_size;
memmove(dest, src, tail_count * item_size);
}
(*count)--;
return true;
}
/** @brief 为具体类型数组预留容量。 */
#define array_reserve(items, capacity, need) \
array_reserve_impl((void **)&(items), sizeof(*(items)), &(capacity), (need))
/** @brief 为具体类型数组追加一个槽位。 */
#define array_push(items, count, capacity) \
array_push_impl((void **)&(items), sizeof(*(items)), &(count), &(capacity))
/** @brief 删除具体类型数组中指定下标的元素。 */
#define array_erase(items, count, index) \
array_erase_impl((items), sizeof(*(items)), &(count), (index))

View File

@@ -1,41 +0,0 @@
#pragma once
#include <stdarg.h>
#include <stdio.h>
#define fatal(format, ...) \
_fatal(__LINE__, __FUNCTION__, __FILE__, format, ##__VA_ARGS__)
void _fatal(
int line,
const char *function,
const char *file,
const char *format,
...
) __attribute__((noreturn)) __attribute__((format(printf, 4, 5)));
#define warn(format, ...) \
_warn(__LINE__, __FUNCTION__, __FILE__, format, ##__VA_ARGS__)
void _warn(
int line,
const char *function,
const char *file,
const char *format,
...
) __attribute__((format(printf, 4, 5)));
const char *current_time_str(void);
#if defined(RELEASE)
#define logger(format, ...)
#elif defined(LOG_FILE)
static inline void logger(const char *format, ...) {
va_list ap;
va_start(ap, format);
FILE *log_file = fopen(LOG_FILE, "a+t");
vfprintf(log_file, format, ap);
va_end(ap);
fclose(log_file);
}
#else
#define logger(format, ...) printf(format, ##__VA_ARGS__)
#endif

View File

@@ -1,29 +0,0 @@
#pragma once
#include <stddef.h>
#include <sys/types.h>
#define ssizeof(foo) (ssize_t)sizeof(foo)
#define countof(foo) (ssizeof(foo) / ssizeof(foo[0]))
#ifdef __GNUC__
#define likely(expr) __builtin_expect(!!(expr), 1)
#define unlikely(expr) __builtin_expect((expr), 0)
#else
#define likely(expr) expr
#define unlikely(expr) expr
#endif
#ifdef MIN
#undef MIN
#endif
#ifndef MIN
#define MIN(a, b) (((a) < (b)) ? (a) : (b))
#endif
#ifdef MAX
#undef MAX
#endif
#ifndef MAX
#define MAX(a, b) (((a) > (b)) ? (a) : (b))
#endif

View File

@@ -1,77 +0,0 @@
#pragma once
#include <stddef.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#define p_alloc_nr(x) (((x) + 16) * 3 / 2)
#define p_new(type, count) ((type *)xmalloc(sizeof(type) * (count)))
#define p_clear(p, count) ((void)memset((p), 0, sizeof(*(p)) * (count)))
#define p_realloc(pp, count) xrealloc((void *)(pp), sizeof(**(pp)) * (count))
#define p_copy(src, count) xmemcopy((src), sizeof(*(src)) * (count))
#define p_delete(mem_p) \
do { \
void **__ptr = (void **)(mem_p); \
free(*__ptr); \
*(void **)__ptr = nullptr; \
} while (0)
#define p_take(dst_p, src_p) \
do { \
p_delete(dst_p); \
*(dst_p) = *(src_p); \
*(src_p) = nullptr; \
} while (0)
static inline char *p_strdup(const char *text) {
char *r = strdup(text);
if (!r) abort();
return r;
}
static inline char *p_strndup(const char *text, size_t n) {
char *r = strndup(text, n);
if (!r) abort();
return r;
}
static inline char *p_strdup_nullable(const char *text) {
if (!text) return nullptr;
return p_strdup(text);
}
static inline void *__attribute__((malloc)) xmalloc(ssize_t size) {
void *ptr;
if (size <= 0) return nullptr;
ptr = calloc(1, size);
if (!ptr) abort();
return ptr;
}
static inline void *xmemcopy(const void *src, size_t n) {
if (!n) return nullptr;
void *mem = xmalloc(n);
void *ret = memcpy(mem, src, n);
if (ret != mem) abort();
return ret;
}
static inline void xrealloc(void **ptr, ssize_t newsize) {
if (newsize <= 0) p_delete(ptr);
else {
*ptr = realloc(*ptr, newsize);
if (!*ptr) abort();
}
}
/*
* Unlike strlen(), a_strlen() accepts nullptr and returns 0 in that case.
*/
static inline ssize_t a_strlen(const char *s) { return s ? strlen(s) : 0; }

View File

@@ -1 +0,0 @@
../../.clang-format

View File

@@ -1,50 +0,0 @@
#include "config/defaults.h"
#include "base/macros.h"
bool config_defaults_build(
const zdwm_api_t *api,
zdwm_config_builder_t *builder,
const zdwm_output_info_t *outputs,
size_t output_count
) {
if (!api || !builder || !outputs || output_count == 0) return false;
zdwm_layout_id_t tile_id = api->register_layout(
builder,
"tile",
"[]=",
"builtin tile",
api->builtin_layouts.tile
);
zdwm_layout_id_t monocle_id = api->register_layout(
builder,
"monocle",
"[M]",
"builtin monocle",
api->builtin_layouts.monocle
);
zdwm_layout_id_t floating_id =
api->register_layout(builder, "floating", "><>", "floating", nullptr);
if (tile_id == ZDWM_LAYOUT_ID_INVALID ||
monocle_id == ZDWM_LAYOUT_ID_INVALID ||
floating_id == ZDWM_LAYOUT_ID_INVALID) {
return false;
}
zdwm_layout_id_t layout_ids[] = {tile_id, monocle_id, floating_id};
for (size_t i = 0; i < output_count; i++) {
if (api->define_workspace(
builder,
i,
"main",
layout_ids,
countof(layout_ids),
tile_id
) == ZDWM_WORKSPACE_ID_INVALID) {
return false;
}
}
return true;
}

View File

@@ -1,5 +0,0 @@
#pragma once
#include <zdwm/config.h>
zdwm_config_setup_fn config_defaults_build;

View File

@@ -1,100 +0,0 @@
#include "config/loader.h"
#include <dlfcn.h>
#include <stdlib.h>
#include <unistd.h>
#include "base/log.h"
#include "base/memory.h"
static char *config_loader_join_path(const char *base, const char *suffix) {
if (!base || !suffix) return nullptr;
size_t base_len = strlen(base);
size_t suffix_len = strlen(suffix);
char *path = p_new(char, base_len + suffix_len + 1);
memcpy(path, base, base_len);
memcpy(path + base_len, suffix, suffix_len + 1);
return path;
}
static bool config_loader_resolve_path(
char **out_path,
bool *out_is_explicit,
const char *override_path
) {
const char *path = override_path;
if (path && *path) {
*out_path = p_strdup(path);
*out_is_explicit = true;
return true;
}
path = getenv("ZDWM_CONFIG_LIB");
if (path && *path) {
*out_path = p_strdup(path);
*out_is_explicit = true;
return true;
}
path = getenv("XDG_CONFIG_HOME");
if (path && *path) {
*out_path = config_loader_join_path(path, "/zdwm/config.so");
*out_is_explicit = false;
return *out_path != nullptr;
}
path = getenv("HOME");
if (path && *path) {
*out_path = config_loader_join_path(path, "/.config/zdwm/config.so");
*out_is_explicit = false;
return *out_path != nullptr;
}
return false;
}
bool config_loader_load(config_loader_t *loader, const char *override_path) {
if (!loader) return false;
config_loader_cleanup(loader);
bool is_explicit = false;
if (!config_loader_resolve_path(&loader->path, &is_explicit, override_path)) {
return false;
}
if (access(loader->path, F_OK) != 0) {
if (!is_explicit) return true;
warn("config library not found: %s", loader->path);
config_loader_cleanup(loader);
return false;
}
loader->handle = dlopen(loader->path, RTLD_NOW | RTLD_LOCAL);
if (!loader->handle) {
warn("failed to load config library %s: %s", loader->path, dlerror());
config_loader_cleanup(loader);
return false;
}
dlerror();
loader->setup = (zdwm_config_setup_fn *)dlsym(loader->handle, "setup");
const char *error = dlerror();
if (!error) return true;
warn("failed to resolve setup from %s: %s", loader->path, error);
config_loader_cleanup(loader);
return false;
}
void config_loader_cleanup(config_loader_t *loader) {
if (!loader) return;
loader->setup = nullptr;
if (loader->handle) dlclose(loader->handle);
loader->handle = nullptr;
p_delete(&loader->path);
}

View File

@@ -1,12 +0,0 @@
#pragma once
#include <zdwm/config.h>
typedef struct config_loader_t {
char *path;
void *handle;
zdwm_config_setup_fn *setup;
} config_loader_t;
bool config_loader_load(config_loader_t *loader, const char *override_path);
void config_loader_cleanup(config_loader_t *loader);

View File

@@ -1,213 +0,0 @@
#include "config/runtime_config.h"
#include <dlfcn.h>
#include <stddef.h>
#include <zdwm/config.h>
#include "base/array.h"
#include "base/memory.h"
#include "config/defaults.h"
#include "config/loader.h"
#include "core/layout.h"
#include "core/rules.h"
#include "core/runtime.h"
#include "core/types.h"
#include "core/wm_desc.h"
struct zdwm_config_builder_t {
layout_registry_t layouts;
rules_t rules;
workspace_desc_t *workspaces;
size_t workspace_count;
size_t workspace_capacity;
size_t output_count;
};
void runtime_config_cleanup(runtime_init_desc_t *desc) {
if (!desc) return;
if (desc->layouts.slots || desc->layouts.slot_count ||
desc->layouts.slot_capacity) {
layout_registry_cleanup(&desc->layouts);
}
workspace_desc_list_cleanup(&desc->workspaces, &desc->workspace_count);
if (desc->config_module_handle) dlclose(desc->config_module_handle);
desc->config_module_handle = nullptr;
}
static void config_builder_cleanup(zdwm_config_builder_t *builder) {
if (!builder) return;
if (builder->layouts.slots || builder->layouts.slot_count ||
builder->layouts.slot_capacity) {
layout_registry_cleanup(&builder->layouts);
}
workspace_desc_list_cleanup(&builder->workspaces, &builder->workspace_count);
builder->workspace_capacity = 0;
builder->output_count = 0;
}
static layout_id_t runtime_config_register_layout(
zdwm_config_builder_t *builder,
const char *name,
const char *symbol,
const char *description,
layout_fn fn
) {
if (!builder || !name || !symbol) return ZDWM_LAYOUT_ID_INVALID;
return layout_register(&builder->layouts, name, symbol, description, fn);
}
static workspace_id_t runtime_config_define_workspace(
zdwm_config_builder_t *builder,
size_t output_index,
const char *name,
const layout_id_t *layout_ids,
size_t layout_count,
layout_id_t initial_layout_id
) {
if (!builder) return ZDWM_WORKSPACE_ID_INVALID;
if (output_index >= builder->output_count) return ZDWM_WORKSPACE_ID_INVALID;
if (builder->workspace_count >= (size_t)ZDWM_WORKSPACE_ID_INVALID) {
return ZDWM_WORKSPACE_ID_INVALID;
}
workspace_desc_t desc = {
.output_index = output_index,
.name = name,
.layout_ids = layout_ids,
.layout_count = layout_count,
.initial_layout_id = initial_layout_id,
};
if (!workspace_desc_valid(&desc, builder->output_count)) {
return ZDWM_WORKSPACE_ID_INVALID;
}
for (size_t i = 0; i < layout_count; i++) {
if (!layout_slot_get(&builder->layouts, layout_ids[i])) {
return ZDWM_WORKSPACE_ID_INVALID;
}
}
workspace_id_t workspace_id = (workspace_id_t)builder->workspace_count;
workspace_desc_t *slot = array_push(
builder->workspaces,
builder->workspace_count,
builder->workspace_capacity
);
slot->output_index = output_index;
slot->name = p_strdup(name);
slot->layout_ids = p_copy(layout_ids, layout_count);
slot->layout_count = layout_count;
slot->initial_layout_id = initial_layout_id;
return workspace_id;
}
static bool rule_match_valid(const rule_match_t *match) {
if (!match) return false;
return match->app_id || match->role || match->class_name ||
match->instance_name;
}
static bool
rule_action_valid(const rule_action_t *action, size_t workspace_count) {
if (!action) return false;
if (action->workspace == ZDWM_WORKSPACE_ID_INVALID) {
return action->switch_to_workspace || action->fullscreen ||
action->maximize || action->floating;
}
return action->workspace < workspace_count;
}
static bool runtime_config_add_rule(
zdwm_config_builder_t *builder,
const zdwm_rule_match_t *match,
const zdwm_rule_action_t *action
) {
if (!builder) return false;
if (!rule_match_valid(match)) return false;
if (!rule_action_valid(action, builder->workspace_count)) return false;
rules_t *rules = &builder->rules;
rule_item_t *rule = array_push(rules->items, rules->count, rules->capacity);
rule->match.app_id = p_strdup_nullable(match->app_id);
rule->match.role = p_strdup_nullable(match->role);
rule->match.class_name = p_strdup_nullable(match->class_name);
rule->match.instance_name = p_strdup_nullable(match->instance_name);
rule->action = *action;
return true;
}
static bool config_builder_finish(
zdwm_config_builder_t *builder,
runtime_init_desc_t *out
) {
if (!builder || !out) return false;
if (!builder->layouts.slot_count || !builder->workspace_count) return false;
if (!layout_registry_move(&builder->layouts, &out->layouts)) return false;
if (!rules_move(&builder->rules, &out->rules)) return false;
out->workspaces = builder->workspaces;
out->workspace_count = builder->workspace_count;
builder->workspaces = nullptr;
builder->workspace_count = 0;
builder->workspace_capacity = 0;
builder->output_count = 0;
return true;
}
static bool runtime_config_build(
zdwm_config_setup_fn *setup,
const output_info_t *outputs,
size_t output_count,
runtime_init_desc_t *out
) {
if (!setup || !out) return false;
zdwm_config_builder_t builder = {0};
builder.output_count = output_count;
zdwm_api_t api = {
.abi_version = ZDWM_CONFIG_ABI_VERSION,
.builtin_layouts =
{
.tile = nullptr,
.monocle = nullptr,
},
.register_layout = runtime_config_register_layout,
.define_workspace = runtime_config_define_workspace,
.add_rule = runtime_config_add_rule,
};
bool ok = setup(&api, &builder, outputs, output_count) &&
config_builder_finish(&builder, out);
config_builder_cleanup(&builder);
return ok;
}
bool runtime_config_load(const char *override_path, runtime_init_desc_t *out) {
if (!out || !out->outputs || out->output_count == 0) return false;
runtime_config_cleanup(out);
config_loader_t loader = {0};
if (!config_loader_load(&loader, override_path)) {
config_loader_cleanup(&loader);
return false;
}
zdwm_config_setup_fn *setup =
loader.setup ? loader.setup : config_defaults_build;
bool ok = runtime_config_build(setup, out->outputs, out->output_count, out);
if (ok) {
out->config_module_handle = loader.handle;
loader.handle = nullptr;
}
config_loader_cleanup(&loader);
return ok;
}

View File

@@ -1,6 +0,0 @@
#pragma once
typedef struct runtime_init_desc_t runtime_init_desc_t;
bool runtime_config_load(const char *override_path, runtime_init_desc_t *out);
void runtime_config_cleanup(runtime_init_desc_t *desc);

View File

@@ -1 +0,0 @@
../../.clang-format

View File

@@ -1,39 +0,0 @@
#pragma once
#include <stddef.h>
#include "core/event.h"
#include "core/plan.h"
#include "core/types.h"
typedef struct backend_t backend_t;
typedef struct backend_detect_t {
output_info_t *outputs;
size_t output_count;
} backend_detect_t;
backend_t *backend_create(const char *display_name);
void backend_destroy(backend_t *backend);
backend_detect_t *backend_detect(backend_t *backend);
void backend_detect_destroy(backend_detect_t *detect);
/**
* @brief 阻塞等待 backend 产出下一个可交给 runtime 的归一化事件
* @details
* backend 可以在内部丢弃无关的原始平台事件,直到拿到一个可路由事件或遇到
* stop/error 条件。
*
* 返回 true 时,`event` 已被完整填充;若其中包含堆内存,所有权转交给调用方,
* 调用方必须在本轮处理结束后调用 event_cleanup() 或 event_reset()。
*
* 返回 false 时,表示 backend 请求停止或遇到错误;此时 `event` 必须保持为
* 无需 cleanup 的状态,调用方可以直接退出循环。
*/
bool backend_next_event(backend_t *backend, event_t *event);
bool backend_apply_effect(
backend_t *backend,
const effect_t *effects,
size_t effect_count
);

View File

@@ -1,38 +0,0 @@
#pragma once
#include "core/types.h"
#include "core/wm_desc.h"
typedef enum command_type_t {
ZDWM_COMMAND_MANAGE_WINDOW,
ZDWM_COMMAND_SWITCH_WORKSPACE
} command_type_t;
typedef struct manage_window_command_t {
workspace_id_t workspace;
bool floating;
bool switch_to_workspace;
window_info_t info;
} manage_window_command_t;
typedef struct switch_workspace_command_t {
output_id_t output;
workspace_id_t workspace;
} switch_workspace_command_t;
/**
* @brief 非 owning 的命令值对象
* @details
* command_t 自身不拥有任何堆内存;其当前所有 payload 中出现的指针字段均为借用
* 语义。
*
* 因此 command_t 支持按值浅拷贝,且不需要单独的 cleanup 接口。若接收方需要在
* 源数据生命周期之外继续持有相关内容,必须自行复制。
*/
typedef struct command_t {
command_type_t type;
union {
manage_window_command_t manage_window;
switch_workspace_command_t switch_workspace;
} as;
} command_t;

View File

@@ -1,23 +0,0 @@
#include "core/command_buffer.h"
#include "base/array.h"
#include "base/memory.h"
#include "core/command.h"
void command_buffer_reset(command_buffer_t *buffer) {
if (!buffer->items || !buffer->capacity) return;
p_clear(buffer->items, buffer->capacity);
buffer->count = 0;
}
void command_buffer_cleanup(command_buffer_t *buffer) {
p_delete(&buffer->items);
buffer->count = 0;
buffer->capacity = 0;
}
void command_buffer_push(command_buffer_t *buffer, const command_t *command) {
command_t *cmd = array_push(buffer->items, buffer->count, buffer->capacity);
*cmd = *command;
}

View File

@@ -1,15 +0,0 @@
#pragma once
#include <stddef.h>
#include "core/command.h"
typedef struct command_buffer_t {
command_t *items;
size_t count;
size_t capacity;
} command_buffer_t;
void command_buffer_reset(command_buffer_t *buffer);
void command_buffer_cleanup(command_buffer_t *buffer);
void command_buffer_push(command_buffer_t *buffer, const command_t *command);

View File

@@ -1,45 +0,0 @@
#include "core/event.h"
#include "base/memory.h"
static void window_layer_props_cleanup(window_layer_props_t *props) {
if (!props) return;
p_delete(&props->types);
props->type_count = 0;
p_delete(&props->states);
props->state_count = 0;
}
static void window_metadata_cleanup(window_metadata_t *metadata) {
if (!metadata) return;
p_delete(&metadata->title);
p_delete(&metadata->app_id);
p_delete(&metadata->role);
p_delete(&metadata->class_name);
p_delete(&metadata->instance_name);
}
void event_cleanup(event_t *event) {
if (!event) return;
switch (event->type) {
case ZDWM_EVENT_WINDOW_MAP_REQUEST:
window_layer_props_cleanup(&event->as.window_map_request.props);
window_metadata_cleanup(&event->as.window_map_request.metadata);
break;
case ZDWM_EVENT_WINDOW_METADATA_CHANGED:
window_metadata_cleanup(&event->as.window_metadata_change.metadata);
break;
default:
break;
}
}
void event_reset(event_t *event) {
if (!event) return;
event_cleanup(event);
p_clear(event, 1);
}

View File

@@ -1,183 +0,0 @@
#pragma once
#include <stdint.h>
#include "core/types.h"
#include "core/window.h"
typedef enum event_type_t {
ZDWM_EVENT_KEY_PRESS,
ZDWM_EVENT_POINTER_BUTTON_PRESS,
ZDWM_EVENT_POINTER_BUTTON_RELEASE,
ZDWM_EVENT_POINTER_MOTION,
ZDWM_EVENT_POINTER_ENTER,
ZDWM_EVENT_WINDOW_MAP_REQUEST,
ZDWM_EVENT_WINDOW_REMOVE,
ZDWM_EVENT_WINDOW_METADATA_CHANGED,
ZDWM_EVENT_WINDOW_HINTS_CHANGED,
ZDWM_EVENT_WINDOW_ACTIVATE_REQUEST,
ZDWM_EVENT_WINDOW_STATE_REQUEST,
ZDWM_EVENT_CONFIGURE_REQUEST,
} event_type_t;
typedef enum modifier_bit_t {
ZDWM_MOD_SHIFT = 1u << 0,
ZDWM_MOD_CTRL = 1u << 1,
ZDWM_MOD_ALT = 1u << 2,
ZDWM_MOD_SUPER = 1u << 3,
} modifier_bit_t;
typedef uint32_t modifier_mask_t;
typedef uint32_t keysym_t;
typedef struct key_press_event_t {
modifier_mask_t modifiers;
keysym_t keysym;
uint32_t keycode;
} key_press_event_t;
typedef enum button_t {
ZDWM_BUTTON_LEFT,
ZDWM_BUTTON_RIGHT,
ZDWM_BUTTON_MIDDLE,
} button_t;
typedef struct pointer_button_event_t {
modifier_mask_t modifiers;
button_t button;
window_id_t window;
point_t root;
point_t local;
} pointer_button_event_t;
typedef struct pointer_motion_event_t {
window_id_t window;
point_t root;
point_t local;
} pointer_motion_event_t;
typedef struct pointer_enter_event_t {
window_id_t window;
point_t root;
point_t local;
} pointer_enter_event_t;
typedef struct window_map_request_event_t {
window_id_t window;
window_id_t transient_for;
bool override_redirect;
bool skip_taskbar;
bool urgent;
bool fixed_size;
window_geometry_mode_t geometry_mode;
rect_t rect;
window_layer_props_t props;
window_metadata_t metadata;
} window_map_request_event_t;
typedef enum window_remove_reason_t {
ZDWM_WINDOW_REMOVE_WITHDRAWN,
ZDWM_WINDOW_REMOVE_DESTROY,
} window_remove_reason_t;
typedef struct window_remove_event_t {
window_id_t window;
window_remove_reason_t reason;
} window_remove_event_t;
typedef enum window_metadata_change_flags_t {
ZDWM_WINDOW_METADATA_CHANGE_TITLE = 1u << 0,
ZDWM_WINDOW_METADATA_CHANGE_APP_ID = 1u << 1,
ZDWM_WINDOW_METADATA_CHANGE_ROLE = 1u << 2,
ZDWM_WINDOW_METADATA_CHANGE_CLASS = 1u << 3,
ZDWM_WINDOW_METADATA_CHANGE_INSTANCE = 1u << 4,
} window_metadata_change_flags_t;
typedef struct window_metadata_change_event_t {
window_id_t window;
uint32_t changed_fields;
window_metadata_t metadata;
} window_metadata_change_event_t;
typedef enum window_hint_changed_flags_t {
ZDWM_WINDOW_HINT_CHANGED_NONE = 0,
ZDWM_WINDOW_HINT_CHANGED_URGENT = 1u << 0,
ZDWM_WINDOW_HINT_CHANGED_FIXED_SIZE = 1u << 1,
ZDWM_WINDOW_HINT_CHANGED_SKIP_TASKBAR = 1u << 2,
} window_hint_changed_flags_t;
typedef struct window_hints_changed_event_t {
window_id_t window;
uint32_t changed_fields;
bool urgent;
bool fixed_size;
bool skip_taskbar;
} window_hints_changed_event_t;
typedef enum window_activation_source_t {
ZDWM_WINDOW_ACTIVATION_SOURCE_LEGACY = 0,
ZDWM_WINDOW_ACTIVATION_SOURCE_APPLICATION = 1,
ZDWM_WINDOW_ACTIVATION_SOURCE_PAGER = 2,
} window_activation_source_t;
typedef struct window_activate_request_event_t {
window_id_t window;
window_activation_source_t source;
} window_activate_request_event_t;
typedef enum window_state_request_kind_t {
ZDWM_WINDOW_STATE_REQUEST_FULLSCREEN,
ZDWM_WINDOW_STATE_REQUEST_MAXIMIZED,
ZDWM_WINDOW_STATE_REQUEST_MINIMIZED,
} window_state_request_kind_t;
typedef enum window_state_request_action_t {
ZDWM_WINDOW_STATE_REQUEST_ACTION_ADD,
ZDWM_WINDOW_STATE_REQUEST_ACTION_REMOVE,
ZDWM_WINDOW_STATE_REQUEST_ACTION_TOGGLE,
} window_state_request_action_t;
typedef struct window_state_request_event_t {
window_id_t window;
window_state_request_kind_t kind;
window_state_request_action_t action;
} window_state_request_event_t;
typedef enum configure_request_field_t {
ZDWM_CONFIGURE_REQUEST_FIELD_NONE = 0,
ZDWM_CONFIGURE_REQUEST_FIELD_X = 1u << 0,
ZDWM_CONFIGURE_REQUEST_FIELD_Y = 1u << 1,
ZDWM_CONFIGURE_REQUEST_FIELD_WIDTH = 1u << 2,
ZDWM_CONFIGURE_REQUEST_FIELD_HEIGHT = 1u << 3,
} configure_request_field_t;
typedef struct configure_request_event_t {
window_id_t window;
uint32_t changed_fields;
int32_t x;
int32_t y;
int32_t width;
int32_t height;
} configure_request_event_t;
typedef struct event_t {
event_type_t type;
union {
key_press_event_t key_press;
pointer_button_event_t pointer_button_press;
pointer_button_event_t pointer_button_release;
pointer_motion_event_t pointer_motion;
pointer_enter_event_t pointer_enter;
window_map_request_event_t window_map_request;
window_remove_event_t window_remove;
window_metadata_change_event_t window_metadata_change;
window_hints_changed_event_t window_hints_changed;
window_activate_request_event_t window_activate_request;
window_state_request_event_t window_state_request;
configure_request_event_t configure_request;
} as;
} event_t;
void event_cleanup(event_t *event);
void event_reset(event_t *event);

View File

@@ -1,103 +0,0 @@
#include "core/layer.h"
#include <stddef.h>
#include "base/array.h"
#include "base/memory.h"
#include "core/types.h"
#include "core/window.h"
void layer_stack_cleanup(layer_stack_t *layer) {
p_delete(&layer->order);
layer->count = 0;
layer->capacity = 0;
}
void layer_stack_append(layer_stack_t *layer, window_id_t window) {
window_id_t *win_ptr =
array_push(layer->order, layer->count, layer->capacity);
*win_ptr = window;
}
bool layer_stack_remove(layer_stack_t *layer, window_id_t window) {
bool found = false;
size_t index = 0;
for (size_t i = 0; i < layer->count; ++i) {
if (layer->order[i] != window) continue;
found = true;
index = i;
break;
}
if (found) {
for (size_t i = index; i < layer->count - 1; ++i) {
layer->order[i] = layer->order[i + 1];
}
layer->count--;
}
return found;
}
bool layer_stack_raise(layer_stack_t *layer, window_id_t window) {
bool found = false;
size_t index = 0;
for (size_t i = 0; i < layer->count; ++i) {
if (layer->order[i] != window) continue;
found = true;
index = i;
break;
}
if (!found) return false;
bool need_change = index != layer->count - 1;
if (!need_change) return false;
for (size_t i = index; i < layer->count - 1; ++i) {
layer->order[i] = layer->order[i + 1];
}
layer->order[layer->count - 1] = window;
return true;
}
bool layer_stack_lower(layer_stack_t *layer, window_id_t window) {
bool found = false;
size_t index = 0;
for (size_t i = 0; i < layer->count; ++i) {
if (layer->order[i] != window) continue;
found = true;
index = i;
break;
}
if (!found) return false;
bool need_change = index != 0;
if (!need_change) return false;
for (size_t i = index; i > 0; --i) {
layer->order[i] = layer->order[i - 1];
}
layer->order[0] = window;
return true;
}
layer_type_t layer_classify(const window_layer_props_t *props) {
for (size_t i = 0; i < props->type_count; ++i) {
window_type_t type = props->types[i];
if (type == ZDWM_WINDOW_TYPE_NOTIFICATION) return ZDWM_WINDOW_LAYER_OVERLAY;
}
for (size_t i = 0; i < props->state_count; ++i) {
window_state_t state = props->states[i];
if (state == ZDWM_WINDOW_STATE_ABOVE) return ZDWM_WINDOW_LAYER_TOP;
}
return ZDWM_WINDOW_LAYER_NORMAL;
}

View File

@@ -1,60 +0,0 @@
#pragma once
#include <stddef.h>
#include "core/types.h"
#include "core/window.h"
/* 层级从低到高 */
typedef enum layer_type_t {
ZDWM_WINDOW_LAYER_DESKTOP = 0,
ZDWM_WINDOW_LAYER_NORMAL,
ZDWM_WINDOW_LAYER_TOP,
ZDWM_WINDOW_LAYER_OVERLAY,
ZDWM_WINDOW_LAYER_COUNT,
} layer_type_t;
typedef struct layer_stack_t {
window_id_t *order;
size_t count;
size_t capacity;
} layer_stack_t;
/**
* @brief 释放层栈内部资源并重置为空状态。
* @param layer 目标层栈。
*/
void layer_stack_cleanup(layer_stack_t *layer);
/**
* @brief 在该层栈顶部追加一个窗口。
* @param layer 目标层栈。
* @param window 待追加的窗口 ID。
*/
void layer_stack_append(layer_stack_t *layer, window_id_t window);
/**
* @brief 删除指定窗口。
* @param layer 目标层栈。
* @param window 待删除的窗口 ID。
* @return 找到并删除窗口时返回 true否则返回 false。
*/
bool layer_stack_remove(layer_stack_t *layer, window_id_t window);
/**
* @brief 将指定窗口提升到该层栈顶。
* @param layer 目标层栈。
* @param window 待提升的窗口 ID。
* @return 仅在窗口存在且顺序发生变化时返回 true否则返回 false。
*/
bool layer_stack_raise(layer_stack_t *layer, window_id_t window);
/**
* @brief 将指定窗口下沉到该层栈底。
* @param layer 目标层栈。
* @param window 待下沉的窗口 ID。
* @return 仅在窗口存在且顺序发生变化时返回 true否则返回 false。
*/
bool layer_stack_lower(layer_stack_t *layer, window_id_t window);
layer_type_t layer_classify(const window_layer_props_t *props);

View File

@@ -1,93 +0,0 @@
#include "core/layout.h"
#include <stddef.h>
#include <stdlib.h>
#include <string.h>
#include "base/array.h"
#include "base/memory.h"
#include "core/types.h"
void layout_result_cleanup(layout_result_t *result) {
p_delete(&result->items);
result->item_count = 0;
result->item_capacity = 0;
}
void layout_result_push(layout_result_t *result, layout_item_t item) {
layout_item_t *slot =
array_push(result->items, result->item_count, result->item_capacity);
*slot = item;
}
void layout_registry_cleanup(layout_registry_t *registry) {
for (size_t i = 0; i < registry->slot_count; ++i) {
layout_slot_t *r = &registry->slots[i];
p_delete(&r->name);
p_delete(&r->symbol);
p_delete(&r->description);
r->fn = nullptr;
r->id = ZDWM_LAYOUT_ID_INVALID;
}
p_delete(&registry->slots);
registry->slot_count = 0;
registry->slot_capacity = 0;
}
bool layout_registry_move(layout_registry_t *src, layout_registry_t *dest) {
if (!src || !dest) return false;
dest->slots = src->slots;
dest->slot_count = src->slot_count;
dest->slot_capacity = src->slot_capacity;
src->slots = nullptr;
src->slot_count = 0;
src->slot_capacity = 0;
return true;
}
size_t layout_registry_count(const layout_registry_t *registry) {
return registry->slot_count;
}
const layout_slot_t *
layout_registry_at(const layout_registry_t *registry, size_t index) {
if (index >= registry->slot_count) return nullptr;
return &registry->slots[index];
}
layout_id_t layout_register(
layout_registry_t *registry,
const char *name,
const char *symbol,
const char *description,
layout_fn fn
) {
if (!name || !symbol) return ZDWM_LAYOUT_ID_INVALID;
layout_id_t id = (layout_id_t)registry->slot_count;
layout_slot_t *r =
array_push(registry->slots, registry->slot_count, registry->slot_capacity);
r->id = id;
r->name = p_strdup(name);
r->symbol = p_strdup(symbol);
r->description = p_strdup_nullable(description);
r->fn = fn;
return r->id;
}
layout_fn layout_get(const layout_registry_t *registry, layout_id_t id) {
const layout_slot_t *layout_slot = layout_slot_get(registry, id);
if (layout_slot) return layout_slot->fn;
return nullptr;
}
const layout_slot_t *
layout_slot_get(const layout_registry_t *registry, layout_id_t id) {
if (id >= registry->slot_count) return nullptr;
return &registry->slots[id];
}

View File

@@ -1,95 +0,0 @@
#pragma once
#include <stddef.h>
#include <zdwm/layout.h>
#include "core/types.h"
typedef zdwm_layout_ctx_t layout_ctx_t;
typedef zdwm_layout_item_t layout_item_t;
typedef zdwm_layout_result_t layout_result_t;
typedef zdwm_layout_fn layout_fn;
typedef struct layout_slot_t {
layout_id_t id;
/*
* 布局名称,用于:
* - 配置引用
* - 日志输出
* - 调试展示
*
* 推荐使用稳定、可读的名称,如 "tile"、"monocle"、"floating"。
*/
const char *name;
/*
* 面向用户的布局说明。
*
* 用于帮助信息、调试输出和交互式布局选择;可为空。
*/
const char *description;
/*
* 布局符号,用于状态栏显示等紧凑场景。
*
* 推荐使用 1-2 个字符的简短标识符。
*/
const char *symbol;
/*
* 布局执行函数。
*
* fn == NULL 表示 floating 布局,即该布局不参与平铺计算。
*/
layout_fn fn;
} layout_slot_t;
typedef struct layout_registry_t {
layout_slot_t *slots;
size_t slot_count;
size_t slot_capacity;
} layout_registry_t;
void layout_result_cleanup(layout_result_t *result);
void layout_result_push(layout_result_t *result, layout_item_t item);
void layout_registry_cleanup(layout_registry_t *registry);
bool layout_registry_move(layout_registry_t *src, layout_registry_t *dest);
size_t layout_registry_count(const layout_registry_t *registry);
const layout_slot_t *
layout_registry_at(const layout_registry_t *registry, size_t index);
/*
* 注册一个布局并返回其稳定 ID。
*
* 前置条件:
* - registry 必须已初始化
*
* 参数约束:
* - name 不可为空,用于稳定配置名、日志和调试展示
* - symbol 不可为空,用于状态栏等紧凑展示
* - description 可为空
* - fn 可为空fn == NULL 表示 floating 布局,不参与平铺计算
*
* 返回值:
* - 成功时返回新注册布局的 ID
* - name 或 symbol 非法时返回 ZDWM_LAYOUT_ID_INVALID
*
* 注册成功后,布局 ID 与其在 registry 中的槽位索引保持一致。
*/
layout_id_t layout_register(
layout_registry_t *registry,
const char *name,
const char *symbol,
const char *description,
layout_fn fn
);
/*
* 获取可执行的布局函数。
*
* 返回 NULL 的情况包括:
* - id 无效
* - id 对应的 slot 不存在
* - slot 存在,但 fn == NULL表示 floating 布局)
*/
layout_fn layout_get(const layout_registry_t *registry, layout_id_t id);
const layout_slot_t *
layout_slot_get(const layout_registry_t *registry, layout_id_t id);

View File

@@ -1,22 +0,0 @@
#include "core/plan.h"
#include "base/array.h"
#include "base/memory.h"
void plan_reset(plan_t *plan) {
if (!plan->effects || !plan->capacity) return;
p_clear(plan->effects, plan->capacity);
plan->count = 0;
}
void plan_cleanup(plan_t *plan) {
p_delete(&plan->effects);
plan->count = 0;
plan->capacity = 0;
}
void plan_push_effect(plan_t *plan, const effect_t *effect) {
effect_t *eft = array_push(plan->effects, plan->count, plan->capacity);
*eft = *effect;
}

View File

@@ -1,44 +0,0 @@
#pragma once
#include <stddef.h>
#include <stdint.h>
#include "core/types.h"
typedef uint32_t dirty_mask_t;
typedef enum dirty_flags_t : dirty_mask_t {
ZDWM_DIRTY_NONE = 0,
ZDWM_DIRTY_LAYOUT = 1u << 0,
} dirty_flags_t;
typedef enum effect_type_t {
ZDWM_EFFECT_NONE,
ZDWM_EFFECT_MAP_WINDOW,
ZDWM_EFFECT_UNMAP_WINDOW,
ZDWM_EFFECT_FOCUS_WINDOW,
} effect_type_t;
typedef struct effect_only_window_id_t {
window_id_t window;
} effect_only_window_id_t;
typedef struct effect_t {
effect_type_t type;
union {
effect_only_window_id_t map;
effect_only_window_id_t unmap;
effect_only_window_id_t focus;
} as;
} effect_t;
typedef struct plan_t {
effect_t *effects;
size_t count;
size_t capacity;
dirty_mask_t dirty_flags;
} plan_t;
void plan_reset(plan_t *plan);
void plan_cleanup(plan_t *plan);
void plan_push_effect(plan_t *plan, const effect_t *effect);

View File

@@ -1,117 +0,0 @@
#include "core/policy.h"
#include <zdwm/types.h>
#include "base/macros.h"
#include "core/command.h"
#include "core/command_buffer.h"
#include "core/event.h"
#include "core/layer.h"
#include "core/rules.h"
#include "core/state.h"
#include "core/types.h"
#include "core/wm_desc.h"
static workspace_id_t derive_window_workspace(const state_t *state) {
return state->outputs[state->current_output_index].current_workspace_id;
}
static bool route_map_request(
const state_t *state,
const rules_t *rules,
const window_map_request_event_t *e,
command_buffer_t *out
) {
if (state_window_get(state, e->window)) return false;
layer_type_t layer_type = layer_classify(&e->props);
if (e->transient_for != ZDWM_WINDOW_ID_INVALID) {
const window_t *window = state_window_get(state, e->transient_for);
if (window) layer_type = MAX(window->layer, layer_type);
}
command_t cmd = {
.type = ZDWM_COMMAND_MANAGE_WINDOW,
.as.manage_window = {
.workspace = derive_window_workspace(state),
.info = {
.id = e->window,
.transient_for = e->transient_for,
.frame_rect = e->rect,
.title = e->metadata.title,
.app_id = e->metadata.app_id,
.role = e->metadata.role,
.class_name = e->metadata.class_name,
.instance_name = e->metadata.instance_name,
.geometry_mode = e->geometry_mode,
.layer_type = layer_type,
.urgent = e->urgent,
.fixed_size = e->fixed_size,
.skip_taskbar = e->skip_taskbar,
},
},
};
rule_action_t action = {.workspace = ZDWM_WORKSPACE_ID_INVALID};
bool have_rule_match = rules_resolve(rules, &e->metadata, &action);
if (have_rule_match) {
manage_window_command_t *data = &cmd.as.manage_window;
if (action.workspace != ZDWM_WORKSPACE_ID_INVALID) {
data->workspace = action.workspace;
}
if (action.floating) {
data->floating = true;
}
if (action.fullscreen) {
data->info.geometry_mode = ZDWM_GEOMETRY_FULLSCREEN;
} else if (action.maximize) {
data->info.geometry_mode = ZDWM_GEOMETRY_MAXIMIZED;
}
}
command_buffer_push(out, &cmd);
if (have_rule_match && action.switch_to_workspace) {
workspace_id_t workspace_id = cmd.as.manage_window.workspace;
const workspace_t *workspace = state_workspace_get(state, workspace_id);
if (workspace) {
command_t switch_workspace_cmd = {
.type = ZDWM_COMMAND_SWITCH_WORKSPACE,
.as.switch_workspace = {
.output = workspace->output_id,
.workspace = workspace_id,
},
};
command_buffer_push(out, &switch_workspace_cmd);
}
}
return true;
}
bool policy_route_event(const policy_context_t *ctx, command_buffer_t *out) {
switch (ctx->event->type) {
case ZDWM_EVENT_WINDOW_MAP_REQUEST:
return route_map_request(
ctx->state,
ctx->rules,
&ctx->event->as.window_map_request,
out
);
default:
return false;
}
}
bool policy_apply_command(
const policy_context_t *ctx,
const command_buffer_t *command_buffer,
plan_t *plan
) {
/* TODO: */
return true;
}

View File

@@ -1,20 +0,0 @@
#pragma once
#include "core/command_buffer.h"
#include "core/event.h"
#include "core/plan.h"
#include "core/rules.h"
#include "core/state.h"
typedef struct policy_context_t {
const state_t *state;
const rules_t *rules;
const event_t *event;
} policy_context_t;
bool policy_route_event(const policy_context_t *ctx, command_buffer_t *out);
bool policy_apply_command(
const policy_context_t *ctx,
const command_buffer_t *command_buffer,
plan_t *plan
);

View File

@@ -1,84 +0,0 @@
#include "core/rules.h"
#include <stddef.h>
#include <string.h>
#include <zdwm/types.h>
#include "base/memory.h"
#include "core/types.h"
bool rules_move(rules_t *src, rules_t *dest) {
if (!dest || !src) return false;
dest->items = src->items;
dest->count = src->count;
dest->capacity = src->capacity;
src->items = nullptr;
src->count = 0;
src->capacity = 0;
return true;
}
void rules_cleanup(rules_t *rules) {
for (size_t i = 0; i < rules->count; ++i) {
rule_match_t *match = &rules->items[i].match;
rule_action_t *action = &rules->items[i].action;
p_delete(&match->app_id);
p_delete(&match->role);
p_delete(&match->class_name);
p_delete(&match->instance_name);
p_clear(action, 1);
action->workspace = ZDWM_WORKSPACE_ID_INVALID;
}
p_delete(&rules->items);
rules->capacity = 0;
rules->count = 0;
}
static inline bool str_match(const char *pattern, const char *value) {
return !pattern || (value && strcmp(pattern, value) == 0);
}
static bool
rule_match_window(const rule_match_t *match, const window_metadata_t *meta) {
return str_match(match->app_id, meta->app_id) &&
str_match(match->role, meta->role) &&
str_match(match->class_name, meta->class_name) &&
str_match(match->instance_name, meta->instance_name);
}
static void rule_action_merge(const rule_action_t *src, rule_action_t *dest) {
if (!src || !dest) return;
if (src->workspace != ZDWM_WORKSPACE_ID_INVALID) {
dest->workspace = src->workspace;
}
dest->switch_to_workspace |= src->switch_to_workspace;
dest->fullscreen |= src->fullscreen;
dest->maximize |= src->maximize;
dest->floating |= src->floating;
}
bool rules_resolve(
const rules_t *rules,
const window_metadata_t *metadata,
rule_action_t *action_out
) {
if (!action_out) return false;
bool matched = false;
for (size_t i = 0; i < rules->count; ++i) {
if (!rule_match_window(&rules->items[i].match, metadata)) continue;
matched = true;
rule_action_merge(&rules->items[i].action, action_out);
}
return matched;
}

View File

@@ -1,26 +0,0 @@
#pragma once
#include <stddef.h>
#include "core/types.h"
#include "core/window.h"
typedef struct rule_item_t {
rule_match_t match;
rule_action_t action;
} rule_item_t;
typedef struct rules_t {
rule_item_t *items;
size_t count;
size_t capacity;
} rules_t;
bool rules_move(rules_t *src, rules_t *dest);
void rules_cleanup(rules_t *rules);
bool rules_resolve(
const rules_t *rules,
const window_metadata_t *metadata,
rule_action_t *action_out
);

View File

@@ -1,132 +0,0 @@
#include "core/runtime.h"
#include <dlfcn.h>
#include "base/memory.h"
#include "core/backend.h"
#include "core/command_buffer.h"
#include "core/event.h"
#include "core/layout.h"
#include "core/plan.h"
#include "core/policy.h"
#include "core/rules.h"
#include "core/state.h"
#include "core/wm_desc.h"
static bool runtime_workspace_desc_has_valid_layouts(
const layout_registry_t *layouts,
const workspace_desc_t *workspace
) {
if (!layouts || !workspace) return false;
for (size_t i = 0; i < workspace->layout_count; i++) {
if (!layout_slot_get(layouts, workspace->layout_ids[i])) return false;
}
return true;
}
static bool runtime_init_desc_valid(const runtime_init_desc_t *desc) {
if (!desc || !desc->backend || !desc->outputs || desc->output_count == 0) {
return false;
}
if (!desc->layouts.slots || desc->layouts.slot_count == 0) return false;
if (!desc->workspaces || desc->workspace_count == 0) return false;
for (size_t i = 0; i < desc->workspace_count; i++) {
const workspace_desc_t *workspace = &desc->workspaces[i];
if (!workspace_desc_valid(workspace, desc->output_count)) return false;
if (!runtime_workspace_desc_has_valid_layouts(&desc->layouts, workspace)) {
return false;
}
}
return true;
}
bool runtime_init(runtime_t *runtime, runtime_init_desc_t *desc) {
if (!runtime || !runtime_init_desc_valid(desc)) return false;
p_clear(runtime, 1);
runtime->backend = desc->backend;
layout_registry_move(&desc->layouts, &runtime->layouts);
rules_move(&desc->rules, &runtime->rules);
runtime->config_module_handle = desc->config_module_handle;
desc->backend = nullptr;
desc->config_module_handle = nullptr;
state_init(
&runtime->state,
desc->outputs,
desc->output_count,
desc->workspaces,
desc->workspace_count
);
workspace_desc_list_cleanup(&desc->workspaces, &desc->workspace_count);
desc->outputs = nullptr;
desc->output_count = 0;
return true;
}
void runtime_init_desc_cleanup(runtime_init_desc_t *desc) {
if (!desc) return;
if (desc->backend) {
backend_destroy(desc->backend);
desc->backend = nullptr;
}
if (desc->layouts.slots || desc->layouts.slot_count ||
desc->layouts.slot_capacity) {
layout_registry_cleanup(&desc->layouts);
}
workspace_desc_list_cleanup(&desc->workspaces, &desc->workspace_count);
desc->outputs = nullptr;
desc->output_count = 0;
if (desc->config_module_handle) dlclose(desc->config_module_handle);
desc->config_module_handle = nullptr;
}
void runtime_shutdown(runtime_t *runtime) {
runtime->running = false;
plan_cleanup(&runtime->plan);
command_buffer_cleanup(&runtime->command_buffer);
layout_registry_cleanup(&runtime->layouts);
rules_cleanup(&runtime->rules);
state_cleanup(&runtime->state);
backend_destroy(runtime->backend);
runtime->backend = nullptr;
if (runtime->config_module_handle) dlclose(runtime->config_module_handle);
runtime->config_module_handle = nullptr;
}
void runtime_run(runtime_t *runtime) {
runtime->running = true;
backend_t *backend = runtime->backend;
command_buffer_t *command_buffer = &runtime->command_buffer;
plan_t *plan = &runtime->plan;
while (runtime->running) {
event_t event = {0};
if (!backend_next_event(backend, &event)) break;
command_buffer_reset(command_buffer);
plan_reset(plan);
policy_context_t ctx = {
.state = &runtime->state,
.rules = &runtime->rules,
.event = &event,
};
policy_route_event(&ctx, command_buffer);
policy_apply_command(&ctx, command_buffer, plan);
if (plan->count) backend_apply_effect(backend, plan->effects, plan->count);
event_cleanup(&event);
}
}

View File

@@ -1,40 +0,0 @@
#pragma once
#include <stddef.h>
#include "core/backend.h"
#include "core/command_buffer.h"
#include "core/layout.h"
#include "core/plan.h"
#include "core/rules.h"
#include "core/state.h"
typedef struct runtime_init_desc_t {
backend_t *backend;
output_info_t *outputs;
size_t output_count;
layout_registry_t layouts;
rules_t rules;
workspace_desc_t *workspaces;
size_t workspace_count;
void *config_module_handle;
} runtime_init_desc_t;
typedef struct runtime_t {
bool running;
bool will_restart;
plan_t plan;
command_buffer_t command_buffer;
state_t state;
layout_registry_t layouts;
rules_t rules;
backend_t *backend;
void *config_module_handle;
} runtime_t;
bool runtime_init(runtime_t *runtime, runtime_init_desc_t *desc);
void runtime_init_desc_cleanup(runtime_init_desc_t *desc);
void runtime_shutdown(runtime_t *runtime);
void runtime_run(runtime_t *runtime);

View File

@@ -1,636 +0,0 @@
#include "core/state.h"
#include <stddef.h>
#include <stdint.h>
#include <string.h>
#include <zdwm/types.h>
#include "base/array.h"
#include "base/log.h"
#include "base/memory.h"
#include "core/event.h"
#include "core/layer.h"
#include "core/types.h"
#include "core/wm_desc.h"
void state_init(
state_t *state,
const output_info_t *outputs,
size_t output_count,
const workspace_desc_t *workspaces,
size_t workspace_count
) {
p_clear(state, 1);
state->outputs = p_new(output_t, output_count);
for (size_t i = 0; i < output_count; i++) {
const output_info_t *output_info = &outputs[i];
output_t *output = &state->outputs[i];
output->id = (output_id_t)i;
output->current_workspace_id = ZDWM_WORKSPACE_ID_INVALID;
output->name = p_strdup_nullable(output_info->name);
output->geometry = output_info->geometry;
output->workarea = output_info->geometry;
}
state->output_count = output_count;
state->workspaces = p_new(workspace_t, workspace_count);
for (size_t i = 0; i < workspace_count; i++) {
const workspace_desc_t *workspace_desc = &workspaces[i];
if (!workspace_desc_valid(workspace_desc, output_count)) {
fatal("workspace desc at index %zu invalid", i);
}
output_t *output = &state->outputs[workspace_desc->output_index];
workspace_t *workspace = &state->workspaces[i];
workspace->id = (workspace_id_t)i;
workspace->output_id = output->id;
workspace->focused_window_id = ZDWM_WINDOW_ID_INVALID;
workspace->available_layouts =
p_copy(workspace_desc->layout_ids, workspace_desc->layout_count);
workspace->layout_id = workspace_desc->initial_layout_id;
workspace->layout_count = workspace_desc->layout_count;
workspace->name = p_strdup(workspace_desc->name);
if (output->current_workspace_id == ZDWM_WORKSPACE_ID_INVALID) {
output->current_workspace_id = workspace->id;
}
}
state->workspace_count = workspace_count;
for (size_t i = 0; i < state->output_count; i++) {
const output_t *output = &state->outputs[i];
if (output->current_workspace_id == ZDWM_WORKSPACE_ID_INVALID) {
fatal("output at index %zu has no workspace", i);
}
}
}
void state_cleanup(state_t *state) {
for (size_t i = 0; i < state->window_count; i++) {
window_t *window = &state->windows[i];
p_delete(&window->title);
p_delete(&window->app_id);
p_delete(&window->role);
p_delete(&window->class_name);
p_delete(&window->instance_name);
}
state->window_count = 0;
p_delete(&state->windows);
state->window_capacity = 0;
for (size_t i = 0; i < ZDWM_WINDOW_LAYER_COUNT; ++i) {
layer_stack_t *layer = &state->stacks[i];
p_delete(&layer->order);
layer->order = 0;
layer->capacity = 0;
}
for (size_t i = 0; i < state->workspace_count; i++) {
workspace_t *workspace = &state->workspaces[i];
p_delete(&workspace->name);
p_delete(&workspace->available_layouts);
workspace->layout_count = 0;
}
p_delete(&state->workspaces);
state->workspace_count = 0;
for (size_t i = 0; i < state->output_count; i++) {
output_t *output = &state->outputs[i];
p_delete(&output->name);
}
p_delete(&state->outputs);
state->output_count = 0;
p_clear(state, 1);
}
const workspace_t *
state_workspace_get(const state_t *state, workspace_id_t id) {
if (id < state->workspace_count) return &state->workspaces[id];
return nullptr;
}
const workspace_t *state_workspace_at(const state_t *state, size_t index) {
if (index < state->workspace_count) return &state->workspaces[index];
return nullptr;
}
static void state_workspace_adjust_focused_window(
const state_t *state,
workspace_id_t workspace_id
) {
workspace_t *workspace =
(workspace_t *)state_workspace_get(state, workspace_id);
if (!workspace) return;
const window_t *window =
state_window_get(state, workspace->focused_window_id);
if (window && window->workspace_id == workspace_id) return;
for (size_t i = ZDWM_WINDOW_LAYER_TOP; i >= ZDWM_WINDOW_LAYER_NORMAL; --i) {
const layer_stack_t *layer = &state->stacks[i];
for (size_t i = layer->count; i > 0; --i) {
window_id_t window_id = layer->order[i - 1];
window = state_window_get(state, window_id);
if (window && window->workspace_id == workspace_id) {
workspace->focused_window_id = window_id;
return;
}
}
}
workspace->focused_window_id = ZDWM_WINDOW_ID_INVALID;
}
bool state_workspace_cycle_layout(state_t *state, workspace_id_t workspace_id) {
workspace_t *workspace =
(workspace_t *)state_workspace_get(state, workspace_id);
if (!workspace) return false;
size_t next_index = 0;
bool matched = false;
for (size_t i = 0; i < workspace->layout_count; i++) {
if (workspace->layout_id == workspace->available_layouts[i]) {
next_index = (i + 1) % workspace->layout_count;
matched = true;
break;
}
}
if (!matched) return false;
auto next_layout_id = workspace->available_layouts[next_index];
workspace->layout_id = next_layout_id;
return true;
}
bool state_workspace_set_layout_by_index(
state_t *state,
workspace_id_t workspace_id,
size_t index
) {
workspace_t *workspace =
(workspace_t *)state_workspace_get(state, workspace_id);
if (!workspace) return false;
if (index >= workspace->layout_count) return false;
workspace->layout_id = workspace->available_layouts[index];
return true;
}
bool state_workspace_set_layout_by_id(
state_t *state,
workspace_id_t workspace_id,
layout_id_t layout_id
) {
workspace_t *workspace =
(workspace_t *)state_workspace_get(state, workspace_id);
if (!workspace) return false;
for (size_t i = 0; i < workspace->layout_count; i++) {
if (workspace->available_layouts[i] == layout_id) {
workspace->layout_id = layout_id;
return true;
}
}
return false;
}
void state_workspace_set_focused_window(
state_t *state,
workspace_id_t workspace_id,
window_id_t window_id
) {
workspace_t *workspace =
(workspace_t *)state_workspace_get(state, workspace_id);
if (!workspace) return;
for (size_t i = 0; i < state->window_count; i++) {
window_t *window = &state->windows[i];
if (window->id == window_id && window->workspace_id == workspace_id) {
workspace->focused_window_id = window_id;
return;
}
}
state_workspace_adjust_focused_window(state, workspace_id);
}
size_t state_workspace_count(const state_t *state) {
return state->workspace_count;
}
bool state_workspace_valid(const state_t *state, workspace_id_t id) {
return id < state->workspace_count;
/* 线性扫描作为备用 */
#if 0
if (id >= state->workspace_count) return false;
for (size_t i = 0; i < state->workspace_count; i++) {
if (state->workspaces[i].id == id) return true;
}
return false;
#endif
}
const output_t *state_output_get(const state_t *state, output_id_t id) {
if (id < state->output_count) return &state->outputs[id];
return nullptr;
}
const output_t *state_output_at(const state_t *state, size_t index) {
if (index < state->output_count) return &state->outputs[index];
return nullptr;
}
void state_output_set_workarea(
state_t *state,
output_id_t output_id,
rect_t workarea
) {
output_t *output = (output_t *)state_output_get(state, output_id);
if (output) output->workarea = workarea;
}
void state_output_set_current_workspace(
state_t *state,
output_id_t output_id,
workspace_id_t workspace_id
) {
output_t *output = (output_t *)state_output_get(state, output_id);
if (!output) return;
const workspace_t *workspace = state_workspace_get(state, workspace_id);
if (!workspace || workspace->output_id != output_id) return;
output->current_workspace_id = workspace_id;
}
size_t state_output_count(const state_t *state) { return state->output_count; }
bool state_output_valid(const state_t *state, output_id_t id) {
return id < state->output_count;
/* 线性扫描作为备用 */
#if 0
if (id >= state->output_count) return false;
for (size_t i = 0; i < state->output_count; i++) {
if (state->outputs[i].id == id) return true;
}
return false;
#endif
}
void state_cycle_current_output(state_t *state, int delta) {
int64_t count = (int64_t)state->output_count;
int64_t current = (int64_t)state->current_output_index;
int64_t next = (current + (int64_t)delta) % count;
if (next < 0) next += count;
state->current_output_index = (size_t)next;
}
static inline void window_set_geometry_mode(
window_t *window,
window_geometry_mode_t geometry_mode
) {
window->geometry_mode = geometry_mode;
}
static inline void window_set_floating(window_t *window, bool floating) {
window->floating = floating;
if (floating) window->float_rect = window->frame_rect;
}
static inline void window_set_sticky(window_t *window, bool sticky) {
window->sticky = sticky;
if (sticky) window_set_floating(window, true);
}
static inline void window_set_urgent(window_t *window, bool urgent) {
window->urgent = urgent;
}
static inline void window_set_fixed_size(window_t *window, bool fixed_size) {
window->fixed_size = fixed_size;
if (fixed_size) window_set_floating(window, true);
}
static inline void window_set_float_rect(window_t *window, rect_t rect) {
window->float_rect = rect;
}
static inline void window_set_frame_rect(window_t *window, rect_t rect) {
window->frame_rect = rect;
}
static inline void window_set_title(window_t *window, const char *title) {
p_delete(&window->title);
window->title = p_strdup_nullable(title);
}
static inline void window_set_app_id(window_t *window, const char *app_id) {
p_delete(&window->app_id);
window->app_id = p_strdup_nullable(app_id);
}
static inline void window_set_role(window_t *window, const char *role) {
p_delete(&window->role);
window->role = p_strdup_nullable(role);
}
static inline void window_set_class(window_t *window, const char *class_name) {
p_delete(&window->class_name);
window->class_name = p_strdup_nullable(class_name);
}
static inline void
window_set_instance(window_t *window, const char *instance_name) {
p_delete(&window->instance_name);
window->instance_name = p_strdup_nullable(instance_name);
}
static inline void
window_set_skip_taskbar(window_t *window, bool skip_taskbar) {
window->skip_taskbar = skip_taskbar;
}
const window_t *state_window_add(state_t *state, const window_info_t *info) {
window_id_t id = info->id;
window_t *window = (window_t *)state_window_get(state, id);
if (!window) {
window =
array_push(state->windows, state->window_count, state->window_capacity);
layer_stack_t *layer = &state->stacks[info->layer_type];
layer_stack_append(layer, id);
p_clear(window, 1);
window->id = id;
window->transient_for = info->transient_for;
window->workspace_id = ZDWM_WORKSPACE_ID_INVALID;
window->layer = info->layer_type;
}
window_set_geometry_mode(window, info->geometry_mode);
window_set_urgent(window, info->urgent);
window_set_fixed_size(window, info->fixed_size);
window_set_frame_rect(window, info->frame_rect);
window_set_title(window, info->title);
window_set_app_id(window, info->app_id);
window_set_role(window, info->role);
window_set_class(window, info->class_name);
window_set_instance(window, info->instance_name);
window_set_skip_taskbar(window, info->skip_taskbar);
return window;
}
const window_t *state_window_get(const state_t *state, window_id_t id) {
for (size_t i = 0; i < state->window_count; i++) {
if (state->windows[i].id == id) return &state->windows[i];
}
return nullptr;
}
const window_t *state_window_at(const state_t *state, size_t index) {
if (index < state->window_count) return &state->windows[index];
return nullptr;
}
void state_window_remove(state_t *state, window_id_t id) {
bool matched = false;
size_t index = 0;
for (size_t i = 0; i < state->window_count; i++) {
if (state->windows[i].id == id) {
matched = true;
index = i;
break;
}
}
if (!matched) return;
window_t *window = &state->windows[index];
layer_type_t layer_type = window->layer;
workspace_id_t workspace_id = window->workspace_id;
p_delete(&window->title);
p_delete(&window->app_id);
p_delete(&window->role);
p_delete(&window->class_name);
p_delete(&window->instance_name);
for (size_t i = index + 1; i < state->window_count; i++) {
state->windows[i - 1] = state->windows[i];
}
window = &state->windows[state->window_count - 1];
p_clear(window, 1);
window->id = ZDWM_WINDOW_ID_INVALID;
window->workspace_id = ZDWM_WORKSPACE_ID_INVALID;
state->window_count--;
layer_stack_t *layer = &state->stacks[layer_type];
if (layer) {
layer_stack_remove(layer, id);
}
workspace_t *workspace =
(workspace_t *)state_workspace_get(state, workspace_id);
if (workspace && workspace->focused_window_id == id) {
state_workspace_adjust_focused_window(state, workspace_id);
}
}
size_t state_window_count(const state_t *state) { return state->window_count; }
void state_window_set_workspace(
state_t *state,
window_id_t window_id,
workspace_id_t workspace_id
) {
const workspace_t *workspace = state_workspace_get(state, workspace_id);
window_t *window = (window_t *)state_window_get(state, window_id);
if (!workspace || !window || window->workspace_id == workspace_id) return;
workspace = state_workspace_get(state, window->workspace_id);
window->workspace_id = workspace_id;
if (workspace && workspace->focused_window_id == window_id) {
state_workspace_adjust_focused_window(state, workspace->id);
}
state_workspace_adjust_focused_window(state, workspace_id);
}
void state_window_set_geometry_mode(
state_t *state,
window_id_t window_id,
window_geometry_mode_t geometry_mode
) {
window_t *window = (window_t *)state_window_get(state, window_id);
if (window) window_set_geometry_mode(window, geometry_mode);
}
void state_window_set_floating(
state_t *state,
window_id_t window_id,
bool floating
) {
window_t *window = (window_t *)state_window_get(state, window_id);
if (window) window_set_floating(window, floating);
}
void state_window_set_sticky(
state_t *state,
window_id_t window_id,
bool sticky
) {
window_t *window = (window_t *)state_window_get(state, window_id);
if (window) window_set_sticky(window, sticky);
}
void state_window_set_urgent(
state_t *state,
window_id_t window_id,
bool urgent
) {
window_t *window = (window_t *)state_window_get(state, window_id);
if (window) window_set_urgent(window, urgent);
}
void state_window_set_fixed_size(
state_t *state,
window_id_t window_id,
bool fixed_size
) {
window_t *window = (window_t *)state_window_get(state, window_id);
if (window) window_set_fixed_size(window, fixed_size);
}
void state_window_set_skip_taskbar(
state_t *state,
window_id_t window_id,
bool skip_taskbar
) {
window_t *window = (window_t *)state_window_get(state, window_id);
if (window) window_set_skip_taskbar(window, skip_taskbar);
}
void state_window_set_float_rect(
state_t *state,
window_id_t window_id,
rect_t float_rect
) {
window_t *window = (window_t *)state_window_get(state, window_id);
if (window) window_set_float_rect(window, float_rect);
}
void state_window_set_frame_rect(
state_t *state,
window_id_t window_id,
rect_t frame_rect
) {
window_t *window = (window_t *)state_window_get(state, window_id);
if (window) window_set_frame_rect(window, frame_rect);
}
bool state_window_take_metadata(
state_t *state,
window_id_t window_id,
window_metadata_t *metadata,
uint32_t changed_fields
) {
window_t *window = (window_t *)state_window_get(state, window_id);
if (!window) return false;
if (changed_fields & ZDWM_WINDOW_METADATA_CHANGE_TITLE) {
p_take(&window->title, &metadata->title);
}
if (changed_fields & ZDWM_WINDOW_METADATA_CHANGE_APP_ID) {
p_take(&window->app_id, &metadata->app_id);
}
if (changed_fields & ZDWM_WINDOW_METADATA_CHANGE_ROLE) {
p_take(&window->role, &metadata->role);
}
if (changed_fields & ZDWM_WINDOW_METADATA_CHANGE_CLASS) {
p_take(&window->class_name, &metadata->class_name);
}
if (changed_fields & ZDWM_WINDOW_METADATA_CHANGE_INSTANCE) {
p_take(&window->instance_name, &metadata->instance_name);
}
return true;
}
bool state_window_set_title(
state_t *state,
window_id_t window_id,
const char *title
) {
window_t *window = (window_t *)state_window_get(state, window_id);
if (!window) return false;
window_set_title(window, title);
return true;
}
bool state_window_set_app_id(
state_t *state,
window_id_t window_id,
const char *app_id
) {
window_t *window = (window_t *)state_window_get(state, window_id);
if (!window) return false;
window_set_app_id(window, app_id);
return true;
}
bool state_window_set_role(
state_t *state,
window_id_t window_id,
const char *role
) {
window_t *window = (window_t *)state_window_get(state, window_id);
if (!window) return false;
window_set_role(window, role);
return true;
}
bool state_window_set_class(
state_t *state,
window_id_t window_id,
const char *class_name
) {
window_t *window = (window_t *)state_window_get(state, window_id);
if (!window) return false;
window_set_class(window, class_name);
return true;
}
bool state_window_set_instance(
state_t *state,
window_id_t window_id,
const char *instance_name
) {
window_t *window = (window_t *)state_window_get(state, window_id);
if (!window) return false;
window_set_instance(window, instance_name);
return true;
}
bool state_stack_raise(state_t *state, window_id_t window_id) {
const window_t *window = state_window_get(state, window_id);
if (!window) return false;
layer_stack_t *layer = &state->stacks[window->layer];
if (!layer) return false;
return layer_stack_raise(layer, window_id);
}
bool state_stack_lower(state_t *state, window_id_t window_id) {
const window_t *window = state_window_get(state, window_id);
if (!window) return false;
layer_stack_t *layer = &state->stacks[window->layer];
if (!layer) return false;
return layer_stack_lower(layer, window_id);
}

View File

@@ -1,264 +0,0 @@
#pragma once
#include <stddef.h>
#include <stdint.h>
#include "core/layer.h"
#include "core/types.h"
#include "core/window.h"
#include "core/wm_desc.h"
/* 核心实体定义 */
typedef struct window_t {
window_id_t id;
window_id_t transient_for;
workspace_id_t workspace_id;
layer_type_t layer;
window_geometry_mode_t geometry_mode;
bool floating;
bool sticky;
bool urgent;
bool fixed_size;
/* 几何信息(均为包含边框后的外框矩形) */
rect_t float_rect; /* floating 模式下记忆的外框矩形 */
rect_t frame_rect; /* 当前外框矩形(由 layout 或 float_rect 解析) */
/* 元数据(核心算法不依赖,仅用于规则匹配和信息展示) */
char *title;
char *app_id;
char *role;
char *class_name;
char *instance_name;
bool skip_taskbar;
} window_t;
typedef struct workspace_t {
workspace_id_t id;
output_id_t output_id; /* 固定归属某个输出 */
layout_id_t layout_id; /* 当前活动布局(可用布局列表中的一个) */
window_id_t focused_window_id;
/* 当前 workspace 的可用布局列表 */
const layout_id_t *available_layouts;
size_t layout_count;
/* 名称(核心算法不依赖) */
const char *name;
} workspace_t;
typedef struct output_t {
output_id_t id;
workspace_id_t current_workspace_id;
const char *name;
rect_t geometry; /* 输出完整几何 */
rect_t workarea; /* 可用区域(排除面板等) */
} output_t;
/* 全局状态容器 */
typedef struct state_t {
/* 按 id 稳定引用的 workspace / output 集合 */
workspace_t *workspaces;
size_t workspace_count;
output_t *outputs;
size_t output_count;
size_t current_output_index;
window_t *windows;
size_t window_count;
size_t window_capacity;
/* 分层堆叠顺序:每层内部从低到高排列 */
layer_stack_t stacks[ZDWM_WINDOW_LAYER_COUNT];
} state_t;
/*
* 调用约束:
* - state 必须是有效的非空指针
* - state_init() / state_cleanup() 必须成对调用
* - 同一个 state 生命周期内state_init() 不允许重复调用
* - 除 state_init() 外,其余 state 相关接口都要求 state 已初始化
* - workspaces[i] 必须满足 workspace_desc_valid(&workspaces[i],
* output_count)
* - 传入空指针、未初始化对象或违反前置条件属于调用方错误
*
* 初始化时根据描述表构建 outputs[] 与 workspaces[]。
* workspace_id 由 state 按描述表顺序分配并保持稳定,不从 desc 中读取。
* 每个 output 的 current_workspace_id 会自动设置为首个归属到该 output 的
* workspace若某个 output 没有任何归属 workspacestate_init() 会失败。
*/
void state_init(
state_t *state,
const output_info_t *outputs,
size_t output_count,
const workspace_desc_t *workspaces,
size_t workspace_count
);
void state_cleanup(state_t *state);
/*
* state 持有的 workspace 集合接口
*
* 这组接口对外只提供只读访问。
* workspace 的运行期可变状态必须通过专门的 state 级更新接口修改。
*/
const workspace_t *state_workspace_get(const state_t *state, workspace_id_t id);
const workspace_t *state_workspace_at(const state_t *state, size_t index);
bool state_workspace_cycle_layout(state_t *state, workspace_id_t workspace_id);
bool state_workspace_set_layout_by_index(
state_t *state,
workspace_id_t workspace_id,
size_t index
);
bool state_workspace_set_layout_by_id(
state_t *state,
workspace_id_t workspace_id,
layout_id_t layout_id
);
void state_workspace_set_focused_window(
state_t *state,
workspace_id_t workspace_id,
window_id_t window_id
);
size_t state_workspace_count(const state_t *state);
bool state_workspace_valid(const state_t *state, workspace_id_t id);
/*
* state 持有的 output 集合接口
*
* 这组接口对外只提供只读访问。
* output 的运行期可变状态必须通过专门的 state 级更新接口修改。
*/
const output_t *state_output_get(const state_t *state, output_id_t id);
const output_t *state_output_at(const state_t *state, size_t index);
void state_output_set_workarea(
state_t *state,
output_id_t output_id,
rect_t workarea
);
void state_output_set_current_workspace(
state_t *state,
output_id_t output_id,
workspace_id_t workspace_id
);
size_t state_output_count(const state_t *state);
bool state_output_valid(const state_t *state, output_id_t id);
void state_cycle_current_output(state_t *state, int delta);
/*
* state 持有的 window 集合接口
*
* 这组接口负责管理 state_t.windows[] 容器本身,并维护分层的堆叠顺序。
* 接口返回的 window 对象仅提供只读访问。
* window 的运行期可变状态必须通过专门的 state 级更新接口修改。
*/
/*
* 添加窗口,并将其追加到对应堆栈顶部
*
* info 提供窗口的基础信息
* workspace 归属与其他策略相关字段由后续 state_window_* 接口设置。
*/
const window_t *state_window_add(state_t *state, const window_info_t *info);
const window_t *state_window_get(const state_t *state, window_id_t id);
const window_t *state_window_at(const state_t *state, size_t index);
/* 删除窗口,并同步将其从对应的堆叠栈中移除 */
void state_window_remove(state_t *state, window_id_t id);
size_t state_window_count(const state_t *state);
/* state 持有的单个 window 状态更新接口 */
void state_window_set_workspace(
state_t *state,
window_id_t window_id,
workspace_id_t workspace_id
);
void state_window_set_geometry_mode(
state_t *state,
window_id_t window_id,
window_geometry_mode_t geometry_mode
);
void state_window_set_floating(
state_t *state,
window_id_t window_id,
bool floating
);
void state_window_set_sticky(
state_t *state,
window_id_t window_id,
bool sticky
);
void state_window_set_urgent(
state_t *state,
window_id_t window_id,
bool urgent
);
void state_window_set_fixed_size(
state_t *state,
window_id_t window_id,
bool fixed_size
);
void state_window_set_skip_taskbar(
state_t *state,
window_id_t window_id,
bool skip_taskbar
);
void state_window_set_float_rect(
state_t *state,
window_id_t window_id,
rect_t float_rect
);
void state_window_set_frame_rect(
state_t *state,
window_id_t window_id,
rect_t frame_rect
);
/**
* @brief 按字段掩码转移窗口元数据的所有权
* @param state 状态实例指针
* @param window_id 目标窗口 id
* @param metadata 元数据来源;成功时仅被转移字段会被置空
* @param changed_fields 需要转移的字段掩码
* @return 成功返回 true若目标窗口不存在则返回 false
* @details
* 成功时,仅转移 changed_fields 指定的字段到目标 window并将 metadata 中对应
* 字段置空;未置位字段保持在 metadata 中,所有权仍归调用方。
*
* 失败时,不接管 metadata 中任何字段的所有权metadata 保持不变,仍由调用方负
* 责后续访问与释放。
*/
bool state_window_take_metadata(
state_t *state,
window_id_t window_id,
window_metadata_t *metadata,
uint32_t changed_fields
);
bool state_window_set_title(
state_t *state,
window_id_t window_id,
const char *title
);
bool state_window_set_app_id(
state_t *state,
window_id_t window_id,
const char *app_id
);
bool state_window_set_role(
state_t *state,
window_id_t window_id,
const char *role
);
bool state_window_set_class(
state_t *state,
window_id_t window_id,
const char *class_name
);
bool state_window_set_instance(
state_t *state,
window_id_t window_id,
const char *instance_name
);
bool state_stack_raise(state_t *state, window_id_t window_id);
bool state_stack_lower(state_t *state, window_id_t window_id);

View File

@@ -1,42 +0,0 @@
#pragma once
#include <stdint.h>
#include <zdwm/rules.h>
#include <zdwm/types.h>
/* clang-format off */
typedef zdwm_layout_id_t layout_id_t;
typedef zdwm_window_id_t window_id_t;
typedef zdwm_workspace_id_t workspace_id_t;
typedef uint32_t output_id_t;
typedef zdwm_rect_t rect_t;
typedef zdwm_output_info_t output_info_t;
typedef zdwm_rule_match_t rule_match_t;
typedef zdwm_rule_action_t rule_action_t;
/* clang-format on */
#define ZDWM_OUTPUT_ID_INVALID ((output_id_t)UINT32_MAX)
typedef struct point_t {
int32_t x;
int32_t y;
} point_t;
typedef enum window_geometry_mode_t {
ZDWM_GEOMETRY_NORMAL,
ZDWM_GEOMETRY_MAXIMIZED,
ZDWM_GEOMETRY_FULLSCREEN,
ZDWM_GEOMETRY_MINIMIZED,
} window_geometry_mode_t;
typedef enum focus_direction_t {
ZDWM_FOCUS_PREV,
ZDWM_FOCUS_NEXT,
} focus_direction_t;
typedef enum cross_output_policy_t {
ZDWM_CROSS_OUTPUT_KEEP_WORKSPACE,
ZDWM_CROSS_OUTPUT_MOVE_TO_TARGET_WORKSPACE,
} cross_output_policy_t;

View File

@@ -1,45 +0,0 @@
#pragma once
#include <stddef.h>
/* Internal semantic window types mapped from backend-specific protocols. */
typedef enum window_type_t {
ZDWM_WINDOW_TYPE_NORMAL = 0,
ZDWM_WINDOW_TYPE_DESKTOP,
ZDWM_WINDOW_TYPE_DOCK,
ZDWM_WINDOW_TYPE_TOOLBAR,
ZDWM_WINDOW_TYPE_DIALOG,
ZDWM_WINDOW_TYPE_UTILITY,
ZDWM_WINDOW_TYPE_SPLASH,
ZDWM_WINDOW_TYPE_MENU,
ZDWM_WINDOW_TYPE_DROPDOWN_MENU,
ZDWM_WINDOW_TYPE_POPUP_MENU,
ZDWM_WINDOW_TYPE_TOOLTIP,
ZDWM_WINDOW_TYPE_COMBO,
ZDWM_WINDOW_TYPE_DND,
ZDWM_WINDOW_TYPE_NOTIFICATION,
} window_type_t;
/* Internal semantic window states mapped from backend-specific protocols. */
typedef enum window_state_t {
ZDWM_WINDOW_STATE_ABOVE = 0,
ZDWM_WINDOW_STATE_FULLSCREEN,
ZDWM_WINDOW_STATE_MODAL,
ZDWM_WINDOW_STATE_STICKY,
} window_state_t;
typedef struct window_layer_props_t {
window_type_t *types;
size_t type_count;
window_state_t *states;
size_t state_count;
} window_layer_props_t;
typedef struct window_metadata_t {
char *title;
char *app_id;
char *role;
char *class_name;
char *instance_name;
} window_metadata_t;

View File

@@ -1,87 +0,0 @@
#pragma once
#include <stddef.h>
#include "base/memory.h"
#include "core/layer.h"
#include "core/types.h"
/*
* 由 backend 提供的窗口基础信息。
*
* 这些字符串由调用方提供state 如需长期持有,应自行复制。
*/
typedef struct window_info_t {
window_id_t id;
window_id_t transient_for;
rect_t frame_rect;
const char *title;
const char *app_id;
const char *role;
const char *class_name;
const char *instance_name;
window_geometry_mode_t geometry_mode;
layer_type_t layer_type;
bool urgent;
bool fixed_size;
bool skip_taskbar;
} window_info_t;
typedef struct workspace_desc_t {
size_t output_index; /* 对应 state_init() 中 outputs[] 的索引 */
const char *name;
const layout_id_t *layout_ids;
size_t layout_count;
layout_id_t initial_layout_id;
} workspace_desc_t;
static inline void workspace_desc_cleanup(workspace_desc_t *workspace) {
if (!workspace) return;
p_delete(&workspace->name);
p_delete(&workspace->layout_ids);
workspace->output_index = 0;
workspace->layout_count = 0;
workspace->initial_layout_id = ZDWM_LAYOUT_ID_INVALID;
}
static inline void
workspace_desc_list_cleanup(workspace_desc_t **list, size_t *count) {
if (!list || !count) return;
for (size_t i = 0; i < *count; i++) {
workspace_desc_cleanup(&(*list)[i]);
}
p_delete(list);
*count = 0;
}
/*
* workspace 描述校验接口。
*
* 这组接口只校验描述表自身的一致性,不访问 state。
*/
static inline bool workspace_desc_layouts_valid(
const workspace_desc_t *workspace
) {
if (!workspace || !workspace->layout_count || !workspace->layout_ids) {
return false;
}
for (size_t i = 0; i < workspace->layout_count; i++) {
if (workspace->layout_ids[i] == workspace->initial_layout_id) return true;
}
return false;
}
static inline bool
workspace_desc_valid(const workspace_desc_t *workspace, size_t output_count) {
if (!workspace || !workspace->name) return false;
if (workspace->output_index >= output_count) return false;
return workspace_desc_layouts_valid(workspace);
}

View File

@@ -35,7 +35,7 @@ static const config_status_item_t status_list[] = {
.type = status_net_down,
.icon_type = icon_type_image,
.icon = "/home/zedhugh/develop/zswm/src/resources/icons/"
"corner-left-down-line.png",
"corner-left-down-line.png",
.color = "#87af5f",
},
{
@@ -91,9 +91,9 @@ static const button_t button_list[] = {
};
#define TAGKEYS(KEY, TAG) \
{modifier_super, KEY, select_tag_of_current_monitor, {.ui = TAG}}, { \
modifier_super | modifier_shift, KEY, send_client_to_tag, {.ui = TAG}, \
}
{modifier_super, KEY, select_tag_of_current_monitor, {.ui = TAG}}, { \
modifier_super | modifier_shift, KEY, send_client_to_tag, {.ui = TAG}, \
}
static const char launcher[] =
"rofi -show combi -modes combi -combi-modes window,drun,run,ssh,windowcd";
@@ -105,6 +105,8 @@ static const char browser[] = "firefox-bin";
static const char browser_class[] = "firefox";
static const char chrome[] = "google-chrome-stable";
static const char chrome_class[] = "Google-chrome";
static const char mpv[] = "mpv";
static const char mpv_class[] = "mpv";
static const keyboard_t key_list[] = {
{modifier_super, XK_r, spawn, {.ptr = launcher}},
{modifier_super, XK_Return, spawn, {.ptr = terminal}},
@@ -132,6 +134,12 @@ static const keyboard_t key_list[] = {
raise_or_run,
{.ptr = (const char *[]){chrome_class, chrome}},
},
{
modifier_super,
XK_v,
raise_or_run,
{.ptr = (const char *[]){mpv_class, mpv}},
},
{modifier_super | modifier_shift, XK_q, quit, {.b = false}},
{modifier_super | modifier_control, XK_r, quit, {.b = true}},
{modifier_super | modifier_shift,

View File

@@ -113,8 +113,7 @@ static image_cache_entry_t *image_cache_get(const char *path) {
entry->surface = nullptr;
}
if (!image_load_surface(path, &entry->surface, &entry->width,
&entry->height)) {
if (!image_load_surface(path, &entry->surface, &entry->width, &entry->height)) {
return nullptr;
}
return entry;

View File

@@ -1,7 +1,6 @@
#include "rect.h"
#include "base/macros.h"
#include "base/memory.h"
#include "utils.h"
static inline bool rect_valid(rect_t rect) {
return rect.x1 < rect.x2 && rect.y1 < rect.y2;

View File

@@ -17,8 +17,7 @@ size_t rect_subtract(rect_t source, rect_t clip, rect_t remaining[4]);
* @param source 源矩形
* @param clips 要减去的矩形数组,可为 nullptr
* @param clip_count clips 的长度
* @param remaining 输出剩余矩形数组;若不为
* nullptr则由函数分配内存并由调用方释放
* @param remaining 输出剩余矩形数组;若不为 nullptr则由函数分配内存并由调用方释放
* @return 剩余矩形数量
*
* @code{.c}

View File

@@ -123,8 +123,7 @@ static void tray_get_icon_layout(const tray_icon_t *icon, uint16_t *width,
}
static uint16_t tray_compute_width(void) {
if (!tray.initialized || tray.host_monitor == nullptr ||
tray.icon_count == 0) {
if (!tray.initialized || tray.host_monitor == nullptr || tray.icon_count == 0) {
return 0;
}
@@ -164,10 +163,11 @@ static void tray_select_icon_events(xcb_window_t window) {
xcb_params_cw_t params = {
.back_pixel = wm.color_set.bar_bg.argb,
.border_pixel = 0,
.event_mask =
XCB_EVENT_MASK_PROPERTY_CHANGE | XCB_EVENT_MASK_STRUCTURE_NOTIFY,
.event_mask = XCB_EVENT_MASK_PROPERTY_CHANGE |
XCB_EVENT_MASK_STRUCTURE_NOTIFY,
};
uint32_t mask = XCB_CW_BACK_PIXEL | XCB_CW_BORDER_PIXEL | XCB_CW_EVENT_MASK;
uint32_t mask =
XCB_CW_BACK_PIXEL | XCB_CW_BORDER_PIXEL | XCB_CW_EVENT_MASK;
xcb_aux_change_window_attributes(wm.xcb_conn, window, mask, &params);
xcb_clear_area(wm.xcb_conn, false, window, 0, 0, 0, 0);
}
@@ -180,9 +180,9 @@ static void tray_release_icon_window(tray_icon_t *icon) {
.event_mask = XCB_EVENT_MASK_NO_EVENT,
.border_pixel = 0,
};
xcb_aux_change_window_attributes(wm.xcb_conn, icon->window,
XCB_CW_EVENT_MASK | XCB_CW_BORDER_PIXEL,
&params);
xcb_aux_change_window_attributes(
wm.xcb_conn, icon->window, XCB_CW_EVENT_MASK | XCB_CW_BORDER_PIXEL,
&params);
xcb_reparent_window(wm.xcb_conn, icon->window, wm.screen->root, 0, 0);
}
@@ -299,8 +299,7 @@ static void tray_add_icon(xcb_window_t window) {
static bool tray_create_window(void) {
static xcb_colormap_t colormap = XCB_NONE;
if (tray.host_monitor == nullptr ||
tray.host_monitor->bar_window == XCB_NONE) {
if (tray.host_monitor == nullptr || tray.host_monitor->bar_window == XCB_NONE) {
return false;
}
@@ -320,8 +319,8 @@ static bool tray_create_window(void) {
const xcb_create_window_value_list_t value_list = {
.background_pixel = wm.color_set.bar_bg.argb,
.border_pixel = 0,
.event_mask =
XCB_EVENT_MASK_SUBSTRUCTURE_REDIRECT | XCB_EVENT_MASK_SUBSTRUCTURE_NOTIFY,
.event_mask = XCB_EVENT_MASK_SUBSTRUCTURE_REDIRECT |
XCB_EVENT_MASK_SUBSTRUCTURE_NOTIFY,
.colormap = colormap,
};
@@ -363,8 +362,7 @@ static bool tray_take_selection_owner(void) {
XCB_CURRENT_TIME);
owner_cookie = xcb_get_selection_owner(wm.xcb_conn, tray.selection_atom);
owner_reply =
xcb_get_selection_owner_reply(wm.xcb_conn, owner_cookie, nullptr);
owner_reply = xcb_get_selection_owner_reply(wm.xcb_conn, owner_cookie, nullptr);
bool success = owner_reply && owner_reply->owner == tray.window;
p_delete(&owner_reply);
@@ -380,12 +378,18 @@ static void tray_set_properties(void) {
const color_t *fg = &wm.color_set.tag_color;
uint32_t colors[12] = {
tray_color_component_u16(fg->red), tray_color_component_u16(fg->green),
tray_color_component_u16(fg->blue), tray_color_component_u16(fg->red),
tray_color_component_u16(fg->green), tray_color_component_u16(fg->blue),
tray_color_component_u16(fg->red), tray_color_component_u16(fg->green),
tray_color_component_u16(fg->blue), tray_color_component_u16(fg->red),
tray_color_component_u16(fg->green), tray_color_component_u16(fg->blue),
tray_color_component_u16(fg->red),
tray_color_component_u16(fg->green),
tray_color_component_u16(fg->blue),
tray_color_component_u16(fg->red),
tray_color_component_u16(fg->green),
tray_color_component_u16(fg->blue),
tray_color_component_u16(fg->red),
tray_color_component_u16(fg->green),
tray_color_component_u16(fg->blue),
tray_color_component_u16(fg->red),
tray_color_component_u16(fg->green),
tray_color_component_u16(fg->blue),
};
xcb_change_property(wm.xcb_conn, XCB_PROP_MODE_REPLACE, tray.window,
_NET_SYSTEM_TRAY_COLORS, XCB_ATOM_CARDINAL, 32,

View File

@@ -1,4 +1,4 @@
#include "base/log.h"
#include "utils.h"
#include <stdarg.h>
#include <stdio.h>
@@ -19,6 +19,9 @@ const char *current_time_str(void) {
return buffer;
}
/**
* Print error and exit with EXIT_FAILURE code.
*/
void _fatal(int line, const char *fct, const char *file, const char *fmt, ...) {
va_list ap;
@@ -31,6 +34,9 @@ void _fatal(int line, const char *fct, const char *file, const char *fmt, ...) {
exit(EXIT_FAILURE);
}
/**
* Print error message on stderr
*/
void _warn(int line, const char *fct, const char *file, const char *fmt, ...) {
va_list ap;

View File

@@ -1,8 +1,103 @@
#pragma once
/* Compatibility umbrella header. Prefer including headers from src/base in new
* code. */
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "base/log.h"
#include "base/macros.h"
#include "base/memory.h"
#define ssizeof(foo) (ssize_t)sizeof(foo)
#define countof(foo) (ssizeof(foo) / ssizeof(foo[0]))
#define p_alloc_nr(x) (((x) + 16) * 3 / 2)
#define p_new(type, count) ((type *)xmalloc(sizeof(type) * (count)))
#define p_clear(p, count) ((void)memset((p), 0, sizeof(*(p)) * (count)))
#define p_realloc(pp, count) xrealloc((void *)(pp), sizeof(**(pp)) * (count))
#define p_delete(mem_p) \
do { \
void **__ptr = (void **)(mem_p); \
free(*__ptr); \
*(void **)__ptr = nullptr; \
} while (0)
#ifdef __GNUC__
#define likely(expr) __builtin_expect(!!(expr), 1)
#define unlikely(expr) __builtin_expect((expr), 0)
#else
#define likely(expr) expr
#define unlikely(expr) expr
#endif
static inline void *__attribute__((malloc)) xmalloc(ssize_t size) {
void *ptr;
if (size <= 0) return nullptr;
ptr = calloc(1, size);
if (!ptr) abort();
return ptr;
}
static inline void xrealloc(void **ptr, ssize_t newsize) {
if (newsize <= 0)
p_delete(ptr);
else {
*ptr = realloc(*ptr, newsize);
if (!*ptr) abort();
}
}
/**
* @brief nullptr resistant strlen.
*
* Unlike it's libc sibling, a_strlen returns a ssize_t, and supports its
* argument being nullptr.
*
* @param s the string.
* @return the string length (or 0 if s is nullptr).
*/
static inline ssize_t a_strlen(const char *s) { return s ? strlen(s) : 0; }
#define fatal(format, ...) \
_fatal(__LINE__, __FUNCTION__, __FILE__, format, ##__VA_ARGS__)
void _fatal(int line, const char *function, const char *file,
const char *format, ...) __attribute__((noreturn))
__attribute__((format(printf, 4, 5)));
#define warn(format, ...) \
_fatal(__LINE__, __FUNCTION__, __FILE__, format, ##__VA_ARGS__)
void _warn(int line, const char *function, const char *file, const char *format,
...) __attribute__((format(printf, 4, 5)));
const char *current_time_str(void);
#if defined(RELEASE)
#define logger(format, ...)
#elif defined(LOG_FILE)
static inline void logger(const char *format, ...) {
va_list ap;
va_start(ap, format);
FILE *log_file = fopen(LOG_FILE, "a+t");
vfprintf(log_file, format, ap);
va_end(ap);
fclose(log_file);
}
#else
#define logger(format, ...) printf(format, ##__VA_ARGS__)
#endif
#ifdef MIN
#undef MIN
#endif
#ifndef MIN
#define MIN(a, b) (((a) < (b)) ? (a) : (b))
#endif
#ifdef MAX
#undef MAX
#endif
#ifndef MAX
#define MAX(a, b) (((a) > (b)) ? (a) : (b))
#endif

View File

@@ -2,7 +2,6 @@
#include <stdint.h>
#include <xcb/xproto.h>
#include "types.h"
typedef struct wallpaper_context_t wallpaper_context_t;

View File

@@ -1,3 +0,0 @@
add_subdirectory(backend)
add_subdirectory(config)
add_subdirectory(core)

View File

@@ -1,23 +0,0 @@
set(OUTPUT_UTILS_TEST_APP_NAME "zdwm-output-utils-tests")
add_executable(${OUTPUT_UTILS_TEST_APP_NAME}
${CMAKE_CURRENT_SOURCE_DIR}/output_utils_test.c
${SOURCE_DIR}/backend/output_utils.c
)
target_include_directories(${OUTPUT_UTILS_TEST_APP_NAME} SYSTEM
PRIVATE ${INCLUDE_DIR}
)
target_include_directories(${OUTPUT_UTILS_TEST_APP_NAME}
PRIVATE ${SOURCE_DIR}
PRIVATE ${BUILD_DIR}
)
target_link_libraries(${OUTPUT_UTILS_TEST_APP_NAME}
PRIVATE m
)
add_test(NAME ${OUTPUT_UTILS_TEST_APP_NAME}
COMMAND $<TARGET_FILE:${OUTPUT_UTILS_TEST_APP_NAME}>
)

View File

@@ -1,69 +0,0 @@
#include "backend/output_utils.h"
#include <assert.h>
#include <stdint.h>
#include "core/types.h"
#include "utils.h"
static void assert_output_geometry(
const output_info_t *output,
int32_t x,
int32_t y,
int32_t width,
int32_t height
) {
assert(output);
assert(output->geometry.x == x);
assert(output->geometry.y == y);
assert(output->geometry.width == width);
assert(output->geometry.height == height);
}
static void destroy_detect_result(backend_detect_t *detect) {
assert(detect);
for (size_t i = 0; i < detect->output_count; i++) {
p_delete(&detect->outputs[i].name);
}
p_delete(&detect->outputs);
free(detect);
}
static void test_output_remove_duplication_invalid_input(void) {
output_info_t output = {0};
assert(output_remove_duplication(nullptr, 0) == nullptr);
assert(output_remove_duplication(&output, 0) == nullptr);
}
static void test_output_remove_duplication_keeps_non_nested_outputs(void) {
output_info_t outputs[] = {
{
.geometry = {.x = 0, .y = 0, .width = 3840, .height = 1080},
},
{
.geometry = {.x = 0, .y = 0, .width = 1920, .height = 1080},
},
{
.geometry = {.x = 1920, .y = 0, .width = 1920, .height = 1080},
},
{
.geometry = {.x = 3840, .y = 0, .width = 1280, .height = 1024},
},
};
backend_detect_t *detect =
output_remove_duplication(outputs, countof(outputs));
assert(detect);
assert(detect->output_count == 2);
assert_output_geometry(&detect->outputs[0], 0, 0, 3840, 1080);
assert_output_geometry(&detect->outputs[1], 3840, 0, 1280, 1024);
destroy_detect_result(detect);
}
int main(void) {
test_output_remove_duplication_invalid_input();
test_output_remove_duplication_keeps_non_nested_outputs();
return 0;
}

View File

@@ -1,80 +0,0 @@
set(CONFIG_FIXTURE_TARGET "zdwm-config-fixture")
set(CONFIG_FIXTURE_NO_SETUP_TARGET "zdwm-config-fixture-no-setup")
set(CONFIG_LOADER_TEST_APP_NAME "zdwm-config-loader-tests")
set(RUNTIME_CONFIG_TEST_APP_NAME "zdwm-runtime-config-tests")
add_library(${CONFIG_FIXTURE_TARGET} SHARED
${CMAKE_CURRENT_SOURCE_DIR}/fixture_valid_config.c
)
target_include_directories(${CONFIG_FIXTURE_TARGET}
PRIVATE ${INCLUDE_DIR}
)
add_library(${CONFIG_FIXTURE_NO_SETUP_TARGET} SHARED
${CMAKE_CURRENT_SOURCE_DIR}/fixture_without_setup.c
)
add_executable(${CONFIG_LOADER_TEST_APP_NAME}
${CMAKE_CURRENT_SOURCE_DIR}/config_loader_test.c
${SOURCE_DIR}/base/log.c
${SOURCE_DIR}/config/loader.c
)
target_include_directories(${CONFIG_LOADER_TEST_APP_NAME} SYSTEM
PRIVATE ${INCLUDE_DIR}
)
target_include_directories(${CONFIG_LOADER_TEST_APP_NAME}
PRIVATE ${SOURCE_DIR}
PRIVATE ${BUILD_DIR}
PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}
)
target_link_libraries(${CONFIG_LOADER_TEST_APP_NAME}
PRIVATE ${CMAKE_DL_LIBS}
)
add_dependencies(${CONFIG_LOADER_TEST_APP_NAME}
${CONFIG_FIXTURE_TARGET}
${CONFIG_FIXTURE_NO_SETUP_TARGET}
)
add_executable(${RUNTIME_CONFIG_TEST_APP_NAME}
${CMAKE_CURRENT_SOURCE_DIR}/runtime_config_test.c
${SOURCE_DIR}/base/log.c
${SOURCE_DIR}/config/defaults.c
${SOURCE_DIR}/config/loader.c
${SOURCE_DIR}/config/runtime_config.c
${SOURCE_DIR}/core/command_buffer.c
${SOURCE_DIR}/core/event.c
${SOURCE_DIR}/core/layer.c
${SOURCE_DIR}/core/layout.c
${SOURCE_DIR}/core/plan.c
${SOURCE_DIR}/core/policy.c
${SOURCE_DIR}/core/rules.c
${SOURCE_DIR}/core/runtime.c
${SOURCE_DIR}/core/state.c
)
target_include_directories(${RUNTIME_CONFIG_TEST_APP_NAME} SYSTEM
PRIVATE ${INCLUDE_DIR}
)
target_include_directories(${RUNTIME_CONFIG_TEST_APP_NAME}
PRIVATE ${SOURCE_DIR}
PRIVATE ${BUILD_DIR}
PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}
)
target_link_libraries(${RUNTIME_CONFIG_TEST_APP_NAME}
PRIVATE ${CMAKE_DL_LIBS}
)
add_dependencies(${RUNTIME_CONFIG_TEST_APP_NAME}
${CONFIG_FIXTURE_TARGET}
${CONFIG_FIXTURE_NO_SETUP_TARGET}
)
add_test(NAME ${CONFIG_LOADER_TEST_APP_NAME}
COMMAND $<TARGET_FILE:${CONFIG_LOADER_TEST_APP_NAME}>
$<TARGET_FILE:${CONFIG_FIXTURE_TARGET}>
$<TARGET_FILE:${CONFIG_FIXTURE_NO_SETUP_TARGET}>
)
add_test(NAME ${RUNTIME_CONFIG_TEST_APP_NAME}
COMMAND $<TARGET_FILE:${RUNTIME_CONFIG_TEST_APP_NAME}>
$<TARGET_FILE:${CONFIG_FIXTURE_TARGET}>
$<TARGET_FILE:${CONFIG_FIXTURE_NO_SETUP_TARGET}>
)

View File

@@ -1,200 +0,0 @@
#include <assert.h>
#include <limits.h>
#include <stdbool.h>
#include <stdlib.h>
#include <string.h>
#include "config/loader.h"
#include "helpers.h"
static void test_config_loader_loads_explicit_override(
const char *fixture_path
) {
config_test_clear_env();
config_loader_t loader = {0};
assert(config_loader_load(&loader, fixture_path));
assert(loader.path);
assert(strcmp(loader.path, fixture_path) == 0);
assert(loader.handle);
assert(loader.setup);
config_loader_cleanup(&loader);
}
static void test_config_loader_override_has_priority_over_env(
const char *fixture_path
) {
config_test_clear_env();
assert(
setenv("ZDWM_CONFIG_LIB", "/tmp/definitely-missing-config.so", 1) == 0
);
config_loader_t loader = {0};
assert(config_loader_load(&loader, fixture_path));
assert(loader.path);
assert(strcmp(loader.path, fixture_path) == 0);
assert(loader.handle);
assert(loader.setup);
config_loader_cleanup(&loader);
}
static void test_config_loader_uses_zdwm_config_lib_env(
const char *fixture_path
) {
config_test_clear_env();
assert(setenv("ZDWM_CONFIG_LIB", fixture_path, 1) == 0);
config_loader_t loader = {0};
assert(config_loader_load(&loader, nullptr));
assert(loader.path);
assert(strcmp(loader.path, fixture_path) == 0);
assert(loader.handle);
assert(loader.setup);
config_loader_cleanup(&loader);
}
static void test_config_loader_uses_xdg_config_home(const char *fixture_path) {
config_test_clear_env();
char temp_root[PATH_MAX] = {0};
char config_dir[PATH_MAX] = {0};
char config_path[PATH_MAX] = {0};
config_test_make_temp_dir(
temp_root,
sizeof(temp_root),
"zdwm-config-loader-xdg"
);
config_test_join_path(config_dir, sizeof(config_dir), temp_root, "zdwm");
config_test_join_path(
config_path,
sizeof(config_path),
config_dir,
"config.so"
);
config_test_mkdir(config_dir);
config_test_symlink(fixture_path, config_path);
assert(setenv("XDG_CONFIG_HOME", temp_root, 1) == 0);
config_loader_t loader = {0};
assert(config_loader_load(&loader, nullptr));
assert(loader.path);
assert(strcmp(loader.path, config_path) == 0);
assert(loader.handle);
assert(loader.setup);
config_loader_cleanup(&loader);
config_test_unlink(config_path);
config_test_rmdir(config_dir);
config_test_rmdir(temp_root);
}
static void test_config_loader_uses_home_fallback(const char *fixture_path) {
config_test_clear_env();
char temp_root[PATH_MAX] = {0};
char config_root[PATH_MAX] = {0};
char zdwm_dir[PATH_MAX] = {0};
char config_path[PATH_MAX] = {0};
config_test_make_temp_dir(
temp_root,
sizeof(temp_root),
"zdwm-config-loader-home"
);
config_test_join_path(config_root, sizeof(config_root), temp_root, ".config");
config_test_join_path(zdwm_dir, sizeof(zdwm_dir), config_root, "zdwm");
config_test_join_path(
config_path,
sizeof(config_path),
zdwm_dir,
"config.so"
);
config_test_mkdir(config_root);
config_test_mkdir(zdwm_dir);
config_test_symlink(fixture_path, config_path);
assert(setenv("HOME", temp_root, 1) == 0);
config_loader_t loader = {0};
assert(config_loader_load(&loader, nullptr));
assert(loader.path);
assert(strcmp(loader.path, config_path) == 0);
assert(loader.handle);
assert(loader.setup);
config_loader_cleanup(&loader);
config_test_unlink(config_path);
config_test_rmdir(zdwm_dir);
config_test_rmdir(config_root);
config_test_rmdir(temp_root);
}
static void test_config_loader_allows_missing_implicit_library(void) {
config_test_clear_env();
char temp_root[PATH_MAX] = {0};
char expected_path[PATH_MAX] = {0};
config_test_make_temp_dir(
temp_root,
sizeof(temp_root),
"zdwm-config-loader-missing"
);
config_test_join_path(
expected_path,
sizeof(expected_path),
temp_root,
"zdwm/config.so"
);
assert(setenv("XDG_CONFIG_HOME", temp_root, 1) == 0);
config_loader_t loader = {0};
assert(config_loader_load(&loader, nullptr));
assert(loader.path);
assert(strcmp(loader.path, expected_path) == 0);
assert(loader.handle == nullptr);
assert(loader.setup == nullptr);
config_loader_cleanup(&loader);
config_test_rmdir(temp_root);
}
static void test_config_loader_rejects_missing_explicit_library(void) {
config_test_clear_env();
config_loader_t loader = {0};
assert(!config_loader_load(&loader, "/tmp/definitely-missing-config.so"));
assert(loader.path == nullptr);
assert(loader.handle == nullptr);
assert(loader.setup == nullptr);
}
static void test_config_loader_rejects_library_without_setup(
const char *fixture_path
) {
config_test_clear_env();
config_loader_t loader = {0};
assert(!config_loader_load(&loader, fixture_path));
assert(loader.path == nullptr);
assert(loader.handle == nullptr);
assert(loader.setup == nullptr);
}
int main(int argc, char **argv) {
assert(argc == 3);
test_config_loader_loads_explicit_override(argv[1]);
test_config_loader_override_has_priority_over_env(argv[1]);
test_config_loader_uses_zdwm_config_lib_env(argv[1]);
test_config_loader_uses_xdg_config_home(argv[1]);
test_config_loader_uses_home_fallback(argv[1]);
test_config_loader_allows_missing_implicit_library();
test_config_loader_rejects_missing_explicit_library();
test_config_loader_rejects_library_without_setup(argv[2]);
return 0;
}

View File

@@ -1,45 +0,0 @@
#include <zdwm/config.h>
static bool
fixture_layout(const zdwm_layout_ctx_t *ctx, zdwm_layout_result_t *out) {
if (!ctx || !out) return false;
zdwm_layout_result_reset(out);
if (!ctx->window_ids || ctx->window_count == 0) return true;
zdwm_layout_result_push(
out,
(zdwm_layout_item_t){
.window_id = ctx->window_ids[0],
.rect = ctx->workarea,
}
);
return true;
}
bool setup(
const zdwm_api_t *api,
zdwm_config_builder_t *builder,
const zdwm_output_info_t *outputs,
size_t output_count
) {
if (!api || !builder || !outputs || output_count == 0) return false;
zdwm_layout_id_t layout_id = api->register_layout(
builder,
"test-only",
"[T]",
"fixture layout",
fixture_layout
);
if (layout_id == ZDWM_LAYOUT_ID_INVALID) return false;
return api->define_workspace(
builder,
0,
"from-so",
&layout_id,
1,
layout_id
) != ZDWM_WORKSPACE_ID_INVALID;
}

View File

@@ -1 +0,0 @@
int zdwm_config_fixture_without_setup_symbol(void) { return 1; }

View File

@@ -1,51 +0,0 @@
#pragma once
#include <assert.h>
#include <limits.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>
static inline void config_test_clear_env(void) {
assert(unsetenv("ZDWM_CONFIG_LIB") == 0);
assert(unsetenv("XDG_CONFIG_HOME") == 0);
assert(unsetenv("HOME") == 0);
}
static inline void
config_test_make_temp_dir(char *path, size_t path_size, const char *prefix) {
int len = snprintf(path, path_size, "/tmp/%s-XXXXXX", prefix);
assert(len > 0);
assert((size_t)len < path_size);
assert(mkdtemp(path) == path);
}
static inline void config_test_join_path(
char *out,
size_t out_size,
const char *base,
const char *suffix
) {
int len = snprintf(out, out_size, "%s/%s", base, suffix);
assert(len > 0);
assert((size_t)len < out_size);
}
static inline void config_test_mkdir(const char *path) {
assert(mkdir(path, 0700) == 0);
}
static inline void config_test_symlink(const char *target, const char *path) {
assert(symlink(target, path) == 0);
}
static inline void config_test_unlink(const char *path) {
assert(unlink(path) == 0);
}
static inline void config_test_rmdir(const char *path) {
assert(rmdir(path) == 0);
}

View File

@@ -1,233 +0,0 @@
#include "config/runtime_config.h"
#include <assert.h>
#include <limits.h>
#include <stddef.h>
#include <stdlib.h>
#include <string.h>
#include "core/layout.h"
#include "core/runtime.h"
#include "helpers.h"
struct backend_t {
int unused;
};
void backend_destroy(backend_t *backend) { free(backend); }
bool backend_next_event(backend_t *backend, event_t *event) {
(void)backend;
(void)event;
return false;
}
bool backend_apply_effect(
backend_t *backend,
const effect_t *effects,
size_t effect_count
) {
return false;
}
static backend_t *test_backend_create(void) {
return calloc(1, sizeof(backend_t));
}
static void assert_default_layouts(const runtime_init_desc_t *desc) {
assert(desc);
assert(layout_registry_count(&desc->layouts) == 3);
const layout_slot_t *tile = layout_registry_at(&desc->layouts, 0);
const layout_slot_t *monocle = layout_registry_at(&desc->layouts, 1);
const layout_slot_t *floating = layout_registry_at(&desc->layouts, 2);
assert(tile);
assert(monocle);
assert(floating);
assert(strcmp(tile->name, "tile") == 0);
assert(strcmp(monocle->name, "monocle") == 0);
assert(strcmp(floating->name, "floating") == 0);
}
static void test_runtime_config_loads_user_library(const char *fixture_path) {
config_test_clear_env();
output_info_t outputs[] = {
{
.name = "HDMI-A-1",
.geometry = {.x = 0, .y = 0, .width = 1920, .height = 1080},
},
};
runtime_init_desc_t desc = {
.outputs = outputs,
.output_count = 1,
};
assert(runtime_config_load(fixture_path, &desc));
assert(desc.config_module_handle != nullptr);
assert(layout_registry_count(&desc.layouts) == 1);
assert(desc.workspace_count == 1);
assert(strcmp(desc.workspaces[0].name, "from-so") == 0);
assert(desc.workspaces[0].output_index == 0);
assert(desc.workspaces[0].layout_count == 1);
assert(desc.workspaces[0].initial_layout_id == 0);
const layout_slot_t *layout = layout_registry_at(&desc.layouts, 0);
assert(layout);
assert(strcmp(layout->name, "test-only") == 0);
assert(strcmp(layout->symbol, "[T]") == 0);
runtime_config_cleanup(&desc);
}
static void test_runtime_config_keeps_module_loaded_after_runtime_init(
const char *fixture_path
) {
config_test_clear_env();
output_info_t outputs[] = {
{
.name = "HDMI-A-1",
.geometry = {.x = 0, .y = 0, .width = 1920, .height = 1080},
},
};
window_id_t window_ids[] = {42};
runtime_init_desc_t desc = {
.backend = test_backend_create(),
.outputs = outputs,
.output_count = 1,
};
assert(desc.backend);
assert(runtime_config_load(fixture_path, &desc));
runtime_t runtime = {0};
assert(runtime_init(&runtime, &desc));
assert(desc.config_module_handle == nullptr);
runtime_init_desc_cleanup(&desc);
assert(runtime.config_module_handle != nullptr);
layout_fn layout = layout_get(&runtime.layouts, 0);
assert(layout != nullptr);
layout_result_t result = {0};
layout_ctx_t ctx = {
.workspace_id = 0,
.focused_window_id = window_ids[0],
.workarea = {.x = 11, .y = 22, .width = 333, .height = 444},
.window_ids = window_ids,
.window_count = 1,
};
assert(layout(&ctx, &result));
assert(result.item_count == 1);
assert(result.items[0].window_id == window_ids[0]);
assert(result.items[0].rect.x == ctx.workarea.x);
assert(result.items[0].rect.y == ctx.workarea.y);
assert(result.items[0].rect.width == ctx.workarea.width);
assert(result.items[0].rect.height == ctx.workarea.height);
layout_result_cleanup(&result);
runtime_shutdown(&runtime);
assert(runtime.config_module_handle == nullptr);
}
static void
test_runtime_config_falls_back_to_defaults_when_implicit_library_is_missing(
void
) {
config_test_clear_env();
output_info_t outputs[] = {
{
.name = "eDP-1",
.geometry = {.x = 0, .y = 0, .width = 1920, .height = 1080},
},
{
.name = "DP-1",
.geometry = {.x = 1920, .y = 0, .width = 2560, .height = 1440},
},
};
char temp_root[PATH_MAX] = {0};
config_test_make_temp_dir(
temp_root,
sizeof(temp_root),
"zdwm-runtime-config-missing"
);
assert(setenv("XDG_CONFIG_HOME", temp_root, 1) == 0);
runtime_init_desc_t desc = {
.outputs = outputs,
.output_count = 2,
};
assert(runtime_config_load(nullptr, &desc));
assert(desc.config_module_handle == nullptr);
assert_default_layouts(&desc);
assert(desc.workspace_count == 2);
assert(strcmp(desc.workspaces[0].name, "main") == 0);
assert(strcmp(desc.workspaces[1].name, "main") == 0);
assert(desc.workspaces[0].output_index == 0);
assert(desc.workspaces[1].output_index == 1);
runtime_config_cleanup(&desc);
config_test_rmdir(temp_root);
}
static void test_runtime_config_rejects_missing_explicit_library(void) {
config_test_clear_env();
output_info_t outputs[] = {
{
.name = "HDMI-A-1",
.geometry = {.x = 0, .y = 0, .width = 1920, .height = 1080},
},
};
runtime_init_desc_t desc = {
.outputs = outputs,
.output_count = 1,
};
assert(!runtime_config_load("/tmp/definitely-missing-config.so", &desc));
assert(layout_registry_count(&desc.layouts) == 0);
assert(desc.workspace_count == 0);
assert(desc.workspaces == nullptr);
assert(desc.config_module_handle == nullptr);
}
static void test_runtime_config_rejects_library_without_setup(
const char *fixture_path
) {
config_test_clear_env();
output_info_t outputs[] = {
{
.name = "HDMI-A-1",
.geometry = {.x = 0, .y = 0, .width = 1920, .height = 1080},
},
};
runtime_init_desc_t desc = {
.outputs = outputs,
.output_count = 1,
};
assert(!runtime_config_load(fixture_path, &desc));
assert(layout_registry_count(&desc.layouts) == 0);
assert(desc.workspace_count == 0);
assert(desc.workspaces == nullptr);
assert(desc.config_module_handle == nullptr);
}
int main(int argc, char **argv) {
assert(argc == 3);
test_runtime_config_loads_user_library(argv[1]);
test_runtime_config_keeps_module_loaded_after_runtime_init(argv[1]);
test_runtime_config_falls_back_to_defaults_when_implicit_library_is_missing();
test_runtime_config_rejects_missing_explicit_library();
test_runtime_config_rejects_library_without_setup(argv[2]);
return 0;
}

View File

@@ -1,47 +0,0 @@
set(TEST_APP_NAME "zdwm-tests")
add_executable(${TEST_APP_NAME}
${CMAKE_CURRENT_SOURCE_DIR}/main.c
${SOURCE_DIR}/base/log.c
${SOURCE_DIR}/backend/output_utils.c
${SOURCE_DIR}/config/defaults.c
${SOURCE_DIR}/config/loader.c
${SOURCE_DIR}/config/runtime_config.c
${SOURCE_DIR}/backend/x11/backend.c
${SOURCE_DIR}/backend/x11/event.c
${SOURCE_DIR}/backend/x11/window.c
${SOURCE_DIR}/core/command_buffer.c
${SOURCE_DIR}/core/event.c
${SOURCE_DIR}/core/layer.c
${SOURCE_DIR}/core/layout.c
${SOURCE_DIR}/core/plan.c
${SOURCE_DIR}/core/policy.c
${SOURCE_DIR}/core/rules.c
${SOURCE_DIR}/core/runtime.c
${SOURCE_DIR}/core/state.c
)
target_include_directories(${TEST_APP_NAME} SYSTEM
PRIVATE ${deps_INCLUDE_DIRS}
PRIVATE ${INCLUDE_DIR}
)
target_include_directories(${TEST_APP_NAME}
PRIVATE ${SOURCE_DIR}
PRIVATE ${BUILD_DIR}
)
target_link_libraries(${TEST_APP_NAME}
PRIVATE m
PRIVATE ${CMAKE_DL_LIBS}
PRIVATE ${deps_LIBRARIES}
)
if(HAS_EXECINFO AND LIB_EXECINFO)
target_link_libraries(${TEST_APP_NAME} PRIVATE ${LIB_EXECINFO})
endif()
add_test(NAME ${TEST_APP_NAME} COMMAND $<TARGET_FILE:${TEST_APP_NAME}>)
set_tests_properties(${TEST_APP_NAME} PROPERTIES
ENVIRONMENT "DISPLAY=:3"
)

View File

@@ -1,43 +0,0 @@
#include "base/log.h"
#include "config/runtime_config.h"
#include "core/backend.h"
#include "core/runtime.h"
static void bootstrap(runtime_t *runtime) {
backend_t *backend = backend_create(nullptr);
backend_detect_t *detect = backend_detect(backend);
if (!backend || !detect || detect->output_count == 0) {
fatal("backend detect failed");
}
runtime_init_desc_t desc = {
.backend = backend,
.outputs = detect->outputs,
.output_count = detect->output_count,
};
if (!runtime_config_load(nullptr, &desc)) {
fatal("failed to build runtime inputs");
}
backend = nullptr;
bool inited = runtime_init(runtime, &desc);
runtime_init_desc_cleanup(&desc);
backend_detect_destroy(detect);
if (!inited) {
fatal("zdwm runtime init failed");
}
};
int main(void) {
runtime_t runtime = {0};
bootstrap(&runtime);
runtime_run(&runtime);
runtime_shutdown(&runtime);
return 0;
}