@@ -24,6 +24,7 @@ pkg_check_modules(GLES2 REQUIRED IMPORTED_TARGET glesv2)
|
||||
pkg_check_modules(WLROOTS REQUIRED IMPORTED_TARGET wlroots-0.20)
|
||||
pkg_check_modules(XKBCOMMON REQUIRED IMPORTED_TARGET xkbcommon)
|
||||
pkg_check_modules(OPENXR REQUIRED IMPORTED_TARGET openxr)
|
||||
pkg_check_modules(LUA REQUIRED IMPORTED_TARGET lua)
|
||||
|
||||
find_program(WAYLAND_SCANNER_EXECUTABLE wayland-scanner REQUIRED)
|
||||
message(STATUS "Found wayland-scanner at ${WAYLAND_SCANNER_EXECUTABLE}")
|
||||
@@ -50,6 +51,7 @@ add_executable(${PROJECT_NAME})
|
||||
target_sources(${PROJECT_NAME} PUBLIC
|
||||
src/vec.c
|
||||
|
||||
src/Config.c
|
||||
src/LunarWM.c
|
||||
src/main.c
|
||||
)
|
||||
@@ -60,6 +62,7 @@ target_link_libraries(${PROJECT_NAME} PUBLIC
|
||||
PkgConfig::GLES2
|
||||
PkgConfig::WLROOTS
|
||||
PkgConfig::OPENXR
|
||||
PkgConfig::LUA
|
||||
|
||||
raylib
|
||||
)
|
||||
|
||||
@@ -34,6 +34,8 @@
|
||||
(pkgs.llvmPackages_20.clang-tools.override { enableLibcxx = true; })
|
||||
lldb
|
||||
|
||||
lua
|
||||
|
||||
# For wlroots
|
||||
libxkbcommon
|
||||
libxkbcommon.dev
|
||||
|
||||
15
lunarwm/init.lua
Normal file
15
lunarwm/init.lua
Normal file
@@ -0,0 +1,15 @@
|
||||
function main(kbd)
|
||||
return "Super-" .. kbd
|
||||
end
|
||||
|
||||
return {
|
||||
input = {
|
||||
keyboard = {
|
||||
xkb_options = { "altwin:swap_lalt_lwin" },
|
||||
},
|
||||
},
|
||||
keybindings = {
|
||||
{ bind = main("Escape"), action = lunar.quit_compositor },
|
||||
{ bind = main("Shift-R"), action = lunar.reload_config },
|
||||
},
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
fn lib = {
|
||||
input = {
|
||||
keyboard = {
|
||||
xkb_options = [ "altwin:swap_lalt_lwin" ]
|
||||
}
|
||||
}
|
||||
|
||||
modifier_aliases = {
|
||||
Main = "Super"
|
||||
}
|
||||
|
||||
keybindings = [
|
||||
{ bind = "Main-Escape" action = (lib.quit_compositor) }
|
||||
{ bind = "Main-Shift-R" action = (lib.reload_config) }
|
||||
]
|
||||
}
|
||||
|
||||
334
src/Config.c
Normal file
334
src/Config.c
Normal file
@@ -0,0 +1,334 @@
|
||||
#include "Config.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/stat.h>
|
||||
|
||||
#include <lauxlib.h>
|
||||
#include <lualib.h>
|
||||
|
||||
#include <wlr/types/wlr_keyboard.h>
|
||||
|
||||
char const *get_config_path(void)
|
||||
{
|
||||
char const *paths[] = {
|
||||
"lunarwm/init.lua",
|
||||
};
|
||||
for (size_t i = 0; i < sizeof(paths) / sizeof(paths[0]); i++) {
|
||||
char const *p = paths[i];
|
||||
struct stat s;
|
||||
if (stat(p, &s) == 0)
|
||||
return p;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static int dupstr(char const *s, char **out)
|
||||
{
|
||||
if (!s) {
|
||||
*out = NULL;
|
||||
return 0;
|
||||
}
|
||||
size_t n = strlen(s) + 1;
|
||||
char *m = (char *)malloc(n);
|
||||
if (!m)
|
||||
return -1;
|
||||
memcpy(m, s, n);
|
||||
*out = m;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static uint32_t mod_from_token(char const *t)
|
||||
{
|
||||
if (!t)
|
||||
return 0;
|
||||
if (strcasecmp(t, "Super") == 0)
|
||||
return WLR_MODIFIER_LOGO;
|
||||
if (strcasecmp(t, "Shift") == 0)
|
||||
return WLR_MODIFIER_SHIFT;
|
||||
if (strcasecmp(t, "Ctrl") == 0 || strcasecmp(t, "Control") == 0)
|
||||
return WLR_MODIFIER_CTRL;
|
||||
if (strcasecmp(t, "Alt") == 0)
|
||||
return WLR_MODIFIER_ALT;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int parse_bind(
|
||||
char const *bind, uint32_t *mods_out, xkb_keysym_t *sym_out)
|
||||
{
|
||||
if (!bind || !mods_out || !sym_out)
|
||||
return -1;
|
||||
|
||||
char buf[256];
|
||||
strncpy(buf, bind, sizeof buf - 1);
|
||||
buf[sizeof buf - 1] = 0;
|
||||
|
||||
uint32_t mods = 0;
|
||||
char *save = NULL;
|
||||
char *tok = strtok_r(buf, "-", &save);
|
||||
char *last = tok;
|
||||
while (tok) {
|
||||
last = tok;
|
||||
tok = strtok_r(NULL, "-", &save);
|
||||
}
|
||||
/* walk again to accumulate modifiers (all but last) */
|
||||
strncpy(buf, bind, sizeof buf - 1);
|
||||
buf[sizeof buf - 1] = 0;
|
||||
save = NULL;
|
||||
for (char *t = strtok_r(buf, "-", &save); t;
|
||||
t = strtok_r(NULL, "-", &save)) {
|
||||
if (t == last)
|
||||
break;
|
||||
mods |= mod_from_token(t);
|
||||
}
|
||||
|
||||
/* keysym from last token */
|
||||
int flags = XKB_KEYSYM_CASE_INSENSITIVE;
|
||||
xkb_keysym_t sym = xkb_keysym_from_name(last, flags);
|
||||
if (sym == XKB_KEY_NoSymbol)
|
||||
return -1;
|
||||
|
||||
*mods_out = mods;
|
||||
*sym_out = sym;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int push_config_table_from_idx(lua_State *L, int idx_abs)
|
||||
{
|
||||
if (!lua_istable(L, idx_abs)) {
|
||||
return luaL_error(L, "config: expected table at index %d", idx_abs);
|
||||
}
|
||||
lua_pushvalue(L, idx_abs);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int config_load_ref(lua_State *L, int idx, Config *out)
|
||||
{
|
||||
if (!L || !out)
|
||||
return -1;
|
||||
|
||||
memset(out, 0, sizeof(*out));
|
||||
|
||||
int idx_abs = lua_absindex(L, idx);
|
||||
if (push_config_table_from_idx(L, idx_abs) != 0)
|
||||
return -1;
|
||||
|
||||
lua_getfield(L, -1, "keybindings");
|
||||
if (!lua_istable(L, -1)) {
|
||||
lua_pop(L, 2);
|
||||
return luaL_error(L, "config: 'keybindings' must be a table (array)");
|
||||
}
|
||||
|
||||
size_t n = (size_t)lua_rawlen(L, -1);
|
||||
if (n == 0) {
|
||||
lua_pop(L, 2);
|
||||
return 0;
|
||||
}
|
||||
|
||||
BindingRef *arr = calloc(n, sizeof(BindingRef));
|
||||
if (!arr) {
|
||||
lua_pop(L, 2);
|
||||
return luaL_error(L, "config: OOM allocating bindings");
|
||||
}
|
||||
|
||||
size_t ok_count = 0;
|
||||
for (size_t i = 0; i < n; i++) {
|
||||
lua_rawgeti(L, -1, (lua_Integer)(i + 1));
|
||||
if (!lua_istable(L, -1)) {
|
||||
lua_pop(L, 1);
|
||||
continue;
|
||||
}
|
||||
|
||||
lua_getfield(L, -1, "bind");
|
||||
char const *bind = lua_tostring(L, -1);
|
||||
if (!bind) {
|
||||
lua_pop(L, 2);
|
||||
continue;
|
||||
}
|
||||
|
||||
uint32_t mods = 0;
|
||||
xkb_keysym_t sym = XKB_KEY_NoSymbol;
|
||||
if (parse_bind(bind, &mods, &sym) != 0) {
|
||||
lua_pop(L, 2);
|
||||
continue;
|
||||
}
|
||||
|
||||
lua_getfield(L, -2, "action");
|
||||
if (!lua_isfunction(L, -1)) {
|
||||
lua_pop(L, 3);
|
||||
continue;
|
||||
}
|
||||
|
||||
int ref = luaL_ref(L, LUA_REGISTRYINDEX);
|
||||
lua_pop(L, 1); // pop bind string
|
||||
|
||||
arr[ok_count].mods_mask = mods;
|
||||
arr[ok_count].sym = sym;
|
||||
arr[ok_count].action_ref = ref;
|
||||
ok_count++;
|
||||
|
||||
lua_pop(L, 1); // pop table entry
|
||||
}
|
||||
|
||||
lua_pop(L, 2); // pop keybindings table + config table
|
||||
|
||||
if (ok_count == 0) {
|
||||
free(arr);
|
||||
out->bindings = NULL;
|
||||
out->count = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (ok_count < n) {
|
||||
BindingRef *shr = realloc(arr, ok_count * sizeof(BindingRef));
|
||||
if (shr)
|
||||
arr = shr;
|
||||
}
|
||||
|
||||
out->bindings = arr;
|
||||
out->count = ok_count;
|
||||
return 0;
|
||||
}
|
||||
|
||||
void config_unref(lua_State *L, Config *cfg)
|
||||
{
|
||||
if (!cfg || !cfg->bindings) {
|
||||
if (cfg)
|
||||
cfg->count = 0;
|
||||
return;
|
||||
}
|
||||
for (size_t i = 0; i < cfg->count; i++) {
|
||||
if (cfg->bindings[i].action_ref != LUA_NOREF
|
||||
&& cfg->bindings[i].action_ref != LUA_REFNIL) {
|
||||
luaL_unref(L, LUA_REGISTRYINDEX, cfg->bindings[i].action_ref);
|
||||
}
|
||||
}
|
||||
free(cfg->bindings);
|
||||
cfg->bindings = NULL;
|
||||
cfg->count = 0;
|
||||
}
|
||||
|
||||
static int load_config_file(lua_State *L, char const *path)
|
||||
{
|
||||
if (!path || !path[0])
|
||||
return -1;
|
||||
|
||||
if (luaL_loadfile(L, path) != LUA_OK) {
|
||||
char const *err = lua_tostring(L, -1);
|
||||
fprintf(
|
||||
stderr, "config: loadfile failed: %s\n", err ? err : "(unknown)");
|
||||
lua_pop(L, 1);
|
||||
return -1;
|
||||
}
|
||||
if (lua_pcall(L, 0, 1, 0) != LUA_OK) {
|
||||
char const *err = lua_tostring(L, -1);
|
||||
fprintf(stderr, "config: executing '%s' failed: %s\n", path,
|
||||
err ? err : "(unknown)");
|
||||
lua_pop(L, 1);
|
||||
return -1;
|
||||
}
|
||||
if (!lua_istable(L, -1)) {
|
||||
lua_pop(L, 1);
|
||||
fprintf(stderr, "config: '%s' did not return a table\n", path);
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
ConfigManager *config_manager_create(char const *path)
|
||||
{
|
||||
ConfigManager *cm = (ConfigManager *)calloc(1, sizeof(*cm));
|
||||
if (!cm)
|
||||
return NULL;
|
||||
|
||||
cm->L = luaL_newstate();
|
||||
if (!cm->L) {
|
||||
free(cm);
|
||||
return NULL;
|
||||
}
|
||||
luaL_openlibs(cm->L);
|
||||
|
||||
if (!path)
|
||||
path = get_config_path();
|
||||
if (path) {
|
||||
if (dupstr(path, &cm->path) != 0) {
|
||||
lua_close(cm->L);
|
||||
free(cm);
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
|
||||
if (cm->path && load_config_file(cm->L, cm->path) == 0) {
|
||||
if (config_load_ref(cm->L, -1, &cm->cfg) != 0) {
|
||||
lua_pop(cm->L, 1);
|
||||
} else {
|
||||
lua_pop(cm->L, 1);
|
||||
}
|
||||
}
|
||||
return cm;
|
||||
}
|
||||
|
||||
void config_manager_destroy(ConfigManager *cm)
|
||||
{
|
||||
if (!cm)
|
||||
return;
|
||||
config_unref(cm->L, &cm->cfg);
|
||||
if (cm->L)
|
||||
lua_close(cm->L);
|
||||
free(cm->path);
|
||||
free(cm);
|
||||
}
|
||||
|
||||
int config_manager_reload(ConfigManager *cm)
|
||||
{
|
||||
if (!cm || !cm->path)
|
||||
return -1;
|
||||
|
||||
config_unref(cm->L, &cm->cfg);
|
||||
|
||||
if (load_config_file(cm->L, cm->path) != 0) {
|
||||
return -1;
|
||||
}
|
||||
int rc = config_load_ref(cm->L, -1, &cm->cfg);
|
||||
lua_pop(cm->L, 1);
|
||||
return rc;
|
||||
}
|
||||
|
||||
lua_State *config_manager_lua(ConfigManager *cm) { return cm ? cm->L : NULL; }
|
||||
|
||||
Config const *config_manager_get(ConfigManager *cm)
|
||||
{
|
||||
return cm ? &cm->cfg : NULL;
|
||||
}
|
||||
|
||||
char const *config_manager_path(ConfigManager *cm)
|
||||
{
|
||||
return cm ? cm->path : NULL;
|
||||
}
|
||||
|
||||
int trigger_ref_modsym(
|
||||
lua_State *L, Config const *cfg, uint32_t mods, xkb_keysym_t sym)
|
||||
{
|
||||
if (!L || !cfg)
|
||||
return -1;
|
||||
for (size_t i = 0; i < cfg->count; i++) {
|
||||
BindingRef const *br = &cfg->bindings[i];
|
||||
if (br->sym != sym)
|
||||
continue;
|
||||
if ((mods & br->mods_mask) != br->mods_mask)
|
||||
continue; // require all mods
|
||||
lua_rawgeti(L, LUA_REGISTRYINDEX, br->action_ref);
|
||||
if (!lua_isfunction(L, -1)) {
|
||||
lua_pop(L, 1);
|
||||
return -2;
|
||||
}
|
||||
if (lua_pcall(L, 0, 0, 0) != LUA_OK) {
|
||||
fprintf(stderr, "config: action error: %s\n", lua_tostring(L, -1));
|
||||
lua_pop(L, 1);
|
||||
return -3;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
43
src/Config.h
Normal file
43
src/Config.h
Normal file
@@ -0,0 +1,43 @@
|
||||
#ifndef CONFIG_H
|
||||
#define CONFIG_H
|
||||
|
||||
#include <lua.h>
|
||||
#include <stddef.h>
|
||||
|
||||
#include <xkbcommon/xkbcommon.h>
|
||||
|
||||
typedef struct {
|
||||
xkb_keysym_t sym;
|
||||
uint32_t mods_mask;
|
||||
int action_ref; // luaL_ref(L, LUA_REGISTRYINDEX)
|
||||
} BindingRef;
|
||||
|
||||
typedef struct {
|
||||
BindingRef *bindings;
|
||||
size_t count;
|
||||
} Config;
|
||||
|
||||
char const *get_config_path(void);
|
||||
|
||||
int config_load_ref(lua_State *L, int idx, Config *out);
|
||||
void config_unref(lua_State *L, Config *cfg);
|
||||
int trigger_ref_modsym(
|
||||
lua_State *L, Config const *cfg, uint32_t mods, xkb_keysym_t sym);
|
||||
|
||||
struct ConfigManager {
|
||||
lua_State *L;
|
||||
Config cfg;
|
||||
char *path;
|
||||
};
|
||||
typedef struct ConfigManager ConfigManager;
|
||||
|
||||
ConfigManager *config_manager_create(char const *path);
|
||||
void config_manager_destroy(ConfigManager *cm);
|
||||
|
||||
int config_manager_reload(ConfigManager *cm);
|
||||
|
||||
lua_State *config_manager_lua(ConfigManager *cm);
|
||||
Config const *config_manager_get(ConfigManager *cm);
|
||||
char const *config_manager_path(ConfigManager *cm);
|
||||
|
||||
#endif // CONFIG_H
|
||||
107
src/LunarWM.c
107
src/LunarWM.c
@@ -1,6 +1,7 @@
|
||||
#include "LunarWM.h"
|
||||
|
||||
#include <assert.h>
|
||||
#include <ctype.h>
|
||||
#include <math.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
@@ -127,6 +128,66 @@ bool LunarWM_Toplevel_update(LunarWM_Toplevel *this)
|
||||
return true;
|
||||
}
|
||||
|
||||
static void keysym_name(xkb_keysym_t sym, char *buf, size_t bufsz)
|
||||
{
|
||||
if (bufsz == 0)
|
||||
return;
|
||||
buf[0] = 0;
|
||||
|
||||
char tmp[128] = { 0 };
|
||||
int n = xkb_keysym_get_name(sym, tmp, sizeof tmp);
|
||||
if (n <= 0) {
|
||||
snprintf(buf, bufsz, "Unknown");
|
||||
return;
|
||||
}
|
||||
|
||||
if (strlen(tmp) == 1) {
|
||||
buf[0] = (char)toupper((unsigned char)tmp[0]);
|
||||
buf[1] = 0;
|
||||
} else {
|
||||
snprintf(buf, bufsz, "%s", tmp);
|
||||
}
|
||||
}
|
||||
|
||||
static size_t fill_mod_list(uint32_t mods, char const *outv[4])
|
||||
{
|
||||
size_t k = 0;
|
||||
if (mods & WLR_MODIFIER_LOGO)
|
||||
outv[k++] = "Super";
|
||||
if (mods & WLR_MODIFIER_SHIFT)
|
||||
outv[k++] = "Shift";
|
||||
if (mods & WLR_MODIFIER_CTRL)
|
||||
outv[k++] = "Ctrl";
|
||||
if (mods & WLR_MODIFIER_ALT)
|
||||
outv[k++] = "Alt";
|
||||
return k;
|
||||
}
|
||||
|
||||
static char *make_hotkey_string(uint32_t mods, xkb_keysym_t sym)
|
||||
{
|
||||
char keyname[128];
|
||||
keysym_name(sym, keyname, sizeof keyname);
|
||||
|
||||
char const *mods_list[4] = { 0 };
|
||||
size_t mcount = fill_mod_list(mods, mods_list);
|
||||
|
||||
size_t need = strlen(keyname) + 1;
|
||||
for (size_t i = 0; i < mcount; ++i)
|
||||
need += strlen(mods_list[i]) + 1;
|
||||
|
||||
char *s = (char *)malloc(need);
|
||||
if (!s)
|
||||
return NULL;
|
||||
|
||||
s[0] = 0;
|
||||
for (size_t i = 0; i < mcount; ++i) {
|
||||
strcat(s, mods_list[i]);
|
||||
strcat(s, "-");
|
||||
}
|
||||
strcat(s, keyname);
|
||||
return s;
|
||||
}
|
||||
|
||||
static void Keyboard_modifiers_notify(struct wl_listener *listener, void *)
|
||||
{
|
||||
auto *kbd
|
||||
@@ -150,7 +211,7 @@ static void Keyboard_key_notify(struct wl_listener *listener, void *data)
|
||||
xkb_keysym_t const keysym
|
||||
= xkb_state_key_get_one_sym(kbd->wlr_keyboard->xkb_state, keycode);
|
||||
|
||||
bool const handled = false;
|
||||
bool handled = false;
|
||||
uint32_t const modifiers = wlr_keyboard_get_modifiers(kbd->wlr_keyboard);
|
||||
if (server->wayland.session && event->state == WL_KEYBOARD_KEY_STATE_PRESSED
|
||||
&& keysym >= XKB_KEY_XF86Switch_VT_1
|
||||
@@ -160,8 +221,11 @@ static void Keyboard_key_notify(struct wl_listener *listener, void *data)
|
||||
return;
|
||||
}
|
||||
if (event->state == WL_KEYBOARD_KEY_STATE_PRESSED) {
|
||||
if ((modifiers & WLR_MODIFIER_LOGO) && syms[XKB_KEY_Q]) {
|
||||
LunarWM_terminate(server);
|
||||
for (int i = 0; i < nsyms; i++) {
|
||||
if (trigger_ref_modsym(
|
||||
server->cman->L, &server->cman->cfg, modifiers, syms[i])
|
||||
== 0)
|
||||
return; // handled
|
||||
}
|
||||
}
|
||||
|
||||
@@ -534,7 +598,7 @@ static bool init_xr(LunarWM *this)
|
||||
wlr_log(WLR_ERROR, "Failed to get GLES graphics requirements");
|
||||
return false;
|
||||
}
|
||||
printf("OpenGL ES range: %d.%d.%d - %d.%d.%d\n",
|
||||
wlr_log(WLR_INFO, "OpenGL ES range: %d.%d.%d - %d.%d.%d\n",
|
||||
XR_VERSION_MAJOR(reqs.minApiVersionSupported),
|
||||
XR_VERSION_MINOR(reqs.minApiVersionSupported),
|
||||
XR_VERSION_PATCH(reqs.minApiVersionSupported),
|
||||
@@ -982,6 +1046,25 @@ static bool init_xr(LunarWM *this)
|
||||
return true;
|
||||
}
|
||||
|
||||
static int l_reload_config(lua_State *L)
|
||||
{
|
||||
ConfigManager *cm = g_wm.cman;
|
||||
if (!cm) {
|
||||
lua_pushnil(L);
|
||||
return 1;
|
||||
}
|
||||
|
||||
config_manager_reload(cm);
|
||||
lua_pushnil(L);
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int l_quit_compositor(lua_State *L)
|
||||
{
|
||||
LunarWM_terminate(&g_wm);
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool LunarWM_init(LunarWM *this)
|
||||
{
|
||||
{ // Init defaults
|
||||
@@ -999,6 +1082,22 @@ bool LunarWM_init(LunarWM *this)
|
||||
this->renderer.camera.projection = CAMERA_PERSPECTIVE;
|
||||
}
|
||||
|
||||
this->cman = config_manager_create(get_config_path());
|
||||
assert(this->cman);
|
||||
|
||||
{
|
||||
lua_State *L = this->cman->L;
|
||||
|
||||
lua_newtable(L);
|
||||
lua_pushcfunction(L, l_quit_compositor);
|
||||
lua_setfield(L, -2, "quit_compositor");
|
||||
lua_pushcfunction(L, l_reload_config);
|
||||
lua_setfield(L, -2, "reload_config");
|
||||
lua_setglobal(L, "lunar");
|
||||
|
||||
config_manager_reload(this->cman);
|
||||
}
|
||||
|
||||
if (getenv("DISPLAY") != nullptr || getenv("WAYLAND_DISPLAY") != nullptr) {
|
||||
wlr_log(WLR_ERROR, "This compositor can only be ran in DRM mode.");
|
||||
return false;
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
#include <raymath.h>
|
||||
#include <rlgl.h>
|
||||
|
||||
#include "Config.h"
|
||||
#include "common.h"
|
||||
|
||||
struct LunarWM;
|
||||
@@ -158,6 +159,9 @@ typedef struct LunarWM {
|
||||
Camera3D camera;
|
||||
} renderer;
|
||||
|
||||
Config config;
|
||||
ConfigManager *cman;
|
||||
|
||||
bool initialized;
|
||||
bool running;
|
||||
} LunarWM;
|
||||
|
||||
Reference in New Issue
Block a user