#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #define namespace namespace_ #include "wlr-layer-shell-unstable-v1-client-protocol.h" extern "C" { #include } #undef namespace #include "blur-client-protocol.h" #include "ext-background-effect-v1-client-protocol.h" #include #include "Theme.hpp" #include "common.hpp" struct TypingBuffer : std::pmr::vector { void push_utf8(const char *s) { for (const unsigned char *p = (const unsigned char *)s; *p;) { u32 cp = 0; int len = 0; if (*p < 0x80) { cp = *p++; len = 1; } else if ((*p & 0xE0) == 0xC0) { cp = *p++ & 0x1F; len = 2; } else if ((*p & 0xF0) == 0xE0) { cp = *p++ & 0x0F; len = 3; } else { cp = *p++ & 0x07; len = 4; } for (int i = 1; i < len; i++) cp = (cp << 6) | ((*p++) & 0x3F); push_back(cp); } } }; struct App { App() { init_wayland(); init_egl(); init_signal(); init_theme_portal(); } ~App(); auto run() -> void { SetWindowSize(m_win_w, m_win_h); while (m_running) { pump_events(); render_frame(); std::this_thread::sleep_for(std::chrono::milliseconds(16)); } } auto set_visible(bool visible) -> void; auto visible() const -> bool { return m_visible; } auto stop() -> void { m_running = false; } private: auto init_wayland() -> void; auto init_egl() -> void; auto init_signal() -> void; auto init_theme_portal() -> void; auto pump_events() -> void; auto render_frame() -> void; auto create_layer_surface() -> void; auto destroy_layer_surface() -> void; auto ensure_egl_surface() -> void; auto update_blur_region() -> void; auto theme() const -> ColorScheme const & { return m_themes[m_active_theme]; } static void on_settings_changed(XdpSettings * /*self*/, const char *ns, const char *key, GVariant * /*value*/, gpointer data); struct { wl_display *display{}; wl_registry *registry{}; wl_compositor *compositor{}; wl_seat *seat{}; wl_keyboard *kbd{}; wl_surface *wl_surface{}; zwlr_layer_shell_v1 *layer_shell{}; zwlr_layer_surface_v1 *layer_surface{}; ext_background_effect_manager_v1 *mgr{}; ext_background_effect_surface_v1 *eff{}; org_kde_kwin_blur_manager *kde_blur_mgr{}; org_kde_kwin_blur *kde_blur{}; } m_wayland; struct { EGLDisplay edpy{EGL_NO_DISPLAY}; EGLConfig ecfg{}; EGLContext ectx{EGL_NO_CONTEXT}; EGLSurface esurf{EGL_NO_SURFACE}; wl_egl_window *wegl{}; } m_gl; struct { XdpPortal *portal{}; XdpSettings *settings{}; } m_xdp; struct { TypingBuffer typing{}; xkb_context *xkb_ctx{}; xkb_keymap *xkb_keymap{}; xkb_state *xkb_state{}; std::unordered_set held; std::unordered_set pressed_syms; std::unordered_set released_syms; auto is_down_evdev(u32 evdev) const -> bool { return held.find(evdev) != held.end(); } auto is_down_sym(xkb_keysym_t sym) const -> bool { if (!xkb_state) return false; for (auto k : held) { if (xkb_state_key_get_one_sym(xkb_state, (xkb_keycode_t)(k + 8)) == sym) return true; } return false; } auto is_sym_pressed(xkb_keysym_t sym) const -> bool { return pressed_syms.find(sym) != pressed_syms.end(); } auto is_sym_released(xkb_keysym_t sym) const -> bool { return released_syms.find(sym) != released_syms.end(); } auto mod_active(const char *name) const -> bool { return xkb_state && xkb_state_mod_name_is_active( xkb_state, name, XKB_STATE_MODS_EFFECTIVE) > 0; } auto ctrl() const -> bool { return mod_active("Control"); } auto shift() const -> bool { return mod_active("Shift"); } void clear_transients() { pressed_syms.clear(); released_syms.clear(); } } m_kbd; enum_array m_themes{make_default_themes()}; Theme m_active_theme{Theme::Light}; int m_win_w{800}; int m_win_h{600}; bool m_running{true}; bool m_visible{true}; int m_sfd{-1}; // Signal fd for toggling visibility }; App::~App() { if (m_sfd != -1) close(m_sfd); destroy_layer_surface(); if (m_gl.edpy != EGL_NO_DISPLAY) { if (m_gl.ectx != EGL_NO_CONTEXT) eglDestroyContext(m_gl.edpy, m_gl.ectx); eglTerminate(m_gl.edpy); } if (m_kbd.xkb_state) xkb_state_unref(m_kbd.xkb_state); if (m_kbd.xkb_keymap) xkb_keymap_unref(m_kbd.xkb_keymap); if (m_kbd.xkb_ctx) xkb_context_unref(m_kbd.xkb_ctx); if (m_wayland.kbd) wl_keyboard_destroy(m_wayland.kbd); if (m_wayland.seat) wl_seat_destroy(m_wayland.seat); if (m_wayland.compositor) wl_compositor_destroy(m_wayland.compositor); if (m_wayland.registry) wl_registry_destroy(m_wayland.registry); if (m_wayland.display) wl_display_disconnect(m_wayland.display); if (m_xdp.settings) g_object_unref(m_xdp.settings); if (m_xdp.portal) g_object_unref(m_xdp.portal); } auto App::init_wayland() -> void { m_wayland.display = wl_display_connect(nullptr); if (!m_wayland.display) { std::fprintf(stderr, "failed to connect to Wayland display\n"); std::exit(EXIT_FAILURE); } static wl_keyboard_listener keyboard_listener{}; { auto kb_keymap = [](void *data, wl_keyboard *, u32 format, i32 fd, u32 size) -> void { auto *app = (App *)data; if (format != WL_KEYBOARD_KEYMAP_FORMAT_XKB_V1) { close(fd); return; } void *map = mmap(nullptr, size, PROT_READ, MAP_PRIVATE, fd, 0); if (map == MAP_FAILED) { close(fd); return; } if (app->m_kbd.xkb_keymap) xkb_keymap_unref(app->m_kbd.xkb_keymap); if (app->m_kbd.xkb_state) { xkb_state_unref(app->m_kbd.xkb_state); app->m_kbd.xkb_state = nullptr; } app->m_kbd.xkb_keymap = xkb_keymap_new_from_string( app->m_kbd.xkb_ctx, (const char *)map, XKB_KEYMAP_FORMAT_TEXT_V1, XKB_KEYMAP_COMPILE_NO_FLAGS); app->m_kbd.xkb_state = app->m_kbd.xkb_keymap ? xkb_state_new(app->m_kbd.xkb_keymap) : nullptr; munmap(map, size); close(fd); }; auto kb_enter = [](void *, wl_keyboard *, u32, wl_surface *, wl_array *) -> void {}; auto kb_leave = [](void *data, wl_keyboard *, u32, wl_surface *) -> void { auto *app = (App *)data; app->m_kbd.held.clear(); }; auto kb_key = [](void *data, wl_keyboard *, u32, u32, u32 key, u32 state) -> void { auto *app = (App *)data; if (!app->m_kbd.xkb_state) return; xkb_keycode_t kc = key + 8; if (state == WL_KEYBOARD_KEY_STATE_PRESSED) { app->m_kbd.held.insert(key); xkb_state_update_key(app->m_kbd.xkb_state, kc, XKB_KEY_DOWN); xkb_keysym_t sym = xkb_state_key_get_one_sym(app->m_kbd.xkb_state, kc); app->m_kbd.pressed_syms.insert(sym); bool ctrl = app->m_kbd.mod_active("Control"); bool alt = app->m_kbd.mod_active("Mod1"); bool meta = app->m_kbd.mod_active("Mod4"); if (!(ctrl || alt || meta)) { u32 cp = xkb_keysym_to_utf32(sym); if (cp >= 0x20) { char buf[8]; int n = xkb_keysym_to_utf8(sym, buf, sizeof(buf)); if (n > 0) app->m_kbd.typing.push_utf8(buf); } } } else { xkb_keysym_t sym = xkb_state_key_get_one_sym(app->m_kbd.xkb_state, kc); app->m_kbd.released_syms.insert(sym); app->m_kbd.held.erase(key); xkb_state_update_key(app->m_kbd.xkb_state, kc, XKB_KEY_UP); } }; auto kb_mods = [](void *data, wl_keyboard *, u32, u32 depressed, u32 latched, u32 locked, u32 group) -> void { auto *app = (App *)data; if (!app->m_kbd.xkb_state) return; xkb_state_update_mask(app->m_kbd.xkb_state, depressed, latched, locked, 0, 0, group); }; auto kb_repeat_info = [](void *, wl_keyboard *, i32, i32) -> void {}; keyboard_listener = {kb_keymap, kb_enter, kb_leave, kb_key, kb_mods, kb_repeat_info}; } auto handle_registry_global = [](void *data, wl_registry *registry, u32 name, const char *interface, u32 version) -> void { auto *app = static_cast(data); if (std::strcmp(interface, wl_compositor_interface.name) == 0) { app->m_wayland.compositor = static_cast( wl_registry_bind(registry, name, &wl_compositor_interface, 4)); } else if (strcmp(interface, wl_seat_interface.name) == 0) { app->m_wayland.seat = static_cast( wl_registry_bind(registry, name, &wl_seat_interface, 9)); static struct wl_seat_listener const seat_listener = { .capabilities = [](void *data, struct wl_seat *seat, u32 caps) { auto *app = static_cast(data); if (caps & WL_SEAT_CAPABILITY_KEYBOARD) { app->m_wayland.kbd = wl_seat_get_keyboard(seat); wl_keyboard_add_listener(app->m_wayland.kbd, &keyboard_listener, data); } }, .name = [](void *data, struct wl_seat *wl_seat, const char *name) {}, }; wl_seat_add_listener(app->m_wayland.seat, &seat_listener, data); } else if (strcmp(interface, zwlr_layer_shell_v1_interface.name) == 0) { app->m_wayland.layer_shell = (zwlr_layer_shell_v1 *)wl_registry_bind( registry, name, &zwlr_layer_shell_v1_interface, version >= 4 ? 4 : version); } else if (strcmp(interface, ext_background_effect_manager_v1_interface.name) == 0) { app->m_wayland.mgr = (ext_background_effect_manager_v1 *)wl_registry_bind( registry, name, &ext_background_effect_manager_v1_interface, 1); } else if (strcmp(interface, "org_kde_kwin_blur_manager") == 0) { app->m_wayland.kde_blur_mgr = (org_kde_kwin_blur_manager *)wl_registry_bind( registry, name, &org_kde_kwin_blur_manager_interface, 1); } }; static wl_registry_listener const registry_listener{ .global = handle_registry_global, .global_remove = [](void *, wl_registry *, u32) {}, }; m_wayland.registry = wl_display_get_registry(m_wayland.display); wl_registry_add_listener(m_wayland.registry, ®istry_listener, this); m_kbd.xkb_ctx = xkb_context_new(XKB_CONTEXT_NO_FLAGS); wl_display_roundtrip(m_wayland.display); create_layer_surface(); } auto App::set_visible(bool visible) -> void { if (visible == m_visible) return; if (visible) { create_layer_surface(); ensure_egl_surface(); } else { destroy_layer_surface(); } if (m_wayland.display) wl_display_flush(m_wayland.display); } auto App::init_egl() -> void { m_gl.edpy = eglGetDisplay(reinterpret_cast(m_wayland.display)); eglInitialize(m_gl.edpy, nullptr, nullptr); const EGLint cfgAttribs[]{ EGL_SURFACE_TYPE, EGL_WINDOW_BIT, EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT, EGL_RED_SIZE, 8, EGL_GREEN_SIZE, 8, EGL_BLUE_SIZE, 8, EGL_ALPHA_SIZE, 8, EGL_NONE, }; EGLint n = 0; eglChooseConfig(m_gl.edpy, cfgAttribs, &m_gl.ecfg, 1, &n); const EGLint ctxAttribs[] = {EGL_CONTEXT_MAJOR_VERSION, 2, EGL_NONE}; m_gl.ectx = eglCreateContext(m_gl.edpy, m_gl.ecfg, EGL_NO_CONTEXT, ctxAttribs); ensure_egl_surface(); InitWindow(m_win_w, m_win_h, ""); } auto App::init_signal() -> void { sigset_t mask; sigemptyset(&mask); sigaddset(&mask, SIGUSR1); if (pthread_sigmask(SIG_BLOCK, &mask, nullptr) != 0) { std::perror("pthread_sigmask"); std::exit(EXIT_FAILURE); } m_sfd = signalfd(-1, &mask, SFD_NONBLOCK | SFD_CLOEXEC); if (m_sfd == -1) { std::perror("signalfd"); std::exit(EXIT_FAILURE); } } void App::on_settings_changed(XdpSettings * /*self*/, const char *ns, const char *key, GVariant * /*value*/, gpointer data) { auto *app = static_cast(data); if (g_strcmp0(ns, "org.freedesktop.appearance") == 0 && g_strcmp0(key, "color-scheme") == 0) { guint v = xdp_settings_read_uint(app->m_xdp.settings, "org.freedesktop.appearance", "color-scheme", NULL, NULL); if (v == 1) app->m_active_theme = Theme::Dark; else app->m_active_theme = Theme::Light; } } auto App::init_theme_portal() -> void { m_xdp.portal = xdp_portal_new(); m_xdp.settings = xdp_portal_get_settings(m_xdp.portal); guint v = xdp_settings_read_uint(m_xdp.settings, "org.freedesktop.appearance", "color-scheme", NULL, NULL); if (v == 1) m_active_theme = Theme::Dark; else m_active_theme = Theme::Light; g_signal_connect(m_xdp.settings, "changed", G_CALLBACK(on_settings_changed), this); } auto App::render_frame() -> void { if (!m_visible || m_gl.edpy == EGL_NO_DISPLAY || m_gl.esurf == EGL_NO_SURFACE) return; glViewport(0, 0, m_win_w, m_win_h); for (auto const cp : m_kbd.typing) { std::println("Char typed: {} ({}) shift={} ctrl={}", rune_to_string(cp), cp, m_kbd.shift() ? 'y' : 'n', m_kbd.ctrl() ? 'y' : 'n'); } if (m_kbd.is_sym_pressed(XKB_KEY_Escape)) { set_visible(!visible()); if (m_kbd.ctrl() && m_kbd.shift()) { m_running = false; } } BeginDrawing(); ClearBackground(BLANK); DrawFPS(10, 10); EndDrawing(); eglSwapBuffers(m_gl.edpy, m_gl.esurf); m_kbd.typing.clear(); m_kbd.clear_transients(); } auto App::create_layer_surface() -> void { if (m_wayland.layer_surface) return; if (!m_wayland.compositor || !m_wayland.layer_shell) return; m_wayland.wl_surface = wl_compositor_create_surface(m_wayland.compositor); if (m_wayland.mgr) { m_wayland.eff = ext_background_effect_manager_v1_get_background_effect( m_wayland.mgr, m_wayland.wl_surface); } if (m_wayland.kde_blur_mgr) { m_wayland.kde_blur = org_kde_kwin_blur_manager_create( m_wayland.kde_blur_mgr, m_wayland.wl_surface); } m_wayland.layer_surface = zwlr_layer_shell_v1_get_layer_surface( m_wayland.layer_shell, m_wayland.wl_surface, nullptr, ZWLR_LAYER_SHELL_V1_LAYER_OVERLAY, "waylight-overlay"); if (!m_wayland.layer_surface) { if (m_wayland.eff) { ext_background_effect_surface_v1_destroy(m_wayland.eff); m_wayland.eff = nullptr; } if (m_wayland.kde_blur) { org_kde_kwin_blur_destroy(m_wayland.kde_blur); m_wayland.kde_blur = nullptr; } if (m_wayland.wl_surface) { wl_surface_destroy(m_wayland.wl_surface); m_wayland.wl_surface = nullptr; } return; } zwlr_layer_surface_v1_set_anchor(m_wayland.layer_surface, 0); zwlr_layer_surface_v1_set_size(m_wayland.layer_surface, m_win_w, m_win_h); zwlr_layer_surface_v1_set_exclusive_zone(m_wayland.layer_surface, 0); if (zwlr_layer_shell_v1_get_version(m_wayland.layer_shell) >= 3) { zwlr_layer_surface_v1_set_keyboard_interactivity( m_wayland.layer_surface, ZWLR_LAYER_SURFACE_V1_KEYBOARD_INTERACTIVITY_ON_DEMAND); } auto handle_layer_configure = [](void *data, zwlr_layer_surface_v1 *ls, u32 serial, u32 w, u32 h) -> void { auto *app = static_cast(data); if (w) app->m_win_w = static_cast(w); if (h) app->m_win_h = static_cast(h); zwlr_layer_surface_v1_ack_configure(ls, serial); if (app->m_gl.edpy != EGL_NO_DISPLAY) { if (!app->m_gl.wegl || app->m_gl.esurf == EGL_NO_SURFACE) { app->ensure_egl_surface(); } else { wl_egl_window_resize(app->m_gl.wegl, app->m_win_w, app->m_win_h, 0, 0); eglMakeCurrent(app->m_gl.edpy, app->m_gl.esurf, app->m_gl.esurf, app->m_gl.ectx); } } app->update_blur_region(); if (app->m_wayland.wl_surface) wl_surface_commit(app->m_wayland.wl_surface); }; auto handle_layer_closed = [](void *data, zwlr_layer_surface_v1 *) -> void { static_cast(data)->m_running = false; }; static const zwlr_layer_surface_v1_listener lsl = { .configure = handle_layer_configure, .closed = handle_layer_closed, }; zwlr_layer_surface_v1_add_listener(m_wayland.layer_surface, &lsl, this); update_blur_region(); if (m_wayland.wl_surface) wl_surface_commit(m_wayland.wl_surface); if (m_wayland.display) wl_display_roundtrip(m_wayland.display); ensure_egl_surface(); m_visible = true; } auto App::destroy_layer_surface() -> void { if (m_gl.edpy != EGL_NO_DISPLAY && m_gl.esurf != EGL_NO_SURFACE) { eglMakeCurrent(m_gl.edpy, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT); eglDestroySurface(m_gl.edpy, m_gl.esurf); m_gl.esurf = EGL_NO_SURFACE; } if (m_gl.wegl) { wl_egl_window_destroy(m_gl.wegl); m_gl.wegl = nullptr; } if (m_wayland.eff) { ext_background_effect_surface_v1_destroy(m_wayland.eff); m_wayland.eff = nullptr; } if (m_wayland.kde_blur) { org_kde_kwin_blur_destroy(m_wayland.kde_blur); m_wayland.kde_blur = nullptr; } if (m_wayland.layer_surface) { zwlr_layer_surface_v1_destroy(m_wayland.layer_surface); m_wayland.layer_surface = nullptr; } if (m_wayland.wl_surface) { wl_surface_destroy(m_wayland.wl_surface); m_wayland.wl_surface = nullptr; } if (m_wayland.display) wl_display_flush(m_wayland.display); m_visible = false; } auto App::ensure_egl_surface() -> void { if (m_gl.edpy == EGL_NO_DISPLAY || m_gl.ectx == EGL_NO_CONTEXT) return; if (!m_wayland.wl_surface) return; if (!m_gl.wegl) m_gl.wegl = wl_egl_window_create(m_wayland.wl_surface, m_win_w, m_win_h); if (!m_gl.wegl) return; if (m_gl.esurf == EGL_NO_SURFACE) { m_gl.esurf = eglCreateWindowSurface( m_gl.edpy, m_gl.ecfg, reinterpret_cast(m_gl.wegl), nullptr); } if (m_gl.esurf == EGL_NO_SURFACE) return; eglMakeCurrent(m_gl.edpy, m_gl.esurf, m_gl.esurf, m_gl.ectx); eglSwapInterval(m_gl.edpy, 1); } auto App::update_blur_region() -> void { if (!m_wayland.compositor) return; if (!m_wayland.eff && !m_wayland.kde_blur) return; wl_region *region = wl_compositor_create_region(m_wayland.compositor); if (!region) return; wl_region_add(region, 0, 0, m_win_w - 50, m_win_h); if (m_wayland.eff) ext_background_effect_surface_v1_set_blur_region(m_wayland.eff, region); if (m_wayland.kde_blur) org_kde_kwin_blur_set_region(m_wayland.kde_blur, region); wl_region_destroy(region); } auto App::pump_events() -> void { while (g_main_context_iteration(nullptr, false)) ; wl_display_dispatch_pending(m_wayland.display); wl_display_flush(m_wayland.display); pollfd fds[2]{{wl_display_get_fd(m_wayland.display), POLLIN, 0}, {m_sfd, POLLIN, 0}}; auto prepared = (wl_display_prepare_read(m_wayland.display) == 0); auto ret = poll(fds, 2, 0); // Wayland if (ret > 0 && (fds[0].revents & POLLIN)) { if (prepared) { wl_display_read_events(m_wayland.display); prepared = false; } } else if (prepared) { wl_display_cancel_read(m_wayland.display); } // Signals if (ret > 0 && (fds[1].revents & POLLIN)) { signalfd_siginfo si; while (read(m_sfd, &si, sizeof(si)) == sizeof(si)) { if (si.ssi_signo == SIGUSR1) { set_visible(!visible()); } } } } bool check_or_signal_running() { const char *lock_path = "/tmp/waylight.lock"; int fd = open(lock_path, O_CREAT | O_RDWR, 0666); if (fd == -1) return false; if (flock(fd, LOCK_EX | LOCK_NB) == -1) { FILE *f = fopen(lock_path, "r"); if (f) { pid_t pid; if (fscanf(f, "%d", &pid) == 1) kill(pid, SIGUSR1); fclose(f); } close(fd); return true; } ftruncate(fd, 0); dprintf(fd, "%d\n", getpid()); return false; } std::optional g_app{}; auto main() -> int { if (check_or_signal_running()) { return 0; } std::signal(SIGINT, [](int) { if (g_app) g_app->stop(); }); g_app.emplace(); g_app->run(); }