diff --git a/CMakeLists.txt b/CMakeLists.txt index d4a266d..1a8ff93 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -43,6 +43,7 @@ set(IMGUI_SOURCES set(WAYLAND_PROTOCOLS src/wayland/protocols/wlr-layer-shell-unstable-v1-client-protocol.c src/wayland/protocols/xdg-shell-client-protocol.c + src/wayland/protocols/hyprland-toplevel-export-v1-client-protocol.c ) # INIH @@ -100,6 +101,7 @@ add_executable(hyprwat src/hyprland/ipc.cpp src/wayland/wayland.cpp src/wayland/display.cpp + src/wayland/shm.cpp src/wayland/layer_surface.cpp src/wayland/input.cpp src/renderer/egl_context.cpp @@ -109,11 +111,13 @@ add_executable(hyprwat src/frames/text.cpp src/frames/custom.cpp src/frames/images.cpp + src/frames/overview.cpp src/flows/simple_flows.cpp src/flows/wifi_flow.cpp src/flows/audio_flow.cpp src/flows/custom_flow.cpp src/flows/wallpaper_flow.cpp + src/flows/overview_flow.cpp src/net/network_manager.cpp src/audio/audio.cpp src/wallpaper/thumbnail.cpp @@ -170,7 +174,7 @@ install(FILES man/hyprwat.6 include(InstallRequiredSystemLibraries) set(CPACK_PACKAGE_NAME "hyprwat") -set(CPACK_PACKAGE_VERSION "0.9.2") +set(CPACK_PACKAGE_VERSION "0.10.0") set(CPACK_PACKAGE_CONTACT "zack@bartel.com") set(CPACK_GENERATOR "DEB;RPM;TGZ") diff --git a/Makefile b/Makefile index c6b6835..30a47e7 100644 --- a/Makefile +++ b/Makefile @@ -46,6 +46,9 @@ run-custom: debug run-wallpaper: debug ./build/debug/hyprwat --wallpaper ~/.local/share/wallpapers +run-overview: debug + ./build/debug/hyprwat --overview + reset-wifi: sudo nmcli device disconnect wlan0 sudo nmcli device connect wlan0 diff --git a/README.md b/README.md index 1005845..8be5820 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,7 @@ hyprwat creates a popup menu at your cursor position where you can select from a - **WiFi selector**: Built-in support for selecting WiFi networks (via dbus and NetworkManager - **Audio selector**: Built-in support for selecting audio input/output devices (via Pipewire) - **Wallpaper selection**: Easily select a wallpaper from a directory of images (hyprpaper only) +- **Workspace overview**: Preview all open workspaces and their windows with a dynamic expose-like overview - **Custom menus**: Define your own menus using simple YAML configuration files - **Theming**: Customize the appearance with a configuration file @@ -67,6 +68,7 @@ If no arguments are provided, hyprwat will read from stdin, expecting one item p - `--password `: Show a password input prompt (masked input) with optional hint text - `--audio`: Show audio input/output device selector (requires pipewire) - `--wifi`: Show WiFi network selection +- `--overview`: Show a visual workspace overview and selector - `--custom `: Load a custom menu from a YAML configuration file - `--wallpaper `: Select a wallpaper from the specified directory (for hyprpaper) @@ -98,6 +100,9 @@ hyprwat --custom ~/.config/hyprwat/menus/powermenu.yaml # Wallpaper selection from a directory hyprwat --wallpaper ~/.local/share/wallpapers +# Visual workspace overview and navigation +hyprwat --overview + ``` See the [examples](examples) directory for more. diff --git a/man/hyprwat.6 b/man/hyprwat.6 index a7c61c9..a0937f8 100644 --- a/man/hyprwat.6 +++ b/man/hyprwat.6 @@ -1,4 +1,4 @@ -.TH HYPRWAT 6 "November 2025" "hyprwat 0.9.2" "User Commands" +.TH HYPRWAT 6 "March 2026" "hyprwat 0.10.0" "User Commands" .SH NAME hyprwat \- A Hyprland menu utility to present selectable options with a customizable interface .SH SYNOPSIS @@ -18,6 +18,9 @@ hyprwat \- A Hyprland menu utility to present selectable options with a customiz --audio .br .B hyprwat +--overview +.br +.B hyprwat --custom \fIconfig.yaml\fR .br .B hyprwat @@ -91,6 +94,12 @@ Select an image file from the specified directory to set as the desktop wallpape $ hyprwat --wallpaper ~/.local/share/wallpapers .EE .TP +.B OVERVIEW MODE +Show a visual overview of all workspaces and their windows, allowing you to easily browse and switch to a selected workspace. +.EX +$ hyprwat --overview +.EE +.TP .B CUSTOM MODE Render a fully custom menu from a YAML configuration file. Custom menus support multiple widget types (buttons, inputs, sliders, checkboxes, color @@ -116,6 +125,9 @@ Show Wi-Fi network selection mode. .BR --audio Show audio input/output device selection mode. .TP +.BR --overview +Show a visual workspace overview and selector. +.TP .BR --wallpaper Select an image file from the specified directory to set as the desktop wallpaper. .TP @@ -267,6 +279,11 @@ $ hyprwat --wifi $ hyprwat --audio .EE .TP +Show the workspace overview: +.EX +$ hyprwat --overview +.EE +.TP Launch a custom control panel: .EX $ hyprwat --custom ~/.config/hyprwat/control-panel.yaml diff --git a/src/flows/overview_flow.cpp b/src/flows/overview_flow.cpp new file mode 100644 index 0000000..0b2406e --- /dev/null +++ b/src/flows/overview_flow.cpp @@ -0,0 +1,31 @@ +#include "overview_flow.hpp" +#include "../frames/overview.hpp" +#include "../wayland/display.hpp" + +OverviewFlow::OverviewFlow(hyprland::Control& hyprctl, wl::Display& wlDisplay, int logicalWidth, int logicalHeight) + : hyprctl(hyprctl), logicalWidth(logicalWidth), logicalHeight(logicalHeight) { + mainFrame = std::make_unique(hyprctl, wlDisplay, logicalWidth, logicalHeight); +} + +OverviewFlow::~OverviewFlow() = default; + +Frame* OverviewFlow::getCurrentFrame() { return mainFrame.get(); } + +bool OverviewFlow::handleResult(const FrameResult& result) { + if (result.action == FrameResult::Action::SUBMIT) { + finalResult = result.value; + done = true; + + // dispatch to selected workspace + int workspaceId = std::stoi(finalResult); + hyprctl.dispatchWorkspace(workspaceId); + } else if (result.action == FrameResult::Action::CANCEL) { + done = true; + } + + return !done; +} + +bool OverviewFlow::isDone() const { return done; } + +std::string OverviewFlow::getResult() const { return finalResult; } diff --git a/src/flows/overview_flow.hpp b/src/flows/overview_flow.hpp new file mode 100644 index 0000000..73095aa --- /dev/null +++ b/src/flows/overview_flow.hpp @@ -0,0 +1,28 @@ +#pragma once + +#include "../hyprland/ipc.hpp" +#include "../ui.hpp" +#include "flow.hpp" + +namespace wl { + class Display; +} + +class OverviewFlow : public Flow { +public: + OverviewFlow(hyprland::Control& hyprctl, wl::Display& wlDisplay, int logicalWidth, int logicalHeight); + ~OverviewFlow() override; + + Frame* getCurrentFrame() override; + bool handleResult(const FrameResult& result) override; + bool isDone() const override; + std::string getResult() const override; + +private: + hyprland::Control& hyprctl; + int logicalWidth; + int logicalHeight; + std::unique_ptr mainFrame; + bool done = false; + std::string finalResult; +}; diff --git a/src/frames/overview.cpp b/src/frames/overview.cpp new file mode 100644 index 0000000..aaa3902 --- /dev/null +++ b/src/frames/overview.cpp @@ -0,0 +1,326 @@ +#define GL_GLEXT_PROTOTYPES 1 +#include "overview.hpp" +#include +#include +#include +#include + +extern "C" { +// mock for undefined reference in hyprland-toplevel-export protocol +extern const struct wl_interface zwlr_foreign_toplevel_handle_v1_interface = { + "zwlr_foreign_toplevel_handle_v1", 3, 0, nullptr, 0, nullptr}; +} + +#define GL_GLEXT_PROTOTYPES 1 + +const struct hyprland_toplevel_export_frame_v1_listener OverviewFrame::export_frame_listener = { + .buffer = OverviewFrame::handle_buffer, + .damage = OverviewFrame::handle_damage, + .flags = OverviewFrame::handle_flags, + .ready = OverviewFrame::handle_ready, + .failed = OverviewFrame::handle_failed, + .linux_dmabuf = OverviewFrame::handle_linux_dmabuf, + .buffer_done = OverviewFrame::handle_buffer_done, +}; + +OverviewFrame::OverviewFrame(hyprland::Control& hyprctl, wl::Display& wlDisplay, int logicalWidth, int logicalHeight) + : hyprctl(hyprctl), wlDisplay(wlDisplay), logicalWidth(logicalWidth), logicalHeight(logicalHeight) { + captureClients(); +} + +OverviewFrame::~OverviewFrame() { + for (auto& w : workspaces) { + for (auto& c : w.clients) { + if (c->frame) + hyprland_toplevel_export_frame_v1_destroy(c->frame); + if (c->texture) { + glDeleteTextures(1, &c->texture); + } + } + } +} + +void OverviewFrame::captureClients() { + auto allWorkspaces = hyprctl.getWorkspaces(); + auto allClients = hyprctl.getClients(); + + // sort workspaces by id + std::sort(allWorkspaces.begin(), allWorkspaces.end(), [](const auto& a, const auto& b) { return a.id < b.id; }); + + for (const auto& w : allWorkspaces) { + WorkspaceView wv; + wv.workspace = w; + for (const auto& c : allClients) { + if (c.workspaceId == w.id && c.mapped && !c.hidden) { + auto capture = std::make_shared(); + capture->owner = this; + capture->client = c; + wv.clients.push_back(capture); + // Also add to global capture list + pendingCaptures.push_back(capture); + } + } + workspaces.push_back(wv); + } + + if (!wlDisplay.exportManager()) { + debug::log(ERR, "Toplevel export manager not available"); + return; + } + + for (auto& c : pendingCaptures) { + try { + unsigned long long addr; + std::stringstream ss; + ss << std::hex << c->client.address; + ss >> addr; + + c->frame = + hyprland_toplevel_export_manager_v1_capture_toplevel(wlDisplay.exportManager(), 0, (uint32_t)addr); + + if (c->frame) { + hyprland_toplevel_export_frame_v1_add_listener(c->frame, &export_frame_listener, c.get()); + } + } catch (...) { + debug::log(ERR, + "Failed to capture client {} on workspace {}", + c->client.title.empty() ? std::to_string(c->client.workspaceId) : c->client.title, + c->client.workspaceId); + c->failed = true; + } + } +} + +void OverviewFrame::handle_buffer(void* data, + struct hyprland_toplevel_export_frame_v1* export_frame, + uint32_t format, + uint32_t width, + uint32_t height, + uint32_t stride) { + auto* c = static_cast(data); + c->captureFormat = format; + c->captureWidth = width; + c->captureHeight = height; + c->captureStride = stride; +} + +void OverviewFrame::handle_damage(void* data, + struct hyprland_toplevel_export_frame_v1* export_frame, + uint32_t x, + uint32_t y, + uint32_t width, + uint32_t height) {} +void OverviewFrame::handle_flags(void* data, struct hyprland_toplevel_export_frame_v1* export_frame, uint32_t flags) {} + +void OverviewFrame::handle_ready(void* data, + struct hyprland_toplevel_export_frame_v1* export_frame, + uint32_t tv_sec_hi, + uint32_t tv_sec_lo, + uint32_t tv_nsec) { + auto* c = static_cast(data); + c->ready = true; +} + +void OverviewFrame::handle_failed(void* data, struct hyprland_toplevel_export_frame_v1* export_frame) { + auto* c = static_cast(data); + c->failed = true; +} + +void OverviewFrame::handle_linux_dmabuf(void* data, + struct hyprland_toplevel_export_frame_v1* export_frame, + uint32_t format, + uint32_t width, + uint32_t height) {} + +void OverviewFrame::handle_buffer_done(void* data, struct hyprland_toplevel_export_frame_v1* export_frame) { + auto* c = static_cast(data); + if (!c->owner) + return; + + try { + c->shmBuffer = std::make_unique( + c->owner->wlDisplay.shm(), c->captureWidth, c->captureHeight, c->captureStride, c->captureFormat); + hyprland_toplevel_export_frame_v1_copy(export_frame, c->shmBuffer->getBuffer(), 1); + } catch (...) { + c->failed = true; + } +} + +void OverviewFrame::createTexture(CapturedClient& c) { + if (c.texture != 0 || !c.ready || !c.shmBuffer) + return; + + // ARGB to RGBA if necessary. SHM format is typically ARGB8888 or XRGB8888. + // OpenGL expects RGBA, so we might need to swap channels. + glGenTextures(1, &c.texture); + glBindTexture(GL_TEXTURE_2D, c.texture); + + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + + // assuming format is WL_SHM_FORMAT_XRGB8888 or ARGB8888 which is BGRA in memory (little endian) + // upload using GL_BGRA_EXT or we manual swizzle. + glTexImage2D(GL_TEXTURE_2D, + 0, + GL_RGBA, + c.captureWidth, + c.captureHeight, + 0, + GL_BGRA_EXT, + GL_UNSIGNED_BYTE, + c.shmBuffer->getData()); + + glGenerateMipmap(GL_TEXTURE_2D); + + // clean up shmBuffer as it's no longer needed in CPU RAM + c.shmBuffer.reset(); +} + +FrameResult OverviewFrame::render() { + // process generated textures + for (auto& w : workspaces) { + for (auto& c : w.clients) { + if (c->ready && c->texture == 0) { + createTexture(*c); + } + } + } + + if (ImGui::IsKeyPressed(ImGuiKey_LeftArrow) || ImGui::IsKeyPressed(ImGuiKey_H)) + navigate(-1); + if (ImGui::IsKeyPressed(ImGuiKey_RightArrow) || ImGui::IsKeyPressed(ImGuiKey_L)) + navigate(1); + if (ImGui::IsKeyPressed(ImGuiKey_Enter) || ImGui::IsKeyPressed(ImGuiKey_Space)) { + if (!workspaces.empty() && selectedIndex >= 0 && selectedIndex < workspaces.size()) { + return FrameResult::Submit(std::to_string(workspaces[selectedIndex].workspace.id)); + } + } + if (ImGui::IsKeyPressed(ImGuiKey_Escape)) { + return FrameResult::Cancel(); + } + + Vec2 size = getSize(); + float edge_padding = 20.0f; + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(edge_padding, edge_padding)); + ImGui::SetNextWindowPos(ImVec2(0, 0)); + ImGui::SetNextWindowSize(ImVec2(size.x, size.y), ImGuiCond_Always); + + ImGui::Begin("Overview", + nullptr, + ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoSavedSettings | + ImGuiWindowFlags_NoResize); + + ImVec2 contentRegion = ImGui::GetContentRegionAvail(); + float wsWidth = logicalWidth * scaleRatio; + float wsHeight = logicalHeight * scaleRatio; + float spacing = 20.0f; + float totalWidthPerWs = wsWidth + spacing; + + float targetScroll = selectedIndex * totalWidthPerWs - (contentRegion.x - wsWidth) * 0.5f; + if (targetScroll < 0) + targetScroll = 0; + scrollOffset += (targetScroll - scrollOffset) * 0.15f; + + if (!workspaces.empty()) { + ImGui::BeginChild("ScrollRegion", + ImVec2(0, contentRegion.y), + false, + ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse); + ImGui::SetScrollX(scrollOffset); + + for (size_t i = 0; i < workspaces.size(); ++i) { + if (i > 0) + ImGui::SameLine(); + + ImGui::BeginGroup(); + + ImVec2 wsMin = ImGui::GetCursorScreenPos(); + ImVec2 wsMax = ImVec2(wsMin.x + wsWidth, wsMin.y + wsHeight); + + bool is_selected = (i == selectedIndex); + + // highlight active or selected + ImU32 bgColor = is_selected ? ImGui::GetColorU32(hoverColor) : ImGui::GetColorU32(workspaceColor); + ImGui::GetWindowDrawList()->AddRectFilled(wsMin, wsMax, bgColor, workspaceRounding); + + if (is_selected) { + ImGui::GetForegroundDrawList()->AddRect( + wsMin, wsMax, IM_COL32(255, 255, 255, 200), workspaceRounding, 0, 2.0f); + } else { + ImGui::GetWindowDrawList()->AddRect( + wsMin, wsMax, IM_COL32(100, 100, 100, 150), workspaceRounding, 0, 1.0f); + } + + for (const auto& c : workspaces[i].clients) { + if (c->texture == 0) + continue; + + // calc scaled bounds + float cx = wsMin.x + c->client.x * scaleRatio; + float cy = wsMin.y + c->client.y * scaleRatio; + float cw = c->client.width * scaleRatio; + float ch = c->client.height * scaleRatio; + + // draw client texture + ImGui::GetWindowDrawList()->AddImageRounded((void*)(intptr_t)c->texture, + ImVec2(cx, cy), + ImVec2(cx + cw, cy + ch), + ImVec2(0, 0), + ImVec2(1, 1), + IM_COL32_WHITE, + clientRounding); + ImGui::GetWindowDrawList()->AddRect( + ImVec2(cx, cy), ImVec2(cx + cw, cy + ch), IM_COL32(50, 50, 50, 150), clientRounding, 0, 1.0f); + } + + ImGui::Dummy(ImVec2(wsWidth, wsHeight)); + ImGui::EndGroup(); + + if (i < workspaces.size() - 1) { + ImGui::SameLine(0.0f, spacing); + } + } + ImGui::EndChild(); + } + + ImGui::End(); + ImGui::PopStyleVar(); + + return FrameResult::Continue(); +} + +Vec2 OverviewFrame::getSize() { + float wsHeight = logicalHeight * scaleRatio; + float wsWidth = logicalWidth * scaleRatio; + float spacing = 20.0f; + + float requiredWidth = workspaces.size() * wsWidth + (workspaces.empty() ? 0 : (workspaces.size() - 1) * spacing); + float maxW = (float)logicalWidth * widthRatio; + + float w = std::min(requiredWidth, maxW); + if (w < wsWidth) + w = wsWidth; // At least one workspace width if empty + + float edgePadding = 20.0f; + return Vec2{w + (edgePadding * 2), wsHeight + (edgePadding * 2)}; +} + +void OverviewFrame::navigate(int direction) { + if (workspaces.empty()) + return; + + selectedIndex += direction; + if (selectedIndex < 0) + selectedIndex = 0; + if (selectedIndex >= workspaces.size()) + selectedIndex = workspaces.size() - 1; +} + +void OverviewFrame::applyTheme(const Config& config) { + hoverColor = config.getColor("theme", "hover_color", "#3366B366"); + workspaceColor = config.getColor("theme", "workspace_color", "#1A1A1CCC"); + workspaceRounding = config.getFloat("theme", "frame_rounding", 12.0f); + widthRatio = config.getFloat("theme", "wallpaper_width_ratio", 0.8f); +} diff --git a/src/frames/overview.hpp b/src/frames/overview.hpp new file mode 100644 index 0000000..e5f575d --- /dev/null +++ b/src/frames/overview.hpp @@ -0,0 +1,94 @@ +#pragma once + +#include "../hyprland/ipc.hpp" +#include "../ui.hpp" +#include "../wayland/display.hpp" +#include "../wayland/protocols/hyprland-toplevel-export-v1-client-protocol.h" +#include "../wayland/shm.hpp" +#include +#include +#include +#include + +class OverviewFrame : public Frame { +public: + OverviewFrame(hyprland::Control& hyprctl, wl::Display& wlDisplay, int logicalWidth, int logicalHeight); + ~OverviewFrame() override; + + FrameResult render() override; + Vec2 getSize() override; + void applyTheme(const Config& config) override; + bool shouldRepositionOnResize() const override { return false; } + bool shouldPositionAtCursor() const override { return false; } + +private: + struct CapturedClient { + OverviewFrame* owner = nullptr; + hyprland::Client client; + struct hyprland_toplevel_export_frame_v1* frame = nullptr; + std::unique_ptr shmBuffer; + GLuint texture = 0; + bool ready = false; + bool failed = false; + int captureFormat = 0; + int captureWidth = 0; + int captureHeight = 0; + int captureStride = 0; + }; + + struct WorkspaceView { + hyprland::Workspace workspace; + std::vector> clients; + }; + + int selectedIndex = 0; + int logicalWidth; + int logicalHeight; + hyprland::Control& hyprctl; + wl::Display& wlDisplay; + + std::vector workspaces; + std::vector> pendingCaptures; + std::mutex captureMutex; + + float padding = 20.0f; + float workspaceRounding = 12.0f; + float clientRounding = 6.0f; + float scaleRatio = 0.2f; // Workspace thumbnail size scaled down relative to monitor + float widthRatio = 0.8f; + ImVec4 hoverColor = ImVec4(0.2f, 0.4f, 0.7f, 1.0f); + ImVec4 workspaceColor = ImVec4(0.1f, 0.1f, 0.15f, 0.8f); + float scrollOffset = 0.0f; + + void captureClients(); + void navigate(int direction); + void createTexture(CapturedClient& c); + + static void handle_buffer(void* data, + struct hyprland_toplevel_export_frame_v1* export_frame, + uint32_t format, + uint32_t width, + uint32_t height, + uint32_t stride); + static void handle_damage(void* data, + struct hyprland_toplevel_export_frame_v1* export_frame, + uint32_t x, + uint32_t y, + uint32_t width, + uint32_t height); + static void handle_flags(void* data, struct hyprland_toplevel_export_frame_v1* export_frame, uint32_t flags); + static void handle_ready(void* data, + struct hyprland_toplevel_export_frame_v1* export_frame, + uint32_t tv_sec_hi, + uint32_t tv_sec_lo, + uint32_t tv_nsec); + static void handle_failed(void* data, struct hyprland_toplevel_export_frame_v1* export_frame); + static void handle_linux_dmabuf(void* data, + struct hyprland_toplevel_export_frame_v1* export_frame, + uint32_t format, + uint32_t width, + uint32_t height); + static void handle_buffer_done(void* data, struct hyprland_toplevel_export_frame_v1* export_frame); + + static const struct hyprland_toplevel_export_frame_v1_listener export_frame_listener; +}; diff --git a/src/hyprland/ipc.cpp b/src/hyprland/ipc.cpp index 96c6e6e..b5b38ad 100644 --- a/src/hyprland/ipc.cpp +++ b/src/hyprland/ipc.cpp @@ -1,5 +1,6 @@ #include "ipc.hpp" #include "../debug/log.hpp" +#include #include #include @@ -44,15 +45,21 @@ namespace hyprland { // read response char buf[4096]; - ssize_t n = read(wfd, buf, sizeof(buf) - 1); - if (n < 0) { - close(wfd); - throw std::runtime_error("Failed to read response"); + std::string response; + while (true) { + ssize_t n = read(wfd, buf, sizeof(buf)); + if (n < 0) { + close(wfd); + throw std::runtime_error("Failed to read response"); + } + if (n == 0) { + break; + } + response.append(buf, n); } - buf[n] = '\0'; close(wfd); - return std::string(buf); + return response; } Vec2 Control::cursorPos() { @@ -91,6 +98,64 @@ namespace hyprland { send("/keyword exec hyprctl hyprpaper unload unused"); } + std::vector Control::getWorkspaces() { + std::vector result; + std::string response = send("j/workspaces"); + try { + auto node = YAML::Load(response); + if (node.IsSequence()) { + for (const auto& w : node) { + Workspace workspace; + workspace.id = w["id"].as(); + workspace.name = w["name"].as(); + workspace.monitor = w["monitor"].as(); + workspace.active = false; // not provided easily, hyprctl activeworkspace has it + result.push_back(workspace); + } + } + } catch (const std::exception& e) { + debug::log(ERR, "Failed to parse workspaces: {}", e.what()); + } + return result; + } + + std::vector Control::getClients() { + std::vector result; + std::string response = send("j/clients"); + try { + auto node = YAML::Load(response); + if (node.IsSequence()) { + for (const auto& c : node) { + Client client; + client.address = c["address"].as(); + client.title = c["title"].as(); + client.class_ = c["class"].as(); + client.initialClass = c["initialClass"].as(); + client.initialTitle = c["initialTitle"].as(); + client.workspaceId = c["workspace"]["id"].as(); + auto at = c["at"].as>(); + if (at.size() >= 2) { + client.x = at[0]; + client.y = at[1]; + } + auto size = c["size"].as>(); + if (size.size() >= 2) { + client.width = size[0]; + client.height = size[1]; + } + client.mapped = c["mapped"].as(true); + client.hidden = c["hidden"].as(false); + result.push_back(client); + } + } + } catch (const std::exception& e) { + debug::log(ERR, "Failed to parse clients: {}", e.what()); + } + return result; + } + + void Control::dispatchWorkspace(int id) { send("dispatch workspace " + std::to_string(id)); } + // Events Events::Events() : Events(getSocketPath(".socket2.sock")) {} diff --git a/src/hyprland/ipc.hpp b/src/hyprland/ipc.hpp index acfc738..32267cb 100644 --- a/src/hyprland/ipc.hpp +++ b/src/hyprland/ipc.hpp @@ -5,8 +5,30 @@ #include #include #include +#include namespace hyprland { + struct Workspace { + int id; + std::string name; + std::string monitor; + bool active = false; + }; + + struct Client { + std::string address; + std::string title; + std::string class_; + std::string initialClass; + std::string initialTitle; + int workspaceId = -1; + int x = 0; + int y = 0; + int width = 0; + int height = 0; + bool mapped = false; + bool hidden = false; + }; class Control { public: explicit Control(); @@ -22,6 +44,10 @@ namespace hyprland { void setWallpaper(const std::string& path); + std::vector getWorkspaces(); + std::vector getClients(); + void dispatchWorkspace(int id); + private: std::string socketPath; }; diff --git a/src/input.cpp b/src/input.cpp index eb65815..891189f 100644 --- a/src/input.cpp +++ b/src/input.cpp @@ -42,6 +42,11 @@ ParseResult Input::parseArgv(int argc, const char* argv[]) { return result; } + if (std::string(argv[1]) == "--overview") { + result.mode = InputMode::OVERVIEW; + return result; + } + if (std::string(argv[1]) == "--wallpaper") { result.mode = InputMode::WALLPAPER; if (argc > 2) { diff --git a/src/input.hpp b/src/input.hpp index 1efba02..ec8a025 100644 --- a/src/input.hpp +++ b/src/input.hpp @@ -8,13 +8,14 @@ #include enum class InputMode { - MENU, // default menu selection mode - INPUT, // text input mode - PASSWORD, // password input mode - WIFI, // wifi selection + password mode - AUDIO, // audio input/output selection mode - CUSTOM, // custom menu from config file - WALLPAPER // wallpaper selection mode + MENU, // default menu selection mode + INPUT, // text input mode + PASSWORD, // password input mode + WIFI, // wifi selection + password mode + AUDIO, // audio input/output selection mode + CUSTOM, // custom menu from config file + WALLPAPER, // wallpaper selection mode + OVERVIEW // overview mode }; struct ParseResult { diff --git a/src/main.cpp b/src/main.cpp index 4305993..90cbb0c 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,6 +1,7 @@ #include "flows/audio_flow.hpp" #include "flows/custom_flow.hpp" #include "flows/flow.hpp" +#include "flows/overview_flow.hpp" #include "flows/simple_flows.hpp" #include "flows/wallpaper_flow.hpp" #include "flows/wifi_flow.hpp" @@ -20,6 +21,7 @@ void usage() { hyprwat --password [hint] hyprwat --wifi hyprwat --audio + hyprwat --overview hyprwat --wallpaper Description: @@ -63,6 +65,9 @@ CUSTOM MODE: WALLPAPER MODE: Use --wallpaper to select wallpapers from a specified directory and set them using hyprpaper. +OVERVIEW MODE: + Use --overview to show a visual grid of all workspaces and windows and navigate between them. + Options: -h, --help Show this help message --input [hint] Show text input mode with optional hint text @@ -71,6 +76,7 @@ WALLPAPER MODE: --audio Show audio input/output device selection mode --custom Load a custom flow from the specified configuration file --wallpaper Select wallpapers from the specified directory and set using hyprpaper + --overview Show a visual workspace overview and selector )"); } @@ -140,6 +146,9 @@ int main(const int argc, const char** argv) { case InputMode::CUSTOM: flow = std::make_unique(args.configPath); break; + case InputMode::OVERVIEW: + flow = std::make_unique(hyprctl, wayland.display(), logicalDisplayWidth, logicalDisplayHeight); + break; case InputMode::WALLPAPER: flow = std::make_unique(hyprctl, args.wallpaperDir, logicalDisplayWidth, logicalDisplayHeight); break; diff --git a/src/wayland/display.cpp b/src/wayland/display.cpp index d57011e..77726ba 100644 --- a/src/wayland/display.cpp +++ b/src/wayland/display.cpp @@ -11,6 +11,10 @@ namespace wl { if (output.output) wl_output_destroy(output.output); } + if (exportManager_) + hyprland_toplevel_export_manager_v1_destroy(exportManager_); + if (shm_) + wl_shm_destroy(shm_); if (seat_) wl_seat_destroy(seat_); if (layerShell_) @@ -71,6 +75,11 @@ namespace wl { static_cast(wl_registry_bind(registry, id, &zwlr_layer_shell_v1_interface, 1)); } else if (strcmp(interface, wl_seat_interface.name) == 0) { self->seat_ = static_cast(wl_registry_bind(registry, id, &wl_seat_interface, 5)); + } else if (strcmp(interface, wl_shm_interface.name) == 0) { + self->shm_ = static_cast(wl_registry_bind(registry, id, &wl_shm_interface, 1)); + } else if (strcmp(interface, hyprland_toplevel_export_manager_v1_interface.name) == 0) { + self->exportManager_ = static_cast( + wl_registry_bind(registry, id, &hyprland_toplevel_export_manager_v1_interface, 2)); } else if (strcmp(interface, wl_output_interface.name) == 0) { wl_output* output = static_cast(wl_registry_bind(registry, id, &wl_output_interface, 4)); diff --git a/src/wayland/display.hpp b/src/wayland/display.hpp index 3bdf7a6..3ceba89 100644 --- a/src/wayland/display.hpp +++ b/src/wayland/display.hpp @@ -1,6 +1,7 @@ #pragma once extern "C" { +#include "protocols/hyprland-toplevel-export-v1-client-protocol.h" #include "protocols/wlr-layer-shell-unstable-v1-client-protocol.h" #include } @@ -34,6 +35,8 @@ namespace wl { wl_compositor* compositor() const { return compositor_; } zwlr_layer_shell_v1* layerShell() const { return layerShell_; } wl_seat* seat() const { return seat_; } + wl_shm* shm() const { return shm_; } + hyprland_toplevel_export_manager_v1* exportManager() const { return exportManager_; } // Output scale management const std::vector& outputs() const { return outputs_; } @@ -49,6 +52,8 @@ namespace wl { wl_compositor* compositor_ = nullptr; zwlr_layer_shell_v1* layerShell_ = nullptr; wl_seat* seat_ = nullptr; + wl_shm* shm_ = nullptr; + hyprland_toplevel_export_manager_v1* exportManager_ = nullptr; std::vector outputs_; std::function scaleCallback; diff --git a/src/wayland/protocols/hyprland-toplevel-export-v1-client-protocol.c b/src/wayland/protocols/hyprland-toplevel-export-v1-client-protocol.c new file mode 100644 index 0000000..5888f9b --- /dev/null +++ b/src/wayland/protocols/hyprland-toplevel-export-v1-client-protocol.c @@ -0,0 +1,99 @@ +/* Generated by wayland-scanner 1.24.0 */ + +/* + * Copyright © 2022 Vaxry + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include +#include +#include +#include "wayland-util.h" + +#ifndef __has_attribute +# define __has_attribute(x) 0 /* Compatibility with non-clang compilers. */ +#endif + +#if (__has_attribute(visibility) || defined(__GNUC__) && __GNUC__ >= 4) +#define WL_PRIVATE __attribute__ ((visibility("hidden"))) +#else +#define WL_PRIVATE +#endif + +extern const struct wl_interface hyprland_toplevel_export_frame_v1_interface; +extern const struct wl_interface wl_buffer_interface; +extern const struct wl_interface zwlr_foreign_toplevel_handle_v1_interface; + +static const struct wl_interface *hyprland_toplevel_export_v1_types[] = { + NULL, + NULL, + NULL, + NULL, + &hyprland_toplevel_export_frame_v1_interface, + NULL, + NULL, + &hyprland_toplevel_export_frame_v1_interface, + NULL, + &zwlr_foreign_toplevel_handle_v1_interface, + &wl_buffer_interface, + NULL, +}; + +static const struct wl_message hyprland_toplevel_export_manager_v1_requests[] = { + { "capture_toplevel", "niu", hyprland_toplevel_export_v1_types + 4 }, + { "destroy", "", hyprland_toplevel_export_v1_types + 0 }, + { "capture_toplevel_with_wlr_toplevel_handle", "2nio", hyprland_toplevel_export_v1_types + 7 }, +}; + +WL_PRIVATE const struct wl_interface hyprland_toplevel_export_manager_v1_interface = { + "hyprland_toplevel_export_manager_v1", 2, + 3, hyprland_toplevel_export_manager_v1_requests, + 0, NULL, +}; + +static const struct wl_message hyprland_toplevel_export_frame_v1_requests[] = { + { "copy", "oi", hyprland_toplevel_export_v1_types + 10 }, + { "destroy", "", hyprland_toplevel_export_v1_types + 0 }, +}; + +static const struct wl_message hyprland_toplevel_export_frame_v1_events[] = { + { "buffer", "uuuu", hyprland_toplevel_export_v1_types + 0 }, + { "damage", "uuuu", hyprland_toplevel_export_v1_types + 0 }, + { "flags", "u", hyprland_toplevel_export_v1_types + 0 }, + { "ready", "uuu", hyprland_toplevel_export_v1_types + 0 }, + { "failed", "", hyprland_toplevel_export_v1_types + 0 }, + { "linux_dmabuf", "uuu", hyprland_toplevel_export_v1_types + 0 }, + { "buffer_done", "", hyprland_toplevel_export_v1_types + 0 }, +}; + +WL_PRIVATE const struct wl_interface hyprland_toplevel_export_frame_v1_interface = { + "hyprland_toplevel_export_frame_v1", 2, + 2, hyprland_toplevel_export_frame_v1_requests, + 7, hyprland_toplevel_export_frame_v1_events, +}; + diff --git a/src/wayland/protocols/hyprland-toplevel-export-v1-client-protocol.h b/src/wayland/protocols/hyprland-toplevel-export-v1-client-protocol.h new file mode 100644 index 0000000..34a3f9c --- /dev/null +++ b/src/wayland/protocols/hyprland-toplevel-export-v1-client-protocol.h @@ -0,0 +1,475 @@ +/* Generated by wayland-scanner 1.24.0 */ + +#ifndef HYPRLAND_TOPLEVEL_EXPORT_V1_CLIENT_PROTOCOL_H +#define HYPRLAND_TOPLEVEL_EXPORT_V1_CLIENT_PROTOCOL_H + +#include +#include +#include "wayland-client.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @page page_hyprland_toplevel_export_v1 The hyprland_toplevel_export_v1 protocol + * capturing the contents of toplevel windows + * + * @section page_desc_hyprland_toplevel_export_v1 Description + * + * This protocol allows clients to ask for exporting another toplevel's + * surface(s) to a buffer. + * + * Particularly useful for sharing a single window. + * + * @section page_ifaces_hyprland_toplevel_export_v1 Interfaces + * - @subpage page_iface_hyprland_toplevel_export_manager_v1 - manager to inform clients and begin capturing + * - @subpage page_iface_hyprland_toplevel_export_frame_v1 - a frame ready for copy + * @section page_copyright_hyprland_toplevel_export_v1 Copyright + *
+ *
+ * Copyright © 2022 Vaxry
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * 3. Neither the name of the copyright holder nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ * 
+ */ +struct hyprland_toplevel_export_frame_v1; +struct hyprland_toplevel_export_manager_v1; +struct wl_buffer; +struct zwlr_foreign_toplevel_handle_v1; + +#ifndef HYPRLAND_TOPLEVEL_EXPORT_MANAGER_V1_INTERFACE +#define HYPRLAND_TOPLEVEL_EXPORT_MANAGER_V1_INTERFACE +/** + * @page page_iface_hyprland_toplevel_export_manager_v1 hyprland_toplevel_export_manager_v1 + * @section page_iface_hyprland_toplevel_export_manager_v1_desc Description + * + * This object is a manager which offers requests to start capturing from a + * source. + * @section page_iface_hyprland_toplevel_export_manager_v1_api API + * See @ref iface_hyprland_toplevel_export_manager_v1. + */ +/** + * @defgroup iface_hyprland_toplevel_export_manager_v1 The hyprland_toplevel_export_manager_v1 interface + * + * This object is a manager which offers requests to start capturing from a + * source. + */ +extern const struct wl_interface hyprland_toplevel_export_manager_v1_interface; +#endif +#ifndef HYPRLAND_TOPLEVEL_EXPORT_FRAME_V1_INTERFACE +#define HYPRLAND_TOPLEVEL_EXPORT_FRAME_V1_INTERFACE +/** + * @page page_iface_hyprland_toplevel_export_frame_v1 hyprland_toplevel_export_frame_v1 + * @section page_iface_hyprland_toplevel_export_frame_v1_desc Description + * + * This object represents a single frame. + * + * When created, a series of buffer events will be sent, each representing a + * supported buffer type. The "buffer_done" event is sent afterwards to + * indicate that all supported buffer types have been enumerated. The client + * will then be able to send a "copy" request. If the capture is successful, + * the compositor will send a "flags" followed by a "ready" event. + * + * wl_shm buffers are always supported, ie. the "buffer" event is guaranteed to be sent. + * + * If the capture failed, the "failed" event is sent. This can happen anytime + * before the "ready" event. + * + * Once either a "ready" or a "failed" event is received, the client should + * destroy the frame. + * @section page_iface_hyprland_toplevel_export_frame_v1_api API + * See @ref iface_hyprland_toplevel_export_frame_v1. + */ +/** + * @defgroup iface_hyprland_toplevel_export_frame_v1 The hyprland_toplevel_export_frame_v1 interface + * + * This object represents a single frame. + * + * When created, a series of buffer events will be sent, each representing a + * supported buffer type. The "buffer_done" event is sent afterwards to + * indicate that all supported buffer types have been enumerated. The client + * will then be able to send a "copy" request. If the capture is successful, + * the compositor will send a "flags" followed by a "ready" event. + * + * wl_shm buffers are always supported, ie. the "buffer" event is guaranteed to be sent. + * + * If the capture failed, the "failed" event is sent. This can happen anytime + * before the "ready" event. + * + * Once either a "ready" or a "failed" event is received, the client should + * destroy the frame. + */ +extern const struct wl_interface hyprland_toplevel_export_frame_v1_interface; +#endif + +#define HYPRLAND_TOPLEVEL_EXPORT_MANAGER_V1_CAPTURE_TOPLEVEL 0 +#define HYPRLAND_TOPLEVEL_EXPORT_MANAGER_V1_DESTROY 1 +#define HYPRLAND_TOPLEVEL_EXPORT_MANAGER_V1_CAPTURE_TOPLEVEL_WITH_WLR_TOPLEVEL_HANDLE 2 + + +/** + * @ingroup iface_hyprland_toplevel_export_manager_v1 + */ +#define HYPRLAND_TOPLEVEL_EXPORT_MANAGER_V1_CAPTURE_TOPLEVEL_SINCE_VERSION 1 +/** + * @ingroup iface_hyprland_toplevel_export_manager_v1 + */ +#define HYPRLAND_TOPLEVEL_EXPORT_MANAGER_V1_DESTROY_SINCE_VERSION 1 +/** + * @ingroup iface_hyprland_toplevel_export_manager_v1 + */ +#define HYPRLAND_TOPLEVEL_EXPORT_MANAGER_V1_CAPTURE_TOPLEVEL_WITH_WLR_TOPLEVEL_HANDLE_SINCE_VERSION 2 + +/** @ingroup iface_hyprland_toplevel_export_manager_v1 */ +static inline void +hyprland_toplevel_export_manager_v1_set_user_data(struct hyprland_toplevel_export_manager_v1 *hyprland_toplevel_export_manager_v1, void *user_data) +{ + wl_proxy_set_user_data((struct wl_proxy *) hyprland_toplevel_export_manager_v1, user_data); +} + +/** @ingroup iface_hyprland_toplevel_export_manager_v1 */ +static inline void * +hyprland_toplevel_export_manager_v1_get_user_data(struct hyprland_toplevel_export_manager_v1 *hyprland_toplevel_export_manager_v1) +{ + return wl_proxy_get_user_data((struct wl_proxy *) hyprland_toplevel_export_manager_v1); +} + +static inline uint32_t +hyprland_toplevel_export_manager_v1_get_version(struct hyprland_toplevel_export_manager_v1 *hyprland_toplevel_export_manager_v1) +{ + return wl_proxy_get_version((struct wl_proxy *) hyprland_toplevel_export_manager_v1); +} + +/** + * @ingroup iface_hyprland_toplevel_export_manager_v1 + * + * Capture the next frame of a toplevel. (window) + * + * The captured frame will not contain any server-side decorations and will + * ignore the compositor-set geometry, like e.g. rounded corners. + * + * It will contain all the subsurfaces and popups, however the latter will be clipped + * to the geometry of the base surface. + * + * The handle parameter refers to the address of the window as seen in `hyprctl clients`. + * For example, for d161e7b0 it would be 3512854448. + */ +static inline struct hyprland_toplevel_export_frame_v1 * +hyprland_toplevel_export_manager_v1_capture_toplevel(struct hyprland_toplevel_export_manager_v1 *hyprland_toplevel_export_manager_v1, int32_t overlay_cursor, uint32_t handle) +{ + struct wl_proxy *frame; + + frame = wl_proxy_marshal_flags((struct wl_proxy *) hyprland_toplevel_export_manager_v1, + HYPRLAND_TOPLEVEL_EXPORT_MANAGER_V1_CAPTURE_TOPLEVEL, &hyprland_toplevel_export_frame_v1_interface, wl_proxy_get_version((struct wl_proxy *) hyprland_toplevel_export_manager_v1), 0, NULL, overlay_cursor, handle); + + return (struct hyprland_toplevel_export_frame_v1 *) frame; +} + +/** + * @ingroup iface_hyprland_toplevel_export_manager_v1 + * + * All objects created by the manager will still remain valid, until their + * appropriate destroy request has been called. + */ +static inline void +hyprland_toplevel_export_manager_v1_destroy(struct hyprland_toplevel_export_manager_v1 *hyprland_toplevel_export_manager_v1) +{ + wl_proxy_marshal_flags((struct wl_proxy *) hyprland_toplevel_export_manager_v1, + HYPRLAND_TOPLEVEL_EXPORT_MANAGER_V1_DESTROY, NULL, wl_proxy_get_version((struct wl_proxy *) hyprland_toplevel_export_manager_v1), WL_MARSHAL_FLAG_DESTROY); +} + +/** + * @ingroup iface_hyprland_toplevel_export_manager_v1 + * + * Same as capture_toplevel, but with a zwlr_foreign_toplevel_handle_v1 handle. + */ +static inline struct hyprland_toplevel_export_frame_v1 * +hyprland_toplevel_export_manager_v1_capture_toplevel_with_wlr_toplevel_handle(struct hyprland_toplevel_export_manager_v1 *hyprland_toplevel_export_manager_v1, int32_t overlay_cursor, struct zwlr_foreign_toplevel_handle_v1 *handle) +{ + struct wl_proxy *frame; + + frame = wl_proxy_marshal_flags((struct wl_proxy *) hyprland_toplevel_export_manager_v1, + HYPRLAND_TOPLEVEL_EXPORT_MANAGER_V1_CAPTURE_TOPLEVEL_WITH_WLR_TOPLEVEL_HANDLE, &hyprland_toplevel_export_frame_v1_interface, wl_proxy_get_version((struct wl_proxy *) hyprland_toplevel_export_manager_v1), 0, NULL, overlay_cursor, handle); + + return (struct hyprland_toplevel_export_frame_v1 *) frame; +} + +#ifndef HYPRLAND_TOPLEVEL_EXPORT_FRAME_V1_ERROR_ENUM +#define HYPRLAND_TOPLEVEL_EXPORT_FRAME_V1_ERROR_ENUM +enum hyprland_toplevel_export_frame_v1_error { + /** + * the object has already been used to copy a wl_buffer + */ + HYPRLAND_TOPLEVEL_EXPORT_FRAME_V1_ERROR_ALREADY_USED = 0, + /** + * buffer attributes are invalid + */ + HYPRLAND_TOPLEVEL_EXPORT_FRAME_V1_ERROR_INVALID_BUFFER = 1, +}; +#endif /* HYPRLAND_TOPLEVEL_EXPORT_FRAME_V1_ERROR_ENUM */ + +#ifndef HYPRLAND_TOPLEVEL_EXPORT_FRAME_V1_FLAGS_ENUM +#define HYPRLAND_TOPLEVEL_EXPORT_FRAME_V1_FLAGS_ENUM +enum hyprland_toplevel_export_frame_v1_flags { + /** + * contents are y-inverted + */ + HYPRLAND_TOPLEVEL_EXPORT_FRAME_V1_FLAGS_Y_INVERT = 1, +}; +#endif /* HYPRLAND_TOPLEVEL_EXPORT_FRAME_V1_FLAGS_ENUM */ + +/** + * @ingroup iface_hyprland_toplevel_export_frame_v1 + * @struct hyprland_toplevel_export_frame_v1_listener + */ +struct hyprland_toplevel_export_frame_v1_listener { + /** + * wl_shm buffer information + * + * Provides information about wl_shm buffer parameters that need + * to be used for this frame. This event is sent once after the + * frame is created if wl_shm buffers are supported. + * @param format buffer format + * @param width buffer width + * @param height buffer height + * @param stride buffer stride + */ + void (*buffer)(void *data, + struct hyprland_toplevel_export_frame_v1 *hyprland_toplevel_export_frame_v1, + uint32_t format, + uint32_t width, + uint32_t height, + uint32_t stride); + /** + * carries the coordinates of the damaged region + * + * This event is sent right before the ready event when + * ignore_damage was not set. It may be generated multiple times + * for each copy request. + * + * The arguments describe a box around an area that has changed + * since the last copy request that was derived from the current + * screencopy manager instance. + * + * The union of all regions received between the call to copy and a + * ready event is the total damage since the prior ready event. + * @param x damaged x coordinates + * @param y damaged y coordinates + * @param width current width + * @param height current height + */ + void (*damage)(void *data, + struct hyprland_toplevel_export_frame_v1 *hyprland_toplevel_export_frame_v1, + uint32_t x, + uint32_t y, + uint32_t width, + uint32_t height); + /** + * frame flags + * + * Provides flags about the frame. This event is sent once before + * the "ready" event. + * @param flags frame flags + */ + void (*flags)(void *data, + struct hyprland_toplevel_export_frame_v1 *hyprland_toplevel_export_frame_v1, + uint32_t flags); + /** + * indicates frame is available for reading + * + * Called as soon as the frame is copied, indicating it is + * available for reading. This event includes the time at which + * presentation happened at. + * + * The timestamp is expressed as tv_sec_hi, tv_sec_lo, tv_nsec + * triples, each component being an unsigned 32-bit value. Whole + * seconds are in tv_sec which is a 64-bit value combined from + * tv_sec_hi and tv_sec_lo, and the additional fractional part in + * tv_nsec as nanoseconds. Hence, for valid timestamps tv_nsec must + * be in [0, 999999999]. The seconds part may have an arbitrary + * offset at start. + * + * After receiving this event, the client should destroy the + * object. + * @param tv_sec_hi high 32 bits of the seconds part of the timestamp + * @param tv_sec_lo low 32 bits of the seconds part of the timestamp + * @param tv_nsec nanoseconds part of the timestamp + */ + void (*ready)(void *data, + struct hyprland_toplevel_export_frame_v1 *hyprland_toplevel_export_frame_v1, + uint32_t tv_sec_hi, + uint32_t tv_sec_lo, + uint32_t tv_nsec); + /** + * frame copy failed + * + * This event indicates that the attempted frame copy has failed. + * + * After receiving this event, the client should destroy the + * object. + */ + void (*failed)(void *data, + struct hyprland_toplevel_export_frame_v1 *hyprland_toplevel_export_frame_v1); + /** + * linux-dmabuf buffer information + * + * Provides information about linux-dmabuf buffer parameters that + * need to be used for this frame. This event is sent once after + * the frame is created if linux-dmabuf buffers are supported. + * @param format fourcc pixel format + * @param width buffer width + * @param height buffer height + */ + void (*linux_dmabuf)(void *data, + struct hyprland_toplevel_export_frame_v1 *hyprland_toplevel_export_frame_v1, + uint32_t format, + uint32_t width, + uint32_t height); + /** + * all buffer types reported + * + * This event is sent once after all buffer events have been + * sent. + * + * The client should proceed to create a buffer of one of the + * supported types, and send a "copy" request. + */ + void (*buffer_done)(void *data, + struct hyprland_toplevel_export_frame_v1 *hyprland_toplevel_export_frame_v1); +}; + +/** + * @ingroup iface_hyprland_toplevel_export_frame_v1 + */ +static inline int +hyprland_toplevel_export_frame_v1_add_listener(struct hyprland_toplevel_export_frame_v1 *hyprland_toplevel_export_frame_v1, + const struct hyprland_toplevel_export_frame_v1_listener *listener, void *data) +{ + return wl_proxy_add_listener((struct wl_proxy *) hyprland_toplevel_export_frame_v1, + (void (**)(void)) listener, data); +} + +#define HYPRLAND_TOPLEVEL_EXPORT_FRAME_V1_COPY 0 +#define HYPRLAND_TOPLEVEL_EXPORT_FRAME_V1_DESTROY 1 + +/** + * @ingroup iface_hyprland_toplevel_export_frame_v1 + */ +#define HYPRLAND_TOPLEVEL_EXPORT_FRAME_V1_BUFFER_SINCE_VERSION 1 +/** + * @ingroup iface_hyprland_toplevel_export_frame_v1 + */ +#define HYPRLAND_TOPLEVEL_EXPORT_FRAME_V1_DAMAGE_SINCE_VERSION 1 +/** + * @ingroup iface_hyprland_toplevel_export_frame_v1 + */ +#define HYPRLAND_TOPLEVEL_EXPORT_FRAME_V1_FLAGS_SINCE_VERSION 1 +/** + * @ingroup iface_hyprland_toplevel_export_frame_v1 + */ +#define HYPRLAND_TOPLEVEL_EXPORT_FRAME_V1_READY_SINCE_VERSION 1 +/** + * @ingroup iface_hyprland_toplevel_export_frame_v1 + */ +#define HYPRLAND_TOPLEVEL_EXPORT_FRAME_V1_FAILED_SINCE_VERSION 1 +/** + * @ingroup iface_hyprland_toplevel_export_frame_v1 + */ +#define HYPRLAND_TOPLEVEL_EXPORT_FRAME_V1_LINUX_DMABUF_SINCE_VERSION 1 +/** + * @ingroup iface_hyprland_toplevel_export_frame_v1 + */ +#define HYPRLAND_TOPLEVEL_EXPORT_FRAME_V1_BUFFER_DONE_SINCE_VERSION 1 + +/** + * @ingroup iface_hyprland_toplevel_export_frame_v1 + */ +#define HYPRLAND_TOPLEVEL_EXPORT_FRAME_V1_COPY_SINCE_VERSION 1 +/** + * @ingroup iface_hyprland_toplevel_export_frame_v1 + */ +#define HYPRLAND_TOPLEVEL_EXPORT_FRAME_V1_DESTROY_SINCE_VERSION 1 + +/** @ingroup iface_hyprland_toplevel_export_frame_v1 */ +static inline void +hyprland_toplevel_export_frame_v1_set_user_data(struct hyprland_toplevel_export_frame_v1 *hyprland_toplevel_export_frame_v1, void *user_data) +{ + wl_proxy_set_user_data((struct wl_proxy *) hyprland_toplevel_export_frame_v1, user_data); +} + +/** @ingroup iface_hyprland_toplevel_export_frame_v1 */ +static inline void * +hyprland_toplevel_export_frame_v1_get_user_data(struct hyprland_toplevel_export_frame_v1 *hyprland_toplevel_export_frame_v1) +{ + return wl_proxy_get_user_data((struct wl_proxy *) hyprland_toplevel_export_frame_v1); +} + +static inline uint32_t +hyprland_toplevel_export_frame_v1_get_version(struct hyprland_toplevel_export_frame_v1 *hyprland_toplevel_export_frame_v1) +{ + return wl_proxy_get_version((struct wl_proxy *) hyprland_toplevel_export_frame_v1); +} + +/** + * @ingroup iface_hyprland_toplevel_export_frame_v1 + * + * Copy the frame to the supplied buffer. The buffer must have the + * correct size, see hyprland_toplevel_export_frame_v1.buffer and + * hyprland_toplevel_export_frame_v1.linux_dmabuf. The buffer needs to have a + * supported format. + * + * If the frame is successfully copied, a "flags" and a "ready" event is + * sent. Otherwise, a "failed" event is sent. + * + * This event will wait for appropriate damage to be copied, unless the ignore_damage + * arg is set to a non-zero value. + */ +static inline void +hyprland_toplevel_export_frame_v1_copy(struct hyprland_toplevel_export_frame_v1 *hyprland_toplevel_export_frame_v1, struct wl_buffer *buffer, int32_t ignore_damage) +{ + wl_proxy_marshal_flags((struct wl_proxy *) hyprland_toplevel_export_frame_v1, + HYPRLAND_TOPLEVEL_EXPORT_FRAME_V1_COPY, NULL, wl_proxy_get_version((struct wl_proxy *) hyprland_toplevel_export_frame_v1), 0, buffer, ignore_damage); +} + +/** + * @ingroup iface_hyprland_toplevel_export_frame_v1 + * + * Destroys the frame. This request can be sent at any time by the client. + */ +static inline void +hyprland_toplevel_export_frame_v1_destroy(struct hyprland_toplevel_export_frame_v1 *hyprland_toplevel_export_frame_v1) +{ + wl_proxy_marshal_flags((struct wl_proxy *) hyprland_toplevel_export_frame_v1, + HYPRLAND_TOPLEVEL_EXPORT_FRAME_V1_DESTROY, NULL, wl_proxy_get_version((struct wl_proxy *) hyprland_toplevel_export_frame_v1), WL_MARSHAL_FLAG_DESTROY); +} + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/src/wayland/protocols/hyprland-toplevel-export-v1.xml b/src/wayland/protocols/hyprland-toplevel-export-v1.xml new file mode 100644 index 0000000..b1185aa --- /dev/null +++ b/src/wayland/protocols/hyprland-toplevel-export-v1.xml @@ -0,0 +1,228 @@ + + + + Copyright © 2022 Vaxry + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + 3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + + + This protocol allows clients to ask for exporting another toplevel's + surface(s) to a buffer. + + Particularly useful for sharing a single window. + + + + + This object is a manager which offers requests to start capturing from a + source. + + + + + Capture the next frame of a toplevel. (window) + + The captured frame will not contain any server-side decorations and will + ignore the compositor-set geometry, like e.g. rounded corners. + + It will contain all the subsurfaces and popups, however the latter will be clipped + to the geometry of the base surface. + + The handle parameter refers to the address of the window as seen in `hyprctl clients`. + For example, for d161e7b0 it would be 3512854448. + + + + + + + + + All objects created by the manager will still remain valid, until their + appropriate destroy request has been called. + + + + + + + Same as capture_toplevel, but with a zwlr_foreign_toplevel_handle_v1 handle. + + + + + + + + + + + This object represents a single frame. + + When created, a series of buffer events will be sent, each representing a + supported buffer type. The "buffer_done" event is sent afterwards to + indicate that all supported buffer types have been enumerated. The client + will then be able to send a "copy" request. If the capture is successful, + the compositor will send a "flags" followed by a "ready" event. + + wl_shm buffers are always supported, ie. the "buffer" event is guaranteed to be sent. + + If the capture failed, the "failed" event is sent. This can happen anytime + before the "ready" event. + + Once either a "ready" or a "failed" event is received, the client should + destroy the frame. + + + + + Provides information about wl_shm buffer parameters that need to be + used for this frame. This event is sent once after the frame is created + if wl_shm buffers are supported. + + + + + + + + + + Copy the frame to the supplied buffer. The buffer must have the + correct size, see hyprland_toplevel_export_frame_v1.buffer and + hyprland_toplevel_export_frame_v1.linux_dmabuf. The buffer needs to have a + supported format. + + If the frame is successfully copied, a "flags" and a "ready" event is + sent. Otherwise, a "failed" event is sent. + + This event will wait for appropriate damage to be copied, unless the ignore_damage + arg is set to a non-zero value. + + + + + + + + This event is sent right before the ready event when ignore_damage was + not set. It may be generated multiple times for each copy + request. + + The arguments describe a box around an area that has changed since the + last copy request that was derived from the current screencopy manager + instance. + + The union of all regions received between the call to copy + and a ready event is the total damage since the prior ready event. + + + + + + + + + + + + + + + + + + + Provides flags about the frame. This event is sent once before the + "ready" event. + + + + + + + Called as soon as the frame is copied, indicating it is available + for reading. This event includes the time at which presentation happened + at. + + The timestamp is expressed as tv_sec_hi, tv_sec_lo, tv_nsec triples, + each component being an unsigned 32-bit value. Whole seconds are in + tv_sec which is a 64-bit value combined from tv_sec_hi and tv_sec_lo, + and the additional fractional part in tv_nsec as nanoseconds. Hence, + for valid timestamps tv_nsec must be in [0, 999999999]. The seconds part + may have an arbitrary offset at start. + + After receiving this event, the client should destroy the object. + + + + + + + + + This event indicates that the attempted frame copy has failed. + + After receiving this event, the client should destroy the object. + + + + + + Destroys the frame. This request can be sent at any time by the client. + + + + + + Provides information about linux-dmabuf buffer parameters that need to + be used for this frame. This event is sent once after the frame is + created if linux-dmabuf buffers are supported. + + + + + + + + + This event is sent once after all buffer events have been sent. + + The client should proceed to create a buffer of one of the supported + types, and send a "copy" request. + + + + diff --git a/src/wayland/shm.cpp b/src/wayland/shm.cpp new file mode 100644 index 0000000..aa7cf76 --- /dev/null +++ b/src/wayland/shm.cpp @@ -0,0 +1,75 @@ +#include "shm.hpp" +#include "../debug/log.hpp" +#include +#include +#include +#include +#include + +namespace wl { + + static int create_shm_file(off_t size) { + int ret, fd; + + // use memfd_create if available + fd = syscall(SYS_memfd_create, "hyprwat-shm-buffer", MFD_CLOEXEC | MFD_ALLOW_SEALING); + + if (fd < 0) { + debug::log(ERR, "Failed to create shm memfd"); + return -1; + } + + do { + ret = ftruncate(fd, size); + } while (ret < 0 && errno == EINTR); + + if (ret < 0) { + close(fd); + debug::log(ERR, "Failed to ftruncate shm memfd"); + return -1; + } + + return fd; + } + + ShmBuffer::ShmBuffer(wl_shm* shm, int width, int height, int stride, uint32_t format) + : width(width), height(height), stride(stride) { + size = stride * height; + + fd = create_shm_file(size); + if (fd < 0) { + throw std::runtime_error("Failed to create shm file"); + } + + data = mmap(nullptr, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); + if (data == MAP_FAILED) { + close(fd); + fd = -1; + throw std::runtime_error("Failed to mmap shm file"); + } + + wl_shm_pool* pool = wl_shm_create_pool(shm, fd, size); + if (!pool) { + munmap(data, size); + close(fd); + fd = -1; + throw std::runtime_error("Failed to create shm pool"); + } + + buffer = wl_shm_pool_create_buffer(pool, 0, width, height, stride, format); + wl_shm_pool_destroy(pool); + } + + ShmBuffer::~ShmBuffer() { + if (buffer) { + wl_buffer_destroy(buffer); + } + if (data != MAP_FAILED && data != nullptr) { + munmap(data, size); + } + if (fd >= 0) { + close(fd); + } + } + +} // namespace wl diff --git a/src/wayland/shm.hpp b/src/wayland/shm.hpp new file mode 100644 index 0000000..6593499 --- /dev/null +++ b/src/wayland/shm.hpp @@ -0,0 +1,30 @@ +#pragma once + +#include +#include +#include + +namespace wl { + + class ShmBuffer { + public: + ShmBuffer(wl_shm* shm, int width, int height, int stride, uint32_t format); + ~ShmBuffer(); + + wl_buffer* getBuffer() const { return buffer; } + void* getData() const { return data; } + int getWidth() const { return width; } + int getHeight() const { return height; } + int getStride() const { return stride; } + + private: + wl_buffer* buffer = nullptr; + void* data = nullptr; + int width = 0; + int height = 0; + int stride = 0; + size_t size = 0; + int fd = -1; + }; + +} // namespace wl