├── src ├── util.hpp ├── font │ ├── font.hpp │ └── font.cpp ├── choice.hpp ├── wayland │ ├── wayland.cpp │ ├── wayland.hpp │ ├── layer_surface.hpp │ ├── display.hpp │ ├── protocols │ │ ├── wlr-layer-shell-unstable-v1-client-protocol.c │ │ └── xdg-shell-client-protocol.c │ ├── input.hpp │ ├── layer_surface.cpp │ ├── display.cpp │ └── input.cpp ├── frames │ ├── input.hpp │ ├── text.hpp │ ├── text.cpp │ ├── images.hpp │ ├── selector.hpp │ ├── input.cpp │ ├── selector.cpp │ ├── custom.hpp │ └── images.cpp ├── vec.hpp ├── util.cpp ├── wallpaper │ ├── thumbnail.hpp │ ├── wallpaper.hpp │ ├── wallpaper.cpp │ └── thumbnail.cpp ├── flows │ ├── flow.hpp │ ├── wallpaper_flow.hpp │ ├── audio_flow.hpp │ ├── custom_flow.hpp │ ├── wifi_flow.hpp │ ├── wallpaper_flow.cpp │ ├── simple_flows.hpp │ ├── audio_flow.cpp │ ├── custom_flow.cpp │ ├── simple_flows.cpp │ └── wifi_flow.cpp ├── renderer │ ├── egl_context.hpp │ └── egl_context.cpp ├── input.hpp ├── net │ ├── network_manager.hpp │ └── network_manager.cpp ├── debug │ └── log.hpp ├── hyprland │ ├── ipc.hpp │ └── ipc.cpp ├── config.hpp ├── audio │ ├── audio.hpp │ └── audio.cpp ├── ui.hpp ├── input.cpp ├── main.cpp └── ui.cpp ├── examples ├── img │ ├── wifi.png │ ├── wallpapers.png │ └── powerprofiles.png ├── wifi.sh ├── hyprwat.conf ├── multiple_inputs.yaml ├── wallpaper.sh ├── powerprofiles.sh ├── powerprofiles.yaml └── custom_menu │ ├── main.yaml │ ├── display.yaml │ ├── power.yaml │ └── widgets.yaml ├── .gitmodules ├── .gitignore ├── TODO.md ├── PKGBUILD ├── .clang-format ├── LICENSE ├── CMakePresets.json ├── Makefile ├── .github └── workflows │ └── cmake-multi-platform.yml ├── CMakeLists.txt ├── README.md └── man └── hyprwat.6 /src/util.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | std::string generateUuid(); 5 | -------------------------------------------------------------------------------- /examples/img/wifi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zackb/hyprwat/HEAD/examples/img/wifi.png -------------------------------------------------------------------------------- /examples/img/wallpapers.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zackb/hyprwat/HEAD/examples/img/wallpapers.png -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "ext/imgui"] 2 | path = ext/imgui 3 | url = https://github.com/ocornut/imgui.git 4 | -------------------------------------------------------------------------------- /examples/img/powerprofiles.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zackb/hyprwat/HEAD/examples/img/powerprofiles.png -------------------------------------------------------------------------------- /src/font/font.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | namespace font { 6 | std::string defaultFontPath(); 7 | } 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | .cache 3 | imgui.ini 4 | compile_commands.json 5 | **/*.o 6 | .idea 7 | _CPack_Packages 8 | *.deb 9 | *.rpm 10 | *.tar.gz 11 | -------------------------------------------------------------------------------- /examples/wifi.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | nmcli -t -f active,ssid,signal dev wifi | awk -F: '!seen[$2]++ { printf "%s:%s (%s%%)%s\n", $2, $2, $3, ($1 == "yes" ? "*" : "") }' | ../build/debug/hyprwat 4 | -------------------------------------------------------------------------------- /src/choice.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | struct Choice { 6 | std::string id; 7 | std::string display; 8 | bool selected = false; 9 | int strength = -1; // convenience field for wifi signal strength 10 | }; 11 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | **TODO 2 | - [x] esc to exit 3 | - [x] click outside to exit isnt working 4 | - [x] pluggable UI "frame"s 5 | - [x] input (password, etc) 6 | - [x] dbus networkmanager 7 | - [x] pipewire 8 | - [x] packaging 9 | - [x] de-dupe wifi networks and choose the best one 10 | - [x] connection specific frame while "Connecting..." 11 | - [x] present error/success 12 | -------------------------------------------------------------------------------- /examples/hyprwat.conf: -------------------------------------------------------------------------------- 1 | [theme] 2 | font_color = #cdd6f4 3 | font_path = ~/.local/share/fonts/MesloLGSDZNerdFont-Regular.ttf 4 | font_size = 14.0 5 | background_color = #1e1e2e 6 | border_color = #33ccffee 7 | window_rounding = 10.0 8 | frame_rounding = 6.0 9 | background_blur = 0.95 10 | hover_color = #3366b3ff 11 | active_color = #3366b366 12 | wallpaper_width_ratio = 0.8 13 | -------------------------------------------------------------------------------- /src/wayland/wayland.cpp: -------------------------------------------------------------------------------- 1 | #include "wayland.hpp" 2 | #include 3 | 4 | namespace wl { 5 | Wayland::Wayland() : display_() { 6 | if (!display_.connect()) { 7 | throw std::runtime_error("Failed to connect to Wayland display"); 8 | } 9 | input_ = std::make_unique(display_.seat()); 10 | display_.roundtrip(); 11 | } 12 | 13 | Wayland::~Wayland() {} 14 | 15 | } // namespace wl 16 | -------------------------------------------------------------------------------- /src/wayland/wayland.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "src/wayland/display.hpp" 4 | #include "src/wayland/input.hpp" 5 | #include 6 | 7 | namespace wl { 8 | class Wayland { 9 | public: 10 | Wayland(); 11 | ~Wayland(); 12 | Display& display() { return display_; } 13 | InputHandler& input() { return *input_; } 14 | 15 | private: 16 | Display display_; 17 | std::unique_ptr input_; 18 | }; 19 | } // namespace wl 20 | -------------------------------------------------------------------------------- /examples/multiple_inputs.yaml: -------------------------------------------------------------------------------- 1 | # Description: Multiple inputs 2 | # Usage: hyprwat --custom multiple_inputs.yaml 3 | 4 | title: "Many Inputs" 5 | sections: 6 | - type: input 7 | id: "input_id" 8 | hint: "Input Example" 9 | password: false 10 | action: 11 | type: execute 12 | command: "echo Input value: {value}" 13 | 14 | - type: input 15 | id: "password_id" 16 | hint: "Password Example" 17 | password: true 18 | action: 19 | type: execute 20 | command: "echo Password value: {value}" 21 | -------------------------------------------------------------------------------- /examples/wallpaper.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # A script to set the wallpaper for Hyprland using hyprwat 3 | # selects a wallpaper using hyprwat and updates the hyprlock and hyprpaper 4 | # configuration files accordingly. 5 | 6 | wall=$(hyprwat --wallpaper ~/.local/share/wallpapers) 7 | 8 | if [ -n "$wall" ]; then 9 | sed -i "s|^\$image = .*|\$image = $wall|" ~/.config/hypr/hyprlock.conf 10 | sed -i "s|^preload = .*|preload = $wall|" ~/.config/hypr/hyprpaper.conf 11 | sed -i "s|^wallpaper =.*,.*|wallpaper = , $wall|" ~/.config/hypr/hyprpaper.conf 12 | fi 13 | 14 | -------------------------------------------------------------------------------- /src/frames/input.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "../ui.hpp" 4 | #include 5 | 6 | class TextInput : public Frame { 7 | public: 8 | TextInput(const std::string& hint = "", bool password = false) : hint(hint), password(password) {} 9 | virtual FrameResult render() override; 10 | virtual Vec2 getSize() override; 11 | 12 | private: 13 | static constexpr uint32_t bufSize = 128; 14 | char inputBuffer[bufSize] = {0}; 15 | ImVec2 lastSize = ImVec2(0, 0); 16 | std::string hint; 17 | bool password = false; 18 | bool shouldFocus = true; 19 | }; 20 | -------------------------------------------------------------------------------- /src/vec.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | struct Vec2 { 4 | float x; 5 | float y; 6 | bool operator!=(const Vec2& other) const { return x != other.x || y != other.y; } 7 | }; 8 | 9 | struct Vec3 { 10 | float x; 11 | float y; 12 | float z; 13 | bool operator!=(const Vec3& other) const { return x != other.x || y != other.y || z != other.z; } 14 | }; 15 | 16 | struct Vec4 { 17 | float x; 18 | float y; 19 | float z; 20 | float w; 21 | bool operator!=(const Vec4& other) const { return x != other.x || y != other.y || z != other.z || w != other.w; } 22 | }; 23 | -------------------------------------------------------------------------------- /src/util.cpp: -------------------------------------------------------------------------------- 1 | #include "util.hpp" 2 | #include 3 | #include 4 | 5 | std::string generateUuid() { 6 | std::random_device rd; 7 | std::mt19937 gen(rd()); 8 | std::uniform_int_distribution dist(0, 15); 9 | std::stringstream ss; 10 | std::string fmt = "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx"; 11 | for (char c : fmt) { 12 | if (c == 'x') 13 | ss << std::hex << dist(gen); 14 | else if (c == 'y') 15 | ss << std::hex << ((dist(gen) & 0x3) | 0x8); 16 | else 17 | ss << c; 18 | } 19 | return ss.str(); 20 | } 21 | -------------------------------------------------------------------------------- /src/wallpaper/thumbnail.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | class ThumbnailCache { 7 | public: 8 | ThumbnailCache(const std::string& cacheDir) : filepath_(cacheDir) { std::filesystem::create_directories(cacheDir); } 9 | std::string getOrCreateThumbnail(const std::string& imagePath, int width, int height); 10 | 11 | private: 12 | std::string filepath_; 13 | int hashFileKey(std::string&& path); 14 | bool resizeImage(std::string inPath, std::string outPath, int newW, int newH); 15 | uint64_t fileLastWriteTime(const std::filesystem::path& file); 16 | }; 17 | -------------------------------------------------------------------------------- /src/flows/flow.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "../ui.hpp" 4 | #include 5 | 6 | // Base class for multi-step flows 7 | class Flow { 8 | public: 9 | virtual ~Flow() = default; 10 | 11 | // Get the current frame to display 12 | virtual Frame* getCurrentFrame() = 0; 13 | 14 | // Handle the result from the current frame 15 | // Returns true if flow should continue, false if done 16 | virtual bool handleResult(const FrameResult& result) = 0; 17 | 18 | // Check if flow is complete 19 | virtual bool isDone() const = 0; 20 | 21 | // Get the final result of the flow 22 | virtual std::string getResult() const { return ""; } 23 | }; 24 | -------------------------------------------------------------------------------- /PKGBUILD: -------------------------------------------------------------------------------- 1 | pkgname=hyprwat-bin 2 | pkgver=0.9.2 3 | pkgrel=1 4 | pkgdesc="Hyprwat - A Wayland menu tool" 5 | arch=('x86_64') 6 | url="https://github.com/zackb/hyprwat" 7 | license=('MIT') 8 | depends=('wayland' 'mesa' 'fontconfig' 'libxkbcommon' 'sdbus-cpp' 'pipewire') 9 | provides=('hyprwat') 10 | conflicts=('hyprwat') 11 | source=("https://github.com/zackb/hyprwat/releases/download/$pkgver/hyprwat-$pkgver.tar.gz") 12 | sha256sums=('038aee9499e689e77dcda34092dd4cd6f7c1e4f9747d0f3c1c2d92479db32062') 13 | 14 | package() { 15 | cd "hyprwat-$pkgver" 16 | install -Dm755 bin/hyprwat "$pkgdir/usr/bin/hyprwat" 17 | install -Dm644 share/man/man6/hyprwat.6 "$pkgdir/usr/share/man/man6/hyprwat.6" 18 | } 19 | -------------------------------------------------------------------------------- /src/frames/text.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "../imgui/imgui.h" 4 | #include "../ui.hpp" 5 | #include 6 | 7 | class Text : public Frame { 8 | 9 | public: 10 | Text(const std::string& txt, ImVec4 col = ImVec4(1, 1, 1, 1)) : text(txt), color(col), lastSize(0, 0) {} 11 | 12 | void setText(const std::string& txt, ImVec4 col = ImVec4(1, 1, 1, 1)) { 13 | text = txt; 14 | color = col; 15 | } 16 | 17 | virtual FrameResult render() override; 18 | 19 | Vec2 getSize() override { return Vec2{lastSize.x, lastSize.y}; } 20 | void done() { done_ = true; } 21 | 22 | private: 23 | std::string text; 24 | bool done_ = false; 25 | ImVec4 color; 26 | ImVec2 lastSize; 27 | }; 28 | -------------------------------------------------------------------------------- /examples/powerprofiles.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Define profiles: id -> display name 4 | declare -A profiles=( 5 | ["performance"]="⚡ Performance" 6 | ["balanced"]="⚖ Balanced" 7 | ["power-saver"]="▽ Power Saver" 8 | ) 9 | 10 | # Get the current active profile 11 | current_profile=$(powerprofilesctl get) 12 | 13 | # Build hyprwat arguments 14 | args=() 15 | for id in "${!profiles[@]}"; do 16 | label="${profiles[$id]}" 17 | if [[ "$id" == "$current_profile" ]]; then 18 | args+=("${id}:${label}*") 19 | else 20 | args+=("${id}:${label}") 21 | fi 22 | done 23 | 24 | # Launch hyprwat and capture the selection 25 | selection=$(hyprwat "${args[@]}") 26 | 27 | # If user made a selection, apply it 28 | if [[ -n "$selection" ]]; then 29 | powerprofilesctl set "$selection" 30 | fi 31 | 32 | -------------------------------------------------------------------------------- /src/wallpaper/wallpaper.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "thumbnail.hpp" 4 | #include 5 | #include 6 | #include 7 | 8 | std::string getCacheDir(); 9 | 10 | struct Wallpaper { 11 | std::string path; 12 | std::string thumbnailPath; 13 | std::filesystem::file_time_type modified; 14 | }; 15 | 16 | class WallpaperManager { 17 | 18 | public: 19 | WallpaperManager(const std::string& wallpaperDir) : wallpaperDir(wallpaperDir), thumbnailCache(getCacheDir()) {} 20 | void loadWallpapers(); 21 | const std::vector& getWallpapers() const { return wallpapers; } 22 | 23 | private: 24 | std::string wallpaperDir; 25 | std::vector wallpapers; 26 | ThumbnailCache thumbnailCache; 27 | const std::set wallpaperExts = {".jpg", ".png", ".jpeg", ".bmp", ".gif"}; 28 | }; 29 | -------------------------------------------------------------------------------- /src/renderer/egl_context.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "src/vec.hpp" 4 | extern "C" { 5 | #include 6 | #include 7 | } 8 | 9 | #include 10 | 11 | namespace egl { 12 | class Context { 13 | public: 14 | Context(wl_display* display); 15 | ~Context(); 16 | 17 | bool createWindowSurface(wl_surface* surface, int width, int height); 18 | void makeCurrent(); 19 | void swapBuffers(); 20 | Vec2 getBufferSize() const; 21 | wl_egl_window* window() const { return egl_window; } 22 | 23 | private: 24 | wl_display* display; 25 | EGLDisplay egl_display; 26 | EGLContext egl_context; 27 | EGLSurface egl_surface; 28 | EGLConfig egl_config; 29 | wl_egl_window* egl_window = nullptr; 30 | }; 31 | } // namespace egl 32 | -------------------------------------------------------------------------------- /src/flows/wallpaper_flow.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "../frames/images.hpp" 4 | #include "../hyprland/ipc.hpp" 5 | #include "flow.hpp" 6 | 7 | class WallpaperFlow : public Flow { 8 | public: 9 | WallpaperFlow(hyprland::Control& hyprctl, 10 | const std::string& wallpaperDir, 11 | const int logicalWidth, 12 | const int logicalHeight); 13 | ~WallpaperFlow(); 14 | 15 | Frame* getCurrentFrame() override; 16 | bool handleResult(const FrameResult& result) override; 17 | bool isDone() const override; 18 | std::string getResult() const override; 19 | 20 | private: 21 | WallpaperManager wallpaperManager; 22 | hyprland::Control& hyprctl; 23 | std::unique_ptr imageList; 24 | std::string finalResult; 25 | std::thread loadingThread; 26 | std::atomic loadingStarted{false}; 27 | bool done = false; 28 | }; 29 | -------------------------------------------------------------------------------- /src/font/font.cpp: -------------------------------------------------------------------------------- 1 | #include "font.hpp" 2 | #include 3 | 4 | namespace font { 5 | std::string defaultFontPath() { 6 | FcInit(); 7 | FcPattern* pat = FcPatternCreate(); 8 | FcPatternAddString(pat, FC_FAMILY, (const FcChar8*)"sans"); 9 | FcConfigSubstitute(nullptr, pat, FcMatchPattern); 10 | FcDefaultSubstitute(pat); 11 | 12 | FcResult result; 13 | FcPattern* match = FcFontMatch(nullptr, pat, &result); 14 | std::string path; 15 | 16 | if (match) { 17 | FcChar8* file = nullptr; 18 | if (FcPatternGetString(match, FC_FILE, 0, &file) == FcResultMatch) { 19 | path = (const char*)file; 20 | } 21 | FcPatternDestroy(match); 22 | } 23 | 24 | FcPatternDestroy(pat); 25 | FcFini(); 26 | 27 | return path; 28 | } 29 | } // namespace font 30 | -------------------------------------------------------------------------------- /src/frames/text.cpp: -------------------------------------------------------------------------------- 1 | #include "text.hpp" 2 | 3 | FrameResult Text::render() { 4 | ImGuiStyle& style = ImGui::GetStyle(); 5 | ImVec2 windowPadding = style.WindowPadding; 6 | 7 | ImGui::SetNextWindowSize(ImGui::GetIO().DisplaySize); 8 | ImGui::SetNextWindowPos(ImVec2(0, 0)); 9 | ImGui::Begin("Status", 10 | nullptr, 11 | ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoSavedSettings | 12 | ImGuiWindowFlags_NoResize); 13 | 14 | ImGui::TextColored(color, "%s", text.c_str()); 15 | 16 | ImVec2 contentSize = ImGui::GetItemRectSize(); 17 | lastSize = ImVec2(contentSize.x + windowPadding.x * 2, contentSize.y + windowPadding.y * 2); 18 | 19 | bool escPressed = ImGui::IsKeyPressed(ImGuiKey_Escape); 20 | 21 | ImGui::End(); 22 | 23 | if (escPressed || done_) { 24 | return FrameResult::Cancel(); 25 | } 26 | 27 | return FrameResult::Continue(); 28 | } 29 | -------------------------------------------------------------------------------- /src/flows/audio_flow.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "../audio/audio.hpp" 4 | #include "../frames/selector.hpp" 5 | #include "flow.hpp" 6 | #include 7 | #include 8 | 9 | class AudioFlow : public Flow { 10 | public: 11 | AudioFlow(); 12 | ~AudioFlow() override = default; 13 | 14 | void start(); 15 | Frame* getCurrentFrame() override; 16 | bool handleResult(const FrameResult& result) override; 17 | bool isDone() const override; 18 | std::string getResult() const override; 19 | 20 | private: 21 | enum class State { 22 | SELECT_TYPE, // input or output 23 | SELECT_DEVICE, // specific device 24 | DONE 25 | }; 26 | 27 | AudioManagerClient audioManager; 28 | State currentState = State::SELECT_TYPE; 29 | bool done = false; 30 | 31 | std::unique_ptr typeSelector; 32 | std::unique_ptr deviceSelector; 33 | 34 | std::string selectedType; // "input" or "output" 35 | uint32_t selectedDeviceId = 0; 36 | 37 | void populateDeviceSelector(); 38 | }; 39 | -------------------------------------------------------------------------------- /.clang-format: -------------------------------------------------------------------------------- 1 | BasedOnStyle: LLVM 2 | 3 | # --- Indentation --- 4 | IndentWidth: 4 5 | TabWidth: 4 6 | UseTab: Never 7 | 8 | # --- Access Modifiers --- 9 | AccessModifierOffset: -4 10 | 11 | ConstructorInitializerAllOnOneLineOrOnePerLine: true 12 | 13 | # --- Braces --- 14 | BreakBeforeBraces: Attach # Opening brace stays on same line 15 | 16 | # --- Pointers & References --- 17 | PointerAlignment: Left 18 | DerivePointerAlignment: false 19 | 20 | # --- Line Length --- 21 | ColumnLimit: 120 22 | 23 | # --- Namespace formatting --- 24 | NamespaceIndentation: All 25 | 26 | # --- Include formatting --- 27 | SortIncludes: true 28 | IncludeBlocks: Preserve 29 | 30 | # --- Function arguments --- 31 | BinPackParameters: false 32 | BinPackArguments: false 33 | AllowAllParametersOfDeclarationOnNextLine: true 34 | 35 | # --- Constructor initializer formatting --- 36 | BreakConstructorInitializersBeforeComma: true 37 | 38 | # --- Lists --- 39 | Cpp11BracedListStyle: true 40 | SpacesInContainerLiterals: true 41 | 42 | # --- Misc --- 43 | AlignAfterOpenBracket: Align 44 | IndentWrappedFunctionNames: true 45 | 46 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Zack Bartel 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/flows/custom_flow.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "../frames/custom.hpp" 4 | #include "flow.hpp" 5 | #include 6 | #include 7 | #include 8 | 9 | class CustomFlow : public Flow { 10 | public: 11 | explicit CustomFlow(const std::string& rootConfigPath); 12 | ~CustomFlow() override = default; 13 | 14 | Frame* getCurrentFrame() override; 15 | bool handleResult(const FrameResult& result) override; 16 | bool isDone() const override; 17 | std::string getResult() const override; 18 | 19 | private: 20 | struct MenuLevel { 21 | std::unique_ptr frame; 22 | std::string configPath; 23 | std::string title; 24 | }; 25 | 26 | // Stack of menu levels (allows back navigation) 27 | std::stack menuStack; 28 | 29 | // Current state 30 | bool done = false; 31 | std::string finalResult; 32 | 33 | // Helper methods 34 | void pushMenu(const std::string& configPath); 35 | void popMenu(); 36 | bool isCustomAction(const std::string& result) const; 37 | std::string extractCustomPath(const std::string& result) const; 38 | }; 39 | -------------------------------------------------------------------------------- /CMakePresets.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 3, 3 | "cmakeMinimumRequired": { 4 | "major": 3, 5 | "minor": 19 6 | }, 7 | "configurePresets": [ 8 | { 9 | "name": "debug", 10 | "displayName": "Debug Build (Ninja)", 11 | "description": "Ninja Debug build inside build/debug", 12 | "generator": "Ninja", 13 | "binaryDir": "${sourceDir}/build/debug", 14 | "cacheVariables": { 15 | "CMAKE_BUILD_TYPE": "Debug", 16 | "CMAKE_EXPORT_COMPILE_COMMANDS": "ON" 17 | } 18 | }, 19 | { 20 | "name": "release", 21 | "displayName": "Release Build (Ninja)", 22 | "description": "Ninja Release build inside build/release", 23 | "generator": "Ninja", 24 | "binaryDir": "${sourceDir}/build/release", 25 | "cacheVariables": { 26 | "CMAKE_BUILD_TYPE": "Release", 27 | "CMAKE_EXPORT_COMPILE_COMMANDS": "ON" 28 | } 29 | } 30 | ], 31 | "buildPresets": [ 32 | { 33 | "name": "debug", 34 | "configurePreset": "debug" 35 | }, 36 | { 37 | "name": "release", 38 | "configurePreset": "release" 39 | } 40 | ] 41 | } 42 | 43 | -------------------------------------------------------------------------------- /examples/powerprofiles.yaml: -------------------------------------------------------------------------------- 1 | # Description: Menu for selecting power profiles using powerprofilesctl 2 | # Usage: hyprwat --custom powerprofiles.yaml 3 | 4 | title: "Power Settings" 5 | 6 | sections: 7 | - type: text 8 | content: "Select a power profile:" 9 | 10 | - type: separator 11 | 12 | - type: selectable_list 13 | items: 14 | - id: "performance" 15 | label: "⚡ Performance Mode" 16 | action: 17 | type: execute 18 | command: "powerprofilesctl set performance && notify-send 'Power Profile' 'Performance mode enabled'" 19 | close_on_success: true 20 | 21 | - id: "balanced" 22 | label: "⚖️ Balanced Mode" 23 | selected: true 24 | action: 25 | type: execute 26 | command: "powerprofilesctl set balanced && notify-send 'Power Profile' 'Balanced mode enabled'" 27 | close_on_success: true 28 | 29 | - id: "powersave" 30 | label: "🔋 Power Saver Mode" 31 | action: 32 | type: execute 33 | command: "powerprofilesctl set power-saver && notify-send 'Power Profile' 'Power saver mode enabled'" 34 | close_on_success: true 35 | -------------------------------------------------------------------------------- /src/frames/images.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "../ui.hpp" 4 | #include "../wallpaper/wallpaper.hpp" 5 | #include 6 | 7 | class ImageList : public Frame { 8 | public: 9 | ImageList(const int logicalWidth, const int logicalHeight); 10 | virtual FrameResult render() override; 11 | virtual Vec2 getSize() override; 12 | virtual void applyTheme(const Config& config) override; 13 | virtual bool shouldRepositionOnResize() const override { return false; } 14 | virtual bool shouldPositionAtCursor() const override { return false; } 15 | 16 | void addImages(const std::vector& wallpapers); 17 | 18 | private: 19 | int selectedIndex = 0; 20 | float scrollOffset = 0.0f; 21 | int logicalWidth; 22 | int logicalHeight; 23 | float imageRounding = 8; 24 | float widthRatio = 0.8f; 25 | std::vector textures; 26 | std::vector wallpapers; 27 | std::vector pendingWallpapers; 28 | std::mutex wallpapersMutex; 29 | ImVec4 hoverColor = ImVec4(0.2f, 0.4f, 0.7f, 1.0f); 30 | 31 | void processPendingWallpapers(); 32 | GLuint LoadTextureFromFile(const char* filename); 33 | void navigate(int direction); 34 | }; 35 | -------------------------------------------------------------------------------- /src/input.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "choice.hpp" 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | enum class InputMode { 11 | MENU, // default menu selection mode 12 | INPUT, // text input mode 13 | PASSWORD, // password input mode 14 | WIFI, // wifi selection + password mode 15 | AUDIO, // audio input/output selection mode 16 | CUSTOM, // custom menu from config file 17 | WALLPAPER // wallpaper selection mode 18 | }; 19 | 20 | struct ParseResult { 21 | InputMode mode = InputMode::MENU; 22 | std::vector choices; 23 | std::string hint; // For INPUT mode only 24 | std::string configPath; // For CUSTOM mode only 25 | std::string wallpaperDir; // For WALLPAPER mode only 26 | }; 27 | 28 | class Input { 29 | 30 | public: 31 | using Callback = std::function; 32 | static ParseResult parseArgv(int argc, const char* argv[]); 33 | static void parseStdin(Callback callback); 34 | static std::mutex mutex; 35 | 36 | static std::filesystem::path expandPath(const std::string& path); 37 | 38 | private: 39 | static Choice parseLine(std::string line); 40 | }; 41 | -------------------------------------------------------------------------------- /examples/custom_menu/main.yaml: -------------------------------------------------------------------------------- 1 | title: "Main Menu" 2 | 3 | sections: 4 | - type: text 5 | content: "System Control Center" 6 | style: bold 7 | 8 | - type: separator 9 | 10 | # submenu navigation 11 | - type: button 12 | label: "⚡ Power Settings" 13 | action: 14 | type: submenu 15 | path: "power.yaml" # relative path 16 | 17 | - type: button 18 | label: "🖥️ Display Settings" 19 | action: 20 | type: submenu 21 | path: "display.yaml" 22 | 23 | - type: button 24 | label: "🔊 Audio Settings" 25 | action: 26 | type: submenu 27 | path: "audio.yaml" 28 | 29 | - type: button 30 | label: "Widgets" 31 | action: 32 | type: submenu 33 | path: "widgets.yaml" 34 | 35 | - type: button 36 | label: "📡 Network Settings" 37 | action: 38 | type: submenu 39 | path: "network.yaml" 40 | 41 | - type: separator 42 | 43 | # direct actions at root level 44 | - type: button 45 | label: "🔒 Lock Screen" 46 | action: 47 | type: execute 48 | command: "hyprlock" 49 | close_on_success: true 50 | 51 | - type: button 52 | label: "🚪 Exit" 53 | action: 54 | type: cancel 55 | 56 | shortcuts: 57 | - key: "Escape" 58 | action: 59 | type: cancel 60 | -------------------------------------------------------------------------------- /src/net/network_manager.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | struct WifiNetwork { 8 | std::string ssid; 9 | int strength; // 0-100 10 | }; 11 | 12 | enum ConnectionState { 13 | UNKNOWN, 14 | DISCONNECTED, 15 | ACTIVATING, 16 | CONFIGURING, 17 | AUTHENTICATING, 18 | ACTIVATED, 19 | FAILED, 20 | }; 21 | 22 | class NetworkManagerClient { 23 | 24 | public: 25 | NetworkManagerClient(); 26 | std::vector listWifiNetworks(); 27 | void scanWifiNetworks(std::function callback, int timeoutSeconds = 5); 28 | bool connectToNetwork(const std::string& ssid, 29 | const std::string& password, 30 | std::function statusCallback = nullptr); 31 | void stopScanning() { stopScanRequest = true; } 32 | 33 | private: 34 | std::unique_ptr connection; 35 | std::unique_ptr proxy; 36 | std::unique_ptr connectionProxy; 37 | 38 | std::vector getWifiDevices(); 39 | std::vector getAccessPoints(const sdbus::ObjectPath& device); 40 | bool stopScanRequest = false; 41 | }; 42 | -------------------------------------------------------------------------------- /src/debug/log.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | enum LogLevel { TRACE = 0, DEBUG, INFO, WARN, ERR, CRIT }; 6 | 7 | namespace debug { 8 | inline bool verbose = false; 9 | inline bool quiet = false; 10 | template void log(LogLevel level, const std::string& fmt, Args&&... args) { 11 | 12 | if (quiet && level < ERR) { 13 | return; 14 | } 15 | if (!verbose && level == TRACE) { 16 | return; 17 | } 18 | std::cerr << '['; 19 | 20 | switch (level) { 21 | case TRACE: 22 | std::cerr << "TRACE"; 23 | break; 24 | case DEBUG: 25 | std::cerr << "DEBUG"; 26 | break; 27 | case INFO: 28 | std::cerr << "INFO"; 29 | break; 30 | case WARN: 31 | std::cerr << "WARN"; 32 | break; 33 | case ERR: 34 | std::cerr << "ERR"; 35 | break; 36 | case CRIT: 37 | std::cerr << "CRIT"; 38 | break; 39 | default: 40 | break; 41 | } 42 | 43 | std::cerr << "] "; 44 | 45 | std::cerr << std::vformat(fmt, std::make_format_args(args...)) << std::endl; 46 | } 47 | } // namespace debug 48 | -------------------------------------------------------------------------------- /examples/custom_menu/display.yaml: -------------------------------------------------------------------------------- 1 | title: "Display Settings" 2 | 3 | sections: 4 | - type: text 5 | content: "← Back (ESC)" 6 | 7 | - type: separator 8 | 9 | # nested submenu 10 | - type: button 11 | label: "🖥️ Monitor Configuration" 12 | action: 13 | type: submenu 14 | path: "display-monitors.yaml" 15 | 16 | - type: separator 17 | 18 | - type: slider 19 | id: "brightness" 20 | label: "Screen Brightness" 21 | min: 1 22 | max: 100 23 | default: 50 24 | action: 25 | type: execute 26 | command: "brightnessctl set {value}%" 27 | trigger: on_release 28 | 29 | - type: checkbox 30 | id: "night_light" 31 | label: "Night Light" 32 | default: false 33 | action: 34 | type: execute 35 | command: "hyprshade toggle blue-light-filter" 36 | 37 | - type: combo 38 | id: "refresh_rate" 39 | label: "Refresh Rate" 40 | items: 41 | - "60 Hz" 42 | - "120 Hz" 43 | - "144 Hz" 44 | - "165 Hz" 45 | default: 0 46 | action: 47 | type: execute 48 | command: "hyprctl keyword monitor ,preferred,auto,{index}" 49 | 50 | - type: separator 51 | 52 | - type: button 53 | label: "← Back" 54 | action: 55 | type: back 56 | 57 | shortcuts: 58 | - key: "Escape" 59 | action: 60 | type: back 61 | -------------------------------------------------------------------------------- /src/flows/wifi_flow.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "../frames/input.hpp" 4 | #include "../frames/selector.hpp" 5 | #include "../frames/text.hpp" 6 | #include "../net/network_manager.hpp" 7 | #include "flow.hpp" 8 | #include 9 | #include 10 | 11 | class WifiFlow : public Flow { 12 | public: 13 | WifiFlow(); 14 | ~WifiFlow(); 15 | 16 | void start(); 17 | 18 | Frame* getCurrentFrame() override; 19 | bool handleResult(const FrameResult& result) override; 20 | bool isDone() const override; 21 | std::string getResult() const override; 22 | 23 | // Accessors for individual values 24 | std::string getSelectedNetwork() const; 25 | std::string getPassword() const; 26 | 27 | // network availability callback 28 | void networkDiscovered(const WifiNetwork& network); 29 | 30 | private: 31 | enum class State { SELECT_NETWORK, ENTER_PASSWORD, CONNECTIING }; 32 | 33 | State currentState = State::SELECT_NETWORK; 34 | 35 | NetworkManagerClient nm; 36 | std::thread scanThread; 37 | 38 | std::unique_ptr networkSelector; 39 | std::unique_ptr passwordInput; 40 | std::unique_ptr connectingFrame; 41 | 42 | std::string selectedNetwork; 43 | std::string password; 44 | bool done = false; 45 | int scanTimeout = 5; // seconds 46 | }; 47 | -------------------------------------------------------------------------------- /src/hyprland/ipc.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "../vec.hpp" 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | namespace hyprland { 10 | class Control { 11 | public: 12 | explicit Control(); 13 | explicit Control(const std::string& socketPath); 14 | ~Control(); 15 | 16 | // Send command, return raw response 17 | std::string send(const std::string& command); 18 | 19 | // these are in fractional scale pixels 20 | Vec2 cursorPos(); 21 | float scale(); 22 | 23 | void setWallpaper(const std::string& path); 24 | 25 | private: 26 | std::string socketPath; 27 | }; 28 | 29 | class Events { 30 | public: 31 | using EventCallback = std::function; 32 | 33 | explicit Events(); 34 | explicit Events(const std::string& socketPath); 35 | ~Events(); 36 | 37 | // Start listening on a background thread 38 | void start(EventCallback cb); 39 | 40 | // Stop listening 41 | void stop(); 42 | 43 | private: 44 | void run(EventCallback cb); 45 | 46 | std::string socketPath; 47 | std::thread thread; 48 | std::atomic running{false}; 49 | std::mutex mtx; 50 | int fd{-1}; 51 | }; 52 | } // namespace hyprland 53 | -------------------------------------------------------------------------------- /examples/custom_menu/power.yaml: -------------------------------------------------------------------------------- 1 | title: "Power Settings" 2 | 3 | sections: 4 | - type: text 5 | content: "← Back (ESC)" 6 | 7 | - type: separator 8 | 9 | - type: selectable_list 10 | items: 11 | - id: "performance" 12 | label: "⚡ Performance Mode" 13 | action: 14 | type: execute 15 | command: "powerprofilesctl set performance && notify-send 'Power Profile' 'Performance mode enabled'" 16 | close_on_success: true 17 | 18 | - id: "balanced" 19 | label: "⚖️ Balanced Mode" 20 | selected: true 21 | action: 22 | type: execute 23 | command: "powerprofilesctl set balanced && notify-send 'Power Profile' 'Balanced mode enabled'" 24 | close_on_success: true 25 | 26 | - id: "powersave" 27 | label: "🔋 Power Saver Mode" 28 | action: 29 | type: execute 30 | command: "powerprofilesctl set power-saver && notify-send 'Power Profile' 'Power saver mode enabled'" 31 | close_on_success: true 32 | 33 | - type: separator 34 | 35 | - type: slider 36 | id: "cpu_boost" 37 | label: "CPU Boost" 38 | min: 0 39 | max: 100 40 | default: 50 41 | action: 42 | type: execute 43 | command: "echo 'echo {value} | sudo tee /sys/devices/system/cpu/cpufreq/boost'" 44 | trigger: on_release 45 | 46 | - type: separator 47 | 48 | - type: button 49 | label: "← Back" 50 | action: 51 | type: back 52 | 53 | shortcuts: 54 | - key: "Escape" 55 | action: 56 | type: back # ESC goes back instead of canceling 57 | 58 | -------------------------------------------------------------------------------- /src/frames/selector.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "../choice.hpp" 4 | #include "imgui.h" 5 | #include "src/ui.hpp" 6 | #include "src/vec.hpp" 7 | #include 8 | 9 | class Selector : public Frame { 10 | 11 | public: 12 | /* 13 | void add(Choice& choice) { choices.emplace_back(choice); } 14 | void add(Choice&& choice) { choices.emplace_back(choice); } 15 | void add(const Choice& choice) { choices.emplace_back(choice); } 16 | void add(const Choice&& choice) { choices.emplace_back(choice); } 17 | */ 18 | template void add(T&& choice) { 19 | choices.emplace_back(std::forward(choice)); 20 | if (choice.selected) { 21 | selected = choices.size() - 1; 22 | } 23 | } 24 | 25 | // finds a pointer to choice by its id 26 | // choice can by mutated 27 | Choice* findChoiceById(const std::string& id) { 28 | for (auto& c : choices) 29 | if (c.id == id) 30 | return &c; 31 | return nullptr; 32 | } 33 | 34 | void setSelected(int index) { selected = index; } 35 | 36 | bool RoundedSelectableFullWidth(const char* label, bool selected, float rounding = 6.0f); 37 | 38 | virtual FrameResult render() override; 39 | virtual Vec2 getSize() override { return Vec2{lastSize.x, lastSize.y}; } 40 | virtual void applyTheme(const Config& config) override; 41 | 42 | private: 43 | int selected = -1; 44 | std::vector choices; 45 | ImVec4 activeColor = ImVec4(0.2f, 0.4f, 0.7f, 1.0f); 46 | ImVec4 hoverColor = ImVec4(0.2f, 0.4f, 0.7f, 0.4f); 47 | ImVec2 lastSize = ImVec2(0, 0); 48 | }; 49 | -------------------------------------------------------------------------------- /examples/custom_menu/widgets.yaml: -------------------------------------------------------------------------------- 1 | title: "UI Controls Examples" 2 | sections: 3 | - type: input 4 | id: "input_id" 5 | hint: "Input Example" 6 | password: false 7 | action: 8 | type: execute 9 | command: "echo Input value: {value}" 10 | 11 | - type: input 12 | id: "password_id" 13 | hint: "Password Example" 14 | password: true 15 | action: 16 | type: execute 17 | command: "echo Password value: {value}" 18 | 19 | - type: slider 20 | id: "slider_id" 21 | label: "Slider Example" 22 | min: 0 23 | max: 100 24 | default: 50 25 | action: 26 | type: execute 27 | command: "echo Slider value: {value}" 28 | trigger: on_release 29 | - type: checkbox 30 | id: "checkbox_id" 31 | label: "Checkbox Example" 32 | default: true 33 | action: 34 | type: execute 35 | command: "echo Checkbox is {value}" 36 | - type: combo 37 | id: "combo_id" 38 | label: "Combo Box Example" 39 | items: 40 | - "Option 1" 41 | - "Option 2" 42 | - "Option 3" 43 | default: 1 44 | action: 45 | type: execute 46 | command: "echo Selected option index: {index}" 47 | - type: color_picker 48 | id: "color_picker_id" 49 | label: "Color Picker Example" 50 | default: "#ff0000" 51 | action: 52 | type: execute 53 | command: "echo Selected color: {value}" 54 | trigger: on_release 55 | - type: separator 56 | - type: button 57 | label: "← Back" 58 | action: 59 | type: back 60 | shortcuts: 61 | - key: "Escape" 62 | action: 63 | type: back # ESC goes back instead of canceling 64 | -------------------------------------------------------------------------------- /src/flows/wallpaper_flow.cpp: -------------------------------------------------------------------------------- 1 | #include "wallpaper_flow.hpp" 2 | 3 | // wallpaper selection flow 4 | // logicalWidth and logicalHeight are the size of the display in logical pixels 5 | WallpaperFlow::WallpaperFlow(hyprland::Control& hyprctl, 6 | const std::string& dir, 7 | const int logicalWidth, 8 | const int logicalHeight) 9 | : wallpaperManager(dir), hyprctl(hyprctl) { 10 | 11 | imageList = std::make_unique(logicalWidth, logicalHeight); 12 | } 13 | 14 | WallpaperFlow::~WallpaperFlow() = default; 15 | 16 | Frame* WallpaperFlow::getCurrentFrame() { 17 | if (!loadingStarted) { 18 | // generate thumbnails in background 19 | loadingThread = std::thread([this]() { 20 | wallpaperManager.loadWallpapers(); 21 | const auto& wallpapers = wallpaperManager.getWallpapers(); 22 | imageList->addImages(wallpapers); 23 | }); 24 | loadingStarted = true; 25 | } 26 | 27 | return imageList.get(); 28 | } 29 | 30 | bool WallpaperFlow::handleResult(const FrameResult& result) { 31 | if (result.action == FrameResult::Action::SUBMIT) { 32 | finalResult = result.value; 33 | done = true; 34 | hyprctl.setWallpaper(finalResult); 35 | } else if (result.action == FrameResult::Action::CANCEL) { 36 | done = true; 37 | } 38 | 39 | if (done) { 40 | if (loadingThread.joinable()) { 41 | loadingThread.join(); 42 | } 43 | return false; 44 | } 45 | 46 | return true; 47 | } 48 | 49 | bool WallpaperFlow::isDone() const { return done; } 50 | 51 | std::string WallpaperFlow::getResult() const { return finalResult; } 52 | -------------------------------------------------------------------------------- /src/wallpaper/wallpaper.cpp: -------------------------------------------------------------------------------- 1 | #include "wallpaper.hpp" 2 | #include "../debug/log.hpp" 3 | #include 4 | #include 5 | 6 | namespace fs = std::filesystem; 7 | 8 | std::string getCacheDir() { 9 | if (const char* xdgCache = std::getenv("XDG_CACHE_HOME")) { 10 | return std::string(xdgCache) + "/hyprwat/wallpapers/"; 11 | } 12 | 13 | const char* home = std::getenv("HOME"); 14 | if (!home) { 15 | throw std::runtime_error("Neither XDG_CACHE_HOME nor HOME is set"); 16 | } 17 | 18 | return std::string(home) + "/.cache/hyprwat/wallpapers/"; 19 | } 20 | 21 | void WallpaperManager::loadWallpapers() { 22 | wallpapers.clear(); 23 | try { 24 | for (const auto& entry : fs::recursive_directory_iterator(wallpaperDir)) { 25 | if (fs::is_regular_file(entry.path())) { 26 | // only add images 27 | if (wallpaperExts.contains(entry.path().extension().string())) { 28 | debug::log(DEBUG, "Adding wallpaper: {}", entry.path().string()); 29 | wallpapers.push_back({entry.path().string(), 30 | thumbnailCache.getOrCreateThumbnail(entry.path().string(), 400, 225), 31 | fs::last_write_time(entry.path())}); 32 | } 33 | } else if (fs::is_directory(entry.path())) { 34 | debug::log(DEBUG, "Found directory: {}", entry.path().string()); 35 | } 36 | } 37 | std::sort( 38 | wallpapers.begin(), wallpapers.end(), [](auto const& a, auto const& b) { return a.modified > b.modified; }); 39 | } catch (const fs::filesystem_error& e) { 40 | debug::log(ERR, "Filesystem error while loading wallpapers: {}", e.what()); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/wayland/layer_surface.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "../renderer/egl_context.hpp" 4 | extern "C" { 5 | #include "protocols/wlr-layer-shell-unstable-v1-client-protocol.h" 6 | #include 7 | } 8 | 9 | namespace wl { 10 | 11 | // manages a wl_layer_surface for a Wayland compositor using the wlr-layer-shell protocol 12 | class LayerSurface { 13 | public: 14 | LayerSurface(wl_compositor* compositor, zwlr_layer_shell_v1* shell); 15 | ~LayerSurface(); 16 | 17 | void create(int x, int y, int width, int height); 18 | bool isConfigured() const { return configured; } 19 | wl_surface* surface() const { return surface_; } 20 | void resize(int newWidth, int newHeight, egl::Context& egl); 21 | 22 | void requestExit() { shouldExit_ = true; } 23 | bool shouldExit() const { return shouldExit_; } 24 | 25 | // logical size 26 | int width() const { return width_; } 27 | int height() const { return height_; } 28 | 29 | void bufferScale(int32_t scale); 30 | int32_t bufferScale() const { return scale_; } 31 | void reposition(int x, int y, int viewportWidth, int viewportHeight, int windowWidth, int windowHeight); 32 | 33 | private: 34 | wl_compositor* compositor; 35 | zwlr_layer_shell_v1* layerShell; 36 | wl_surface* surface_ = nullptr; 37 | zwlr_layer_surface_v1* layerSurface = nullptr; 38 | bool configured = false; 39 | bool shouldExit_ = false; 40 | int width_ = 0; 41 | int height_ = 0; 42 | int32_t scale_ = 1; 43 | 44 | static void configureHandler( 45 | void* data, zwlr_layer_surface_v1* layerSurface, uint32_t serial, uint32_t width, uint32_t height); 46 | static void closedHandler(void* data, zwlr_layer_surface_v1* layerSurface); 47 | }; 48 | } // namespace wl 49 | -------------------------------------------------------------------------------- /src/config.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | #include "INIReader.h" 7 | #include "debug/log.hpp" 8 | #include "input.hpp" 9 | #include 10 | #include 11 | #include 12 | 13 | class Config { 14 | public: 15 | explicit Config(const std::string& path) : reader(Input::expandPath(path)) { 16 | if (reader.ParseError() != 0) { 17 | // file not found or parse error 18 | debug::log(WARN, "Config file '{}' not found or invalid. Using defaults.", path); 19 | } 20 | } 21 | 22 | std::string getString(const std::string& section, const std::string& name, const std::string& def = "") const { 23 | return Input::expandPath(reader.Get(section, name, def)).string(); 24 | } 25 | 26 | float getFloat(const std::string& section, const std::string& name, float def = 0.0f) const { 27 | return static_cast(reader.GetReal(section, name, def)); 28 | } 29 | 30 | ImVec4 getColor(const std::string& section, const std::string& name, const std::string& def = "#000000FF") const { 31 | std::string val = reader.Get(section, name, def); 32 | return parseColor(val); 33 | } 34 | 35 | private: 36 | INIReader reader; 37 | 38 | static ImVec4 parseColor(const std::string& hex) { 39 | unsigned int r = 0, g = 0, b = 0, a = 255; 40 | std::string s = hex; 41 | if (!s.empty() && s[0] == '#') 42 | s.erase(0, 1); 43 | 44 | if (s.size() == 6) { 45 | sscanf(s.c_str(), "%02x%02x%02x", &r, &g, &b); 46 | } else if (s.size() == 8) { 47 | sscanf(s.c_str(), "%02x%02x%02x%02x", &r, &g, &b, &a); 48 | } else { 49 | throw std::runtime_error("Invalid color format: " + hex); 50 | } 51 | 52 | return ImVec4(r / 255.0f, g / 255.0f, b / 255.0f, a / 255.0f); 53 | } 54 | }; 55 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | default: debug 2 | 3 | debug: 4 | cmake --preset debug 5 | cmake --build --preset debug 6 | ln -sf build/debug/compile_commands.json 7 | 8 | release: 9 | cmake --preset release 10 | cmake --build --preset release 11 | ln -sf build/release/compile_commands.json 12 | 13 | install: release 14 | cmake --install build/release 15 | 16 | package: release 17 | cpack --config build/release/CPackConfig.cmake 18 | 19 | package-pacman: release 20 | makepkg -s 21 | # then in ../hypwat-bin: makepkg --printsrcinfo > .SRCINFO 22 | 23 | run: debug 24 | ./build/debug/hyprwat foo:FooZZZZZZZZZZZZZZZZZZZZZZZZZZZZ bar:Bar* zbz:'Zack Bartel' 25 | 26 | run2: debug 27 | ./build/debug/hyprwat foo:Foo bar:Bar zbz:'Baz' 28 | run3: debug 29 | echo -e "foo:Foo\nbar:Bar\nbaz:Baz\nqux:Qux\nquux:Quux\ncorge:Corge\ngrault:Grault\ngarply:Garply*\nwaldo:Waldo\nfred:Fred\nplugh:Plugh\nxyzzy:Xyzzy\nthud:Thud" | ./build/debug/hyprwat 30 | 31 | run-input: debug 32 | ./build/debug/hyprwat --input "What is your name?" 33 | 34 | run-password: debug 35 | ./build/debug/hyprwat --password Passphrase 36 | 37 | run-wifi: debug 38 | ./build/debug/hyprwat --wifi 39 | 40 | run-audio: debug 41 | ./build/debug/hyprwat --audio 42 | 43 | run-custom: debug 44 | ./build/debug/hyprwat --custom examples/custom_menu/main.yaml 45 | 46 | run-wallpaper: debug 47 | ./build/debug/hyprwat --wallpaper ~/.local/share/wallpapers 48 | 49 | reset-wifi: 50 | sudo nmcli device disconnect wlan0 51 | sudo nmcli device connect wlan0 52 | 53 | .PHONY: fmt 54 | fmt: 55 | @echo "Formatting code with clang-format..." 56 | @find ./src ./include \( -name "*.cpp" -o -name "*.hpp" -o -name "*.cc" -o -name "*.c" -o -name "*.h" \) ! -name "json.hpp" -print0 | \ 57 | xargs -0 -n 1 clang-format -i 58 | @echo "Done." 59 | 60 | clean: 61 | rm -rf build 62 | rm -rf _CPack_Packages 63 | rm -rf pkg 64 | rm -f hyprwat*.deb 65 | rm -f hyprwat*.rpm 66 | rm -f hyprwat*.tar.gz 67 | rm -f hyprwat*.zst 68 | -------------------------------------------------------------------------------- /src/frames/input.cpp: -------------------------------------------------------------------------------- 1 | #include "input.hpp" 2 | 3 | FrameResult TextInput::render() { 4 | // Calculate desired size based on content 5 | ImGuiStyle& style = ImGui::GetStyle(); 6 | ImVec2 framePadding = style.FramePadding; 7 | ImVec2 windowPadding = style.WindowPadding; 8 | 9 | // Calculate input box size 10 | float inputWidth = 300.0f; // default width for input 11 | 12 | // calculate actual size after rendering 13 | lastSize = ImVec2(inputWidth + windowPadding.x * 2, 50); 14 | 15 | // Set the window to fill the entire display 16 | ImGui::SetNextWindowSize(ImGui::GetIO().DisplaySize); 17 | ImGui::SetNextWindowPos(ImVec2(0, 0)); 18 | 19 | ImGui::Begin("Select", 20 | nullptr, 21 | ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoSavedSettings | 22 | ImGuiWindowFlags_NoResize); 23 | 24 | // Set the input text width 25 | ImGui::SetNextItemWidth(inputWidth); 26 | 27 | // if (ImGui::IsWindowAppearing()) 28 | if (shouldFocus) { 29 | ImGui::SetKeyboardFocusHere(); 30 | shouldFocus = false; 31 | } 32 | 33 | ImGuiInputTextFlags flags = ImGuiInputTextFlags_EnterReturnsTrue; 34 | if (password) { 35 | flags |= ImGuiInputTextFlags_Password; 36 | } 37 | bool enterPressed = ImGui::InputTextWithHint("##pass", hint.c_str(), inputBuffer, bufSize, flags); 38 | 39 | bool escPressed = ImGui::IsKeyPressed(ImGuiKey_Escape); 40 | 41 | // actual content size after rendering 42 | ImVec2 contentSize = ImGui::GetItemRectSize(); 43 | lastSize = ImVec2(inputWidth + windowPadding.x * 2, contentSize.y + windowPadding.y * 2); 44 | 45 | ImGui::End(); 46 | 47 | if (enterPressed) { 48 | return FrameResult::Submit(std::string(inputBuffer)); 49 | } 50 | 51 | if (escPressed) { 52 | return FrameResult::Cancel(); 53 | } 54 | 55 | return FrameResult::Continue(); 56 | } 57 | 58 | Vec2 TextInput::getSize() { return Vec2{lastSize.x, lastSize.y}; } 59 | -------------------------------------------------------------------------------- /src/audio/audio.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | struct AudioDevice { 12 | std::string name; 13 | std::string description; 14 | uint32_t id; 15 | bool isDefault; 16 | }; 17 | 18 | class AudioManagerClient { 19 | public: 20 | AudioManagerClient(); 21 | ~AudioManagerClient(); 22 | 23 | // List audio devices 24 | std::vector listInputDevices(); 25 | std::vector listOutputDevices(); 26 | 27 | // Set default devices 28 | bool setDefaultInput(uint32_t deviceId); 29 | bool setDefaultOutput(uint32_t deviceId); 30 | 31 | private: 32 | struct pw_thread_loop* loop; 33 | struct pw_context* context; 34 | struct pw_core* core; 35 | struct pw_registry* registry; 36 | struct spa_hook registry_listener; 37 | 38 | struct pw_proxy* metadata_proxy; 39 | struct pw_metadata* metadata; 40 | struct spa_hook metadata_listener; 41 | 42 | std::map sinks; 43 | std::map sources; 44 | uint32_t default_sink_id; 45 | uint32_t default_source_id; 46 | 47 | bool initialized; 48 | 49 | void updateDevices(); 50 | std::vector getDevices(const std::map& deviceMap); 51 | bool setDefault(uint32_t deviceId, const std::string& key); 52 | 53 | public: 54 | // Static callbacks need to be public for C-style callback registration 55 | static void registryEventGlobal(void* data, 56 | uint32_t id, 57 | uint32_t permissions, 58 | const char* type, 59 | uint32_t version, 60 | const struct spa_dict* props); 61 | static void registryEventGlobalRemove(void* data, uint32_t id); 62 | static int metadataProperty(void* data, uint32_t id, const char* key, const char* type, const char* value); 63 | }; 64 | -------------------------------------------------------------------------------- /src/flows/simple_flows.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "flow.hpp" 4 | #include 5 | #include 6 | #include 7 | 8 | class Selector; 9 | class TextInput; 10 | struct Choice; 11 | 12 | // flow that shows a menu and exits with the selected item 13 | class MenuFlow : public Flow { 14 | public: 15 | MenuFlow(); 16 | MenuFlow(const std::vector& choices); 17 | ~MenuFlow(); 18 | 19 | void addChoice(const Choice& choice); 20 | Frame* getCurrentFrame() override; 21 | bool handleResult(const FrameResult& result) override; 22 | bool isDone() const override; 23 | std::string getResult() const override; 24 | 25 | private: 26 | std::unique_ptr selector; 27 | bool done = false; 28 | std::string finalResult; 29 | }; 30 | 31 | // flow that shows a text input and exits with the entered text 32 | class InputFlow : public Flow { 33 | public: 34 | InputFlow(const std::string& hint, bool password = false); 35 | ~InputFlow(); 36 | 37 | Frame* getCurrentFrame() override; 38 | bool handleResult(const FrameResult& result) override; 39 | bool isDone() const override; 40 | std::string getResult() const override; 41 | 42 | private: 43 | std::unique_ptr input; 44 | bool done = false; 45 | std::string finalResult; 46 | }; 47 | 48 | // flow that shows a menu, then prompts for input based on selection 49 | class MenuThenInputFlow : public Flow { 50 | public: 51 | MenuThenInputFlow(const std::vector& choices, const std::string& inputHintPrefix = "Enter value for"); 52 | 53 | Frame* getCurrentFrame() override; 54 | bool handleResult(const FrameResult& result) override; 55 | bool isDone() const override; 56 | std::string getResult() const override; 57 | 58 | std::string getSelectedItem() const; 59 | std::string getInputValue() const; 60 | 61 | private: 62 | enum class State { SELECTING, INPUT }; 63 | State currentState = State::SELECTING; 64 | 65 | std::unique_ptr selector; 66 | std::unique_ptr input; 67 | 68 | std::string inputHintPrefix; 69 | std::string selectedItem; 70 | std::string inputValue; 71 | bool done = false; 72 | }; 73 | -------------------------------------------------------------------------------- /src/ui.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "config.hpp" 4 | #include "vec.hpp" 5 | #include "wayland/layer_surface.hpp" 6 | #include "wayland/wayland.hpp" 7 | 8 | // result returned by a frame after user interaction 9 | struct FrameResult { 10 | enum class Action { 11 | CONTINUE, // Keep showing this frame 12 | SUBMIT, // User submitted (Enter pressed, item selected) 13 | CANCEL // User cancelled (ESC pressed) 14 | }; 15 | 16 | Action action = Action::CONTINUE; 17 | std::string value; // The submitted value (selected id, input text, password, etc.) 18 | 19 | static FrameResult Continue() { return {Action::CONTINUE, ""}; } 20 | static FrameResult Submit(const std::string& val) { return {Action::SUBMIT, val}; } 21 | static FrameResult Cancel() { return {Action::CANCEL, ""}; } 22 | }; 23 | 24 | class Frame { 25 | public: 26 | virtual ~Frame() = default; 27 | 28 | // render the frame and return the result of user interaction 29 | // returns FrameResult indicating continue, submit, or cancel 30 | virtual FrameResult render() = 0; 31 | 32 | virtual Vec2 getSize() = 0; 33 | virtual void applyTheme(const Config& config) {}; 34 | 35 | // whether the window should reposition based on cursor when resizing 36 | // default true for menu-like behavior, override to false for centered/static windows 37 | virtual bool shouldRepositionOnResize() const { return true; } 38 | 39 | // whether the window should use cursor position for initial placement 40 | // default true for menu-like behavior, override to false for centered windows 41 | virtual bool shouldPositionAtCursor() const { return true; } 42 | }; 43 | 44 | class UI { 45 | public: 46 | UI(wl::Wayland& wayland) : wayland(wayland) {} 47 | // x, y, scale are the compositor's scale (fractional scales are supported) 48 | void init(int x, int y, float scale); 49 | 50 | // run a single frame until it returns a result 51 | FrameResult run(Frame& frame); 52 | 53 | // Run a flow until completion 54 | void runFlow(class Flow& flow); 55 | 56 | void applyTheme(const Config& config); 57 | 58 | // reposition window to center of screen 59 | void centerWindow(); 60 | 61 | Config* currentConfig = nullptr; 62 | 63 | private: 64 | wl::Wayland& wayland; 65 | std::unique_ptr surface; 66 | std::unique_ptr egl; 67 | int initialX = 0, initialY = 0; 68 | int32_t currentScale = 1; 69 | float currentFractionalScale = 1.0f; 70 | bool running = true; 71 | 72 | FrameResult renderFrame(Frame& frame); 73 | void updateScale(int32_t new_scale); 74 | void setupFont(ImGuiIO& io, const Config& config); 75 | }; 76 | -------------------------------------------------------------------------------- /src/wayland/display.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | extern "C" { 4 | #include "protocols/wlr-layer-shell-unstable-v1-client-protocol.h" 5 | #include 6 | } 7 | 8 | #include 9 | #include 10 | 11 | namespace wl { 12 | struct Output { 13 | wl_output* output; 14 | int32_t scale = 1; 15 | uint32_t id; 16 | int32_t width; 17 | int32_t height; 18 | }; 19 | 20 | class Display { 21 | public: 22 | Display(); 23 | ~Display(); 24 | 25 | bool connect(); 26 | void dispatch(); 27 | void dispatchPending(); 28 | void roundtrip(); 29 | void prepareRead(); 30 | void readEvents(); 31 | void flush(); 32 | 33 | wl_display* display() const { return display_; } 34 | wl_compositor* compositor() const { return compositor_; } 35 | zwlr_layer_shell_v1* layerShell() const { return layerShell_; } 36 | wl_seat* seat() const { return seat_; } 37 | 38 | // Output scale management 39 | const std::vector& outputs() const { return outputs_; } 40 | int32_t getMaxScale() const; 41 | void setScaleChangeCallback(std::function callback) { scaleCallback = callback; } 42 | 43 | // get the size of the first output in physical pixels 44 | std::pair getOutputSize() const; 45 | 46 | private: 47 | wl_display* display_; 48 | wl_registry* registry = nullptr; 49 | wl_compositor* compositor_ = nullptr; 50 | zwlr_layer_shell_v1* layerShell_ = nullptr; 51 | wl_seat* seat_ = nullptr; 52 | 53 | std::vector outputs_; 54 | std::function scaleCallback; 55 | 56 | static void 57 | registryHandler(void* data, wl_registry* registry, uint32_t id, const char* interface, uint32_t version); 58 | static void registryRemover(void* data, wl_registry* registry, uint32_t id); 59 | 60 | // Output callbacks 61 | static void outputGeometry(void* data, 62 | wl_output* output, 63 | int32_t x, 64 | int32_t y, 65 | int32_t physicalWidth, 66 | int32_t physicalHeight, 67 | int32_t subpixel, 68 | const char* make, 69 | const char* model, 70 | int32_t transform); 71 | static void 72 | outputMode(void* data, wl_output* output, uint32_t flags, int32_t width, int32_t height, int32_t refresh); 73 | static void outputDone(void* data, wl_output* output); 74 | static void outputScale(void* data, wl_output* output, int32_t factor); 75 | static void outputName(void* data, wl_output* output, const char* name); 76 | static void outputDescription(void* data, wl_output* output, const char* description); 77 | }; 78 | } // namespace wl 79 | -------------------------------------------------------------------------------- /src/flows/audio_flow.cpp: -------------------------------------------------------------------------------- 1 | #include "audio_flow.hpp" 2 | 3 | AudioFlow::AudioFlow() { 4 | typeSelector = std::make_unique(); 5 | typeSelector->add(Choice{"input", "Input (Microphones)", false}); 6 | typeSelector->add(Choice{"output", "Output (Speakers/Headphones)", false}); 7 | } 8 | 9 | void AudioFlow::populateDeviceSelector() { 10 | deviceSelector = std::make_unique(); 11 | 12 | if (selectedType == "input") { 13 | auto devices = audioManager.listInputDevices(); 14 | for (const auto& dev : devices) { 15 | std::string label = dev.description; 16 | deviceSelector->add(Choice{std::to_string(dev.id), label, dev.isDefault}); 17 | } 18 | } else if (selectedType == "output") { 19 | auto devices = audioManager.listOutputDevices(); 20 | for (const auto& dev : devices) { 21 | std::string label = dev.description; 22 | deviceSelector->add(Choice{std::to_string(dev.id), label, dev.isDefault}); 23 | } 24 | } 25 | } 26 | 27 | Frame* AudioFlow::getCurrentFrame() { 28 | switch (currentState) { 29 | case State::SELECT_TYPE: 30 | return typeSelector.get(); 31 | case State::SELECT_DEVICE: 32 | return deviceSelector.get(); 33 | case State::DONE: 34 | default: 35 | return nullptr; 36 | } 37 | } 38 | 39 | bool AudioFlow::handleResult(const FrameResult& result) { 40 | switch (currentState) { 41 | case State::SELECT_TYPE: 42 | if (result.action == FrameResult::Action::SUBMIT) { 43 | selectedType = result.value; 44 | populateDeviceSelector(); 45 | currentState = State::SELECT_DEVICE; 46 | return true; 47 | } else if (result.action == FrameResult::Action::CANCEL) { 48 | done = true; 49 | return false; 50 | } 51 | break; 52 | 53 | case State::SELECT_DEVICE: 54 | if (result.action == FrameResult::Action::SUBMIT) { 55 | selectedDeviceId = std::stoul(result.value); 56 | 57 | // Set the default device 58 | bool success = false; 59 | if (selectedType == "input") { 60 | success = audioManager.setDefaultInput(selectedDeviceId); 61 | } else if (selectedType == "output") { 62 | success = audioManager.setDefaultOutput(selectedDeviceId); 63 | } 64 | 65 | // TODO: Maybe show a confirmation/error frame? 66 | currentState = State::DONE; 67 | done = true; 68 | return false; 69 | } else if (result.action == FrameResult::Action::CANCEL) { 70 | // Go back to type selection 71 | currentState = State::SELECT_TYPE; 72 | return true; 73 | } 74 | break; 75 | 76 | case State::DONE: 77 | done = true; 78 | return false; 79 | } 80 | 81 | return true; 82 | } 83 | 84 | bool AudioFlow::isDone() const { return done; } 85 | 86 | std::string AudioFlow::getResult() const { 87 | if (!selectedType.empty() && selectedDeviceId != 0) { 88 | return selectedType + ":" + std::to_string(selectedDeviceId); 89 | } 90 | return ""; 91 | } 92 | -------------------------------------------------------------------------------- /src/flows/custom_flow.cpp: -------------------------------------------------------------------------------- 1 | #include "custom_flow.hpp" 2 | #include 3 | 4 | CustomFlow::CustomFlow(const std::string& rootConfigPath) { pushMenu(rootConfigPath); } 5 | 6 | void CustomFlow::pushMenu(const std::string& configPath) { 7 | MenuLevel level; 8 | level.configPath = configPath; 9 | level.frame = std::make_unique(configPath); 10 | menuStack.push(std::move(level)); 11 | } 12 | 13 | void CustomFlow::popMenu() { 14 | if (menuStack.size() > 1) { 15 | menuStack.pop(); 16 | } 17 | } 18 | 19 | bool CustomFlow::isCustomAction(const std::string& result) const { return result.find("__SUBMENU__:") == 0; } 20 | 21 | std::string CustomFlow::extractCustomPath(const std::string& result) const { 22 | if (isCustomAction(result)) { 23 | return result.substr(12); // skip "__SUBMENU__:" 24 | } 25 | return ""; 26 | } 27 | 28 | Frame* CustomFlow::getCurrentFrame() { 29 | if (menuStack.empty()) { 30 | return nullptr; 31 | } 32 | return menuStack.top().frame.get(); 33 | } 34 | 35 | bool CustomFlow::handleResult(const FrameResult& result) { 36 | if (result.action == FrameResult::Action::SUBMIT) { 37 | std::string value = result.value; 38 | 39 | // check for special submenu navigation 40 | if (value == "__BACK__") { 41 | // go back to parent menu 42 | if (menuStack.size() > 1) { 43 | popMenu(); 44 | return true; // continue with parent menu 45 | } else { 46 | // already at root treat as cancel 47 | done = true; 48 | return false; 49 | } 50 | } else if (isCustomAction(value)) { 51 | // navigate to submenu 52 | std::string submenuPath = extractCustomPath(value); 53 | 54 | // if path is relative, make it relative to current config's directory 55 | if (!submenuPath.empty() && submenuPath[0] != '/') { 56 | // extract directory from current config path 57 | std::string currentPath = menuStack.top().configPath; 58 | size_t lastSlash = currentPath.find_last_of('/'); 59 | if (lastSlash != std::string::npos) { 60 | std::string dir = currentPath.substr(0, lastSlash + 1); 61 | submenuPath = dir + submenuPath; 62 | } 63 | } 64 | 65 | pushMenu(submenuPath); 66 | return true; // continue with new submenu 67 | } else { 68 | // normal submit, we're done 69 | finalResult = value; 70 | done = true; 71 | return false; 72 | } 73 | } else if (result.action == FrameResult::Action::CANCEL) { 74 | // on cancel, go back if we're in a submenu, otherwise exit 75 | if (menuStack.size() > 1) { 76 | popMenu(); 77 | return true; // continue with parent menu 78 | } else { 79 | // at root menu, cancel means exit 80 | done = true; 81 | return false; 82 | } 83 | } 84 | 85 | return true; 86 | } 87 | 88 | bool CustomFlow::isDone() const { return done; } 89 | 90 | std::string CustomFlow::getResult() const { return finalResult; } 91 | -------------------------------------------------------------------------------- /src/renderer/egl_context.cpp: -------------------------------------------------------------------------------- 1 | #include "egl_context.hpp" 2 | #include "../debug/log.hpp" 3 | 4 | namespace egl { 5 | 6 | Context::Context(wl_display* display) : display(display) { 7 | egl_display = eglGetDisplay((EGLNativeDisplayType)display); 8 | if (egl_display == EGL_NO_DISPLAY) { 9 | debug::log(ERR, "Failed to get EGL display"); 10 | return; 11 | } 12 | 13 | if (!eglInitialize(egl_display, nullptr, nullptr)) { 14 | debug::log(ERR, "Failed to initialize EGL"); 15 | return; 16 | } 17 | 18 | EGLint num_config; 19 | // static const EGLint attribs[] = {EGL_RENDERABLE_TYPE, EGL_OPENGL_BIT, EGL_NONE}; 20 | static const EGLint attribs[] = {EGL_SURFACE_TYPE, 21 | EGL_WINDOW_BIT, 22 | EGL_RENDERABLE_TYPE, 23 | EGL_OPENGL_ES2_BIT, // Use ES2 instead 24 | EGL_RED_SIZE, 25 | 8, 26 | EGL_GREEN_SIZE, 27 | 8, 28 | EGL_BLUE_SIZE, 29 | 8, 30 | EGL_ALPHA_SIZE, 31 | 8, 32 | EGL_NONE}; 33 | eglChooseConfig(egl_display, attribs, &egl_config, 1, &num_config); 34 | 35 | eglBindAPI(EGL_OPENGL_ES_API); 36 | static const EGLint context_attribs[] = {EGL_CONTEXT_CLIENT_VERSION, 37 | 2, // Request ES 2.0 38 | EGL_NONE}; 39 | egl_context = eglCreateContext(egl_display, egl_config, EGL_NO_CONTEXT, context_attribs); 40 | if (egl_context == EGL_NO_CONTEXT) { 41 | debug::log(ERR, "Failed to create EGL context"); 42 | debug::log(ERR, "Error code: 0x%x", eglGetError()); 43 | return; 44 | } 45 | 46 | egl_surface = EGL_NO_SURFACE; 47 | } 48 | 49 | Vec2 Context::getBufferSize() const { 50 | int width, height; 51 | wl_egl_window_get_attached_size(egl_window, &width, &height); 52 | return {(float)width, (float)height}; 53 | } 54 | 55 | Context::~Context() { 56 | if (egl_window) 57 | wl_egl_window_destroy(egl_window); 58 | if (egl_surface != EGL_NO_SURFACE) 59 | eglDestroySurface(egl_display, egl_surface); 60 | if (egl_context != EGL_NO_CONTEXT) 61 | eglDestroyContext(egl_display, egl_context); 62 | if (egl_display != EGL_NO_DISPLAY) 63 | eglTerminate(egl_display); 64 | } 65 | 66 | bool Context::createWindowSurface(wl_surface* surface, int width, int height) { 67 | egl_window = wl_egl_window_create(surface, width, height); 68 | if (!egl_window) { 69 | debug::log(ERR, "Failed to create EGL window"); 70 | return false; 71 | } 72 | 73 | EGLint surface_attribs[] = {EGL_RENDER_BUFFER, EGL_BACK_BUFFER, EGL_NONE}; 74 | 75 | egl_surface = eglCreateWindowSurface(egl_display, egl_config, (EGLNativeWindowType)egl_window, surface_attribs); 76 | if (egl_surface == EGL_NO_SURFACE) { 77 | debug::log(ERR, "Failed to create EGL surface"); 78 | debug::log(ERR, "Error code: 0x%x", eglGetError()); 79 | return false; 80 | } 81 | 82 | makeCurrent(); 83 | return true; 84 | } 85 | 86 | void Context::makeCurrent() { eglMakeCurrent(egl_display, egl_surface, egl_surface, egl_context); } 87 | 88 | void Context::swapBuffers() { eglSwapBuffers(egl_display, egl_surface); } 89 | } // namespace egl 90 | -------------------------------------------------------------------------------- /src/wayland/protocols/wlr-layer-shell-unstable-v1-client-protocol.c: -------------------------------------------------------------------------------- 1 | /* Generated by wayland-scanner 1.24.0 */ 2 | 3 | /* 4 | * Copyright © 2017 Drew DeVault 5 | * 6 | * Permission to use, copy, modify, distribute, and sell this 7 | * software and its documentation for any purpose is hereby granted 8 | * without fee, provided that the above copyright notice appear in 9 | * all copies and that both that copyright notice and this permission 10 | * notice appear in supporting documentation, and that the name of 11 | * the copyright holders not be used in advertising or publicity 12 | * pertaining to distribution of the software without specific, 13 | * written prior permission. The copyright holders make no 14 | * representations about the suitability of this software for any 15 | * purpose. It is provided "as is" without express or implied 16 | * warranty. 17 | * 18 | * THE COPYRIGHT HOLDERS DISCLAIM ALL WARRANTIES WITH REGARD TO THIS 19 | * SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND 20 | * FITNESS, IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY 21 | * SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 22 | * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN 23 | * AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, 24 | * ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF 25 | * THIS SOFTWARE. 26 | */ 27 | 28 | #include 29 | #include 30 | #include 31 | #include "wayland-util.h" 32 | 33 | #ifndef __has_attribute 34 | # define __has_attribute(x) 0 /* Compatibility with non-clang compilers. */ 35 | #endif 36 | 37 | #if (__has_attribute(visibility) || defined(__GNUC__) && __GNUC__ >= 4) 38 | #define WL_PRIVATE __attribute__ ((visibility("hidden"))) 39 | #else 40 | #define WL_PRIVATE 41 | #endif 42 | 43 | extern const struct wl_interface wl_output_interface; 44 | extern const struct wl_interface wl_surface_interface; 45 | extern const struct wl_interface xdg_popup_interface; 46 | extern const struct wl_interface zwlr_layer_surface_v1_interface; 47 | 48 | static const struct wl_interface *wlr_layer_shell_unstable_v1_types[] = { 49 | NULL, 50 | NULL, 51 | NULL, 52 | NULL, 53 | &zwlr_layer_surface_v1_interface, 54 | &wl_surface_interface, 55 | &wl_output_interface, 56 | NULL, 57 | NULL, 58 | &xdg_popup_interface, 59 | }; 60 | 61 | static const struct wl_message zwlr_layer_shell_v1_requests[] = { 62 | { "get_layer_surface", "no?ous", wlr_layer_shell_unstable_v1_types + 4 }, 63 | { "destroy", "3", wlr_layer_shell_unstable_v1_types + 0 }, 64 | }; 65 | 66 | WL_PRIVATE const struct wl_interface zwlr_layer_shell_v1_interface = { 67 | "zwlr_layer_shell_v1", 5, 68 | 2, zwlr_layer_shell_v1_requests, 69 | 0, NULL, 70 | }; 71 | 72 | static const struct wl_message zwlr_layer_surface_v1_requests[] = { 73 | { "set_size", "uu", wlr_layer_shell_unstable_v1_types + 0 }, 74 | { "set_anchor", "u", wlr_layer_shell_unstable_v1_types + 0 }, 75 | { "set_exclusive_zone", "i", wlr_layer_shell_unstable_v1_types + 0 }, 76 | { "set_margin", "iiii", wlr_layer_shell_unstable_v1_types + 0 }, 77 | { "set_keyboard_interactivity", "u", wlr_layer_shell_unstable_v1_types + 0 }, 78 | { "get_popup", "o", wlr_layer_shell_unstable_v1_types + 9 }, 79 | { "ack_configure", "u", wlr_layer_shell_unstable_v1_types + 0 }, 80 | { "destroy", "", wlr_layer_shell_unstable_v1_types + 0 }, 81 | { "set_layer", "2u", wlr_layer_shell_unstable_v1_types + 0 }, 82 | { "set_exclusive_edge", "5u", wlr_layer_shell_unstable_v1_types + 0 }, 83 | }; 84 | 85 | static const struct wl_message zwlr_layer_surface_v1_events[] = { 86 | { "configure", "uuu", wlr_layer_shell_unstable_v1_types + 0 }, 87 | { "closed", "", wlr_layer_shell_unstable_v1_types + 0 }, 88 | }; 89 | 90 | WL_PRIVATE const struct wl_interface zwlr_layer_surface_v1_interface = { 91 | "zwlr_layer_surface_v1", 5, 92 | 10, zwlr_layer_surface_v1_requests, 93 | 2, zwlr_layer_surface_v1_events, 94 | }; 95 | 96 | -------------------------------------------------------------------------------- /src/flows/simple_flows.cpp: -------------------------------------------------------------------------------- 1 | #include "simple_flows.hpp" 2 | #include "../frames/input.hpp" 3 | #include "../frames/selector.hpp" 4 | 5 | MenuFlow::MenuFlow() { selector = std::make_unique(); } 6 | 7 | MenuFlow::MenuFlow(const std::vector& choices) { 8 | selector = std::make_unique(); 9 | for (auto& choice : choices) { 10 | selector->add(Choice{choice.id, choice.display, choice.selected}); 11 | } 12 | } 13 | 14 | MenuFlow::~MenuFlow() = default; 15 | 16 | void MenuFlow::addChoice(const Choice& choice) { selector->add(choice); } 17 | 18 | Frame* MenuFlow::getCurrentFrame() { return selector.get(); } 19 | 20 | bool MenuFlow::handleResult(const FrameResult& result) { 21 | if (result.action == FrameResult::Action::SUBMIT) { 22 | finalResult = result.value; 23 | done = true; 24 | return false; 25 | } else if (result.action == FrameResult::Action::CANCEL) { 26 | done = true; 27 | return false; 28 | } 29 | return true; 30 | } 31 | 32 | bool MenuFlow::isDone() const { return done; } 33 | 34 | std::string MenuFlow::getResult() const { return finalResult; } 35 | 36 | // SimpleInputFlow implementation 37 | InputFlow::InputFlow(const std::string& hint, bool password) { input = std::make_unique(hint, password); } 38 | 39 | InputFlow::~InputFlow() = default; 40 | 41 | Frame* InputFlow::getCurrentFrame() { return input.get(); } 42 | 43 | bool InputFlow::handleResult(const FrameResult& result) { 44 | if (result.action == FrameResult::Action::SUBMIT) { 45 | finalResult = result.value; 46 | done = true; 47 | return false; 48 | } else if (result.action == FrameResult::Action::CANCEL) { 49 | done = true; 50 | return false; 51 | } 52 | return true; 53 | } 54 | 55 | bool InputFlow::isDone() const { return done; } 56 | 57 | std::string InputFlow::getResult() const { return finalResult; } 58 | 59 | // MenuThenInputFlow implementation 60 | MenuThenInputFlow::MenuThenInputFlow(const std::vector& choices, const std::string& inputHintPrefix) 61 | : inputHintPrefix(inputHintPrefix) { 62 | selector = std::make_unique(); 63 | for (auto& choice : choices) { 64 | selector->add(Choice{choice.id, choice.display, choice.selected}); 65 | } 66 | } 67 | 68 | Frame* MenuThenInputFlow::getCurrentFrame() { 69 | if (currentState == State::SELECTING) { 70 | return selector.get(); 71 | } else if (currentState == State::INPUT) { 72 | return input.get(); 73 | } 74 | return nullptr; 75 | } 76 | 77 | bool MenuThenInputFlow::handleResult(const FrameResult& result) { 78 | if (currentState == State::SELECTING) { 79 | if (result.action == FrameResult::Action::SUBMIT) { 80 | selectedItem = result.value; 81 | // Create input frame with hint based on selection 82 | input = std::make_unique(inputHintPrefix + " " + selectedItem); 83 | currentState = State::INPUT; 84 | return true; 85 | } else if (result.action == FrameResult::Action::CANCEL) { 86 | done = true; 87 | return false; 88 | } 89 | } else if (currentState == State::INPUT) { 90 | if (result.action == FrameResult::Action::SUBMIT) { 91 | inputValue = result.value; 92 | done = true; 93 | return false; 94 | } else if (result.action == FrameResult::Action::CANCEL) { 95 | // Go back to menu 96 | currentState = State::SELECTING; 97 | return true; 98 | } 99 | } 100 | return true; 101 | } 102 | 103 | bool MenuThenInputFlow::isDone() const { return done; } 104 | 105 | std::string MenuThenInputFlow::getResult() const { return selectedItem + ":" + inputValue; } 106 | 107 | std::string MenuThenInputFlow::getSelectedItem() const { return selectedItem; } 108 | 109 | std::string MenuThenInputFlow::getInputValue() const { return inputValue; } 110 | -------------------------------------------------------------------------------- /src/wayland/input.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | extern "C" { 4 | #include 5 | } 6 | 7 | #include "imgui.h" 8 | #include 9 | 10 | #define XKB_MOD_SHIFT (1 << 0) 11 | #define XKB_MOD_LOCK (1 << 1) 12 | #define XKB_MOD_CTRL (1 << 2) 13 | #define XKB_MOD_ALT (1 << 3) 14 | #define XKB_MOD_LOGO (1 << 6) 15 | 16 | namespace wl { 17 | 18 | // handles input from a wl_seat and forwards it to ImGui 19 | class InputHandler { 20 | public: 21 | InputHandler(wl_seat*); 22 | InputHandler(wl_seat* seat, ImGuiIO* io); 23 | ~InputHandler(); 24 | 25 | void setWindowBounds(int width, int height); 26 | bool shouldExit() const { return shouldExit_; } 27 | void setIO(ImGuiIO* new_io) { io = new_io; } 28 | 29 | private: 30 | wl_seat* seat; 31 | wl_pointer* pointer = nullptr; 32 | wl_keyboard* keyboard = nullptr; 33 | ImGuiIO* io; 34 | int width = 0; 35 | int height = 0; 36 | bool shouldExit_ = false; 37 | 38 | // xkb state 39 | struct xkb_context* context = nullptr; 40 | struct xkb_keymap* keymap = nullptr; 41 | struct xkb_state* state = nullptr; 42 | xkb_mod_mask_t controlMask = 0; 43 | xkb_mod_mask_t altMask = 0; 44 | xkb_mod_mask_t shiftMask = 0; 45 | xkb_mod_mask_t superMask = 0; 46 | 47 | // key repeat state 48 | int32_t repeatRate = 0; 49 | int32_t repeatDelay = 0; 50 | 51 | // kb helper functions 52 | void updateModifiers(xkb_state* state); 53 | void handleKey(uint32_t key, bool pressed); 54 | 55 | // seat callbacks 56 | static void seatCapabilities(void* data, wl_seat* seat, uint32_t capabilities); 57 | static void seatName(void* data, wl_seat* seat, const char* name); 58 | 59 | // listener 60 | constexpr static const wl_seat_listener seatListener = {.capabilities = seatCapabilities, .name = seatName}; 61 | 62 | // pointer callbacks 63 | static void pointerEnter( 64 | void* data, wl_pointer* pointer, uint32_t serial, wl_surface* surface, wl_fixed_t sx, wl_fixed_t sy); 65 | static void pointerLeave(void* data, wl_pointer* pointer, uint32_t serial, wl_surface* surface); 66 | static void pointerMotion(void* data, wl_pointer* pointer, uint32_t time, wl_fixed_t sx, wl_fixed_t sy); 67 | static void pointerButton( 68 | void* data, wl_pointer* pointer, uint32_t serial, uint32_t time, uint32_t button, uint32_t state); 69 | static void pointerAxis(void* data, wl_pointer* pointer, uint32_t time, uint32_t axis, wl_fixed_t value); 70 | static void pointerFrame(void* data, wl_pointer* pointer); 71 | static void pointerAxisSource(void* data, wl_pointer* pointer, uint32_t axis_source); 72 | static void pointerAxisStop(void* data, wl_pointer* pointer, uint32_t time, uint32_t axis); 73 | static void pointerAxisDiscrete(void* data, wl_pointer* pointer, uint32_t axis, int32_t discrete); 74 | 75 | // keyboard callbacks 76 | static void keyboardKeymap(void* data, wl_keyboard* keyboard, uint32_t format, int32_t fd, uint32_t size); 77 | static void 78 | keyboardEnter(void* data, wl_keyboard* keyboard, uint32_t serial, wl_surface* surface, wl_array* keys); 79 | static void keyboardLeave(void* data, wl_keyboard* keyboard, uint32_t serial, wl_surface* surface); 80 | static void keyboardKey( 81 | void* data, wl_keyboard* keyboard, uint32_t serial, uint32_t time, uint32_t key, uint32_t state); 82 | static void keyboardModifiers(void* data, 83 | wl_keyboard* keyboard, 84 | uint32_t serial, 85 | uint32_t modsDepressed, 86 | uint32_t modsLatched, 87 | uint32_t modsLocked, 88 | uint32_t group); 89 | static void keyboardRepeatInfo(void* data, wl_keyboard* keyboard, int32_t rate, int32_t delay); 90 | }; 91 | } // namespace wl 92 | -------------------------------------------------------------------------------- /src/wayland/layer_surface.cpp: -------------------------------------------------------------------------------- 1 | #include "layer_surface.hpp" 2 | 3 | namespace wl { 4 | LayerSurface::LayerSurface(wl_compositor* compositor, zwlr_layer_shell_v1* shell) 5 | : compositor(compositor), layerShell(shell) {} 6 | 7 | LayerSurface::~LayerSurface() { 8 | if (layerSurface) 9 | zwlr_layer_surface_v1_destroy(layerSurface); 10 | if (surface_) 11 | wl_surface_destroy(surface_); 12 | } 13 | 14 | // create the layer surface at given position and size 15 | void LayerSurface::create(int x, int y, int width, int height) { 16 | width_ = width; 17 | height_ = height; 18 | 19 | surface_ = wl_compositor_create_surface(compositor); 20 | layerSurface = zwlr_layer_shell_v1_get_layer_surface( 21 | layerShell, surface_, nullptr, ZWLR_LAYER_SHELL_V1_LAYER_OVERLAY, "popup_menu"); 22 | 23 | static const zwlr_layer_surface_v1_listener listener = {.configure = configureHandler, .closed = closedHandler}; 24 | zwlr_layer_surface_v1_add_listener(layerSurface, &listener, this); 25 | 26 | zwlr_layer_surface_v1_set_size(layerSurface, width, height); 27 | zwlr_layer_surface_v1_set_anchor(layerSurface, 28 | ZWLR_LAYER_SURFACE_V1_ANCHOR_TOP | ZWLR_LAYER_SURFACE_V1_ANCHOR_LEFT); 29 | zwlr_layer_surface_v1_set_margin(layerSurface, y, 0, 0, x); 30 | zwlr_layer_surface_v1_set_keyboard_interactivity(layerSurface, 31 | ZWLR_LAYER_SURFACE_V1_KEYBOARD_INTERACTIVITY_EXCLUSIVE); 32 | zwlr_layer_surface_v1_set_exclusive_zone(layerSurface, 0); 33 | 34 | wl_surface_commit(surface_); 35 | } 36 | 37 | // resize the layer surface and associated EGL window 38 | void LayerSurface::resize(int newWidth, int newHeight, egl::Context& egl) { 39 | width_ = newWidth; 40 | height_ = newHeight; 41 | 42 | // Resize the layer surface 43 | zwlr_layer_surface_v1_set_size(layerSurface, newWidth, newHeight); 44 | wl_surface_commit(surface_); 45 | 46 | // Resize the EGL window 47 | if (egl.window()) { 48 | const int bufW = newWidth * (scale_ > 0 ? scale_ : 1); 49 | const int bufH = newHeight * (scale_ > 0 ? scale_ : 1); 50 | wl_egl_window_resize(egl.window(), bufW, bufH, 0, 0); 51 | } 52 | } 53 | 54 | // set the buffer scale for hidpi support 55 | void LayerSurface::bufferScale(int32_t scale) { 56 | scale_ = scale > 0 ? scale : 1; 57 | wl_surface_set_buffer_scale(surface_, scale_); 58 | wl_surface_commit(surface_); 59 | } 60 | 61 | void LayerSurface::configureHandler( 62 | void* data, zwlr_layer_surface_v1* layerSurface, uint32_t serial, uint32_t width, uint32_t height) { 63 | LayerSurface* self = static_cast(data); 64 | 65 | if (width > 0) 66 | self->width_ = width; 67 | if (height > 0) 68 | self->height_ = height; 69 | 70 | zwlr_layer_surface_v1_ack_configure(layerSurface, serial); 71 | wl_surface_commit(self->surface_); 72 | self->configured = true; 73 | } 74 | 75 | void LayerSurface::closedHandler(void* data, zwlr_layer_surface_v1*) { 76 | LayerSurface* self = static_cast(data); 77 | self->shouldExit_ = true; 78 | } 79 | 80 | // logical pixel coordinates 81 | void LayerSurface::reposition( 82 | int x, int y, int viewportWidth, int viewportHeight, int windowWidth, int windowHeight) { 83 | int finalX = x; 84 | int finalY = y; 85 | 86 | if (x + windowWidth > viewportWidth) { 87 | finalX = viewportWidth - windowWidth; 88 | } 89 | 90 | if (y + windowHeight > viewportHeight) { 91 | finalY = viewportHeight - windowHeight; 92 | } 93 | 94 | finalX = std::max(0, finalX); 95 | finalY = std::max(0, finalY); 96 | 97 | zwlr_layer_surface_v1_set_margin(layerSurface, finalY, 0, 0, finalX); 98 | wl_surface_commit(surface_); 99 | } 100 | } // namespace wl 101 | -------------------------------------------------------------------------------- /src/wallpaper/thumbnail.cpp: -------------------------------------------------------------------------------- 1 | #include "thumbnail.hpp" 2 | #include "../debug/log.hpp" 3 | 4 | #include 5 | #include 6 | 7 | #define STB_IMAGE_IMPLEMENTATION 8 | #include 9 | #define STB_IMAGE_RESIZE_IMPLEMENTATION 10 | #include 11 | #define STB_IMAGE_WRITE_IMPLEMENTATION 12 | #include 13 | 14 | namespace fs = std::filesystem; 15 | 16 | std::string ThumbnailCache::getOrCreateThumbnail(const std::string& imagePath, int width, int height) { 17 | int hashKey = hashFileKey(std::string(imagePath)); 18 | if (hashKey == 0) { 19 | return ""; 20 | } 21 | 22 | std::string thumbPath = 23 | filepath_ + std::to_string(hashKey) + "_" + std::to_string(width) + "x" + std::to_string(height) + ".png"; 24 | 25 | if (fs::exists(thumbPath)) { 26 | return thumbPath; // thumbnail already exists 27 | } 28 | 29 | // create thumbnail 30 | if (resizeImage(imagePath, thumbPath, width, height)) { 31 | debug::log(DEBUG, "Thumbnail created at: {}", thumbPath); 32 | return thumbPath; 33 | } else { 34 | return ""; // error resizing image 35 | } 36 | } 37 | 38 | int ThumbnailCache::hashFileKey(std::string&& path) { 39 | 40 | fs::path file(path); 41 | if (!fs::exists(file)) { 42 | debug::log(ERR, "File does not exist: {}", path); 43 | return 0; 44 | } 45 | 46 | // WTF: auto ftime = fs::last_write_time(file).time_since_epoch().count(); 47 | // auto ftime = 48 | // std::chrono::duration_cast(fs::last_write_time(file).time_since_epoch()).count(); 49 | // auto filetime = fs::last_write_time(file); 50 | // std::time_t ftime = std::chrono::system_clock::to_time_t(filetime); 51 | 52 | auto ftime = fileLastWriteTime(file); 53 | auto fsize = fs::file_size(file); 54 | auto fname = file.string(); 55 | 56 | debug::log(DEBUG, "Hashing file: {}, size: {}, last write time: {}", fname, fsize, ftime); 57 | 58 | std::hash str_hash; 59 | size_t h1 = str_hash(fname); 60 | size_t h2 = std::hash{}(fsize); 61 | size_t h3 = std::hash{}(ftime); 62 | return h1 ^ (h2 << 1) ^ (h3 << 2); 63 | } 64 | 65 | // unbelievable 66 | uint64_t ThumbnailCache::fileLastWriteTime(const fs::path& file) { 67 | auto ftime_fs = fs::last_write_time(file); 68 | 69 | // convert file_time_type to system_clock::time_point 70 | auto ftime_sctp = std::chrono::time_point_cast( 71 | ftime_fs - fs::file_time_type::clock::now() + std::chrono::system_clock::now()); 72 | 73 | // seconds since epoch 74 | return std::chrono::duration_cast(ftime_sctp.time_since_epoch()).count(); 75 | } 76 | 77 | bool ThumbnailCache::resizeImage(std::string inPath, std::string outPath, int newW, int newH) { 78 | int w, h, ch; 79 | unsigned char* input = stbi_load(inPath.c_str(), &w, &h, &ch, 0); 80 | if (!input) { 81 | return false; 82 | } 83 | 84 | std::vector output(newW * newH * ch); 85 | 86 | int inputStride = w * ch; 87 | int outputStride = newW * ch; 88 | 89 | stbir_pixel_layout layout; 90 | switch (ch) { 91 | case 1: 92 | layout = STBIR_1CHANNEL; 93 | break; 94 | case 2: 95 | layout = STBIR_2CHANNEL; 96 | break; 97 | case 3: 98 | layout = STBIR_RGB; 99 | break; 100 | case 4: 101 | layout = STBIR_RGBA; 102 | break; 103 | default: 104 | layout = STBIR_RGB; // fallback 105 | break; 106 | } 107 | 108 | // perform resizing using the srgb-aware function 109 | unsigned char* result = 110 | stbir_resize_uint8_srgb(input, w, h, inputStride, output.data(), newW, newH, outputStride, layout); 111 | 112 | if (result == nullptr) { 113 | // resizing failed 114 | stbi_image_free(input); 115 | return false; 116 | } 117 | 118 | // save as png 119 | stbi_write_png(outPath.c_str(), newW, newH, ch, output.data(), outputStride); 120 | 121 | stbi_image_free(input); 122 | return true; 123 | }; 124 | -------------------------------------------------------------------------------- /.github/workflows/cmake-multi-platform.yml: -------------------------------------------------------------------------------- 1 | name: Build and Package 2 | on: 3 | pull_request: 4 | branches: ["main"] 5 | 6 | jobs: 7 | build-ubuntu: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v4 11 | with: 12 | submodules: recursive 13 | 14 | - name: Install dependencies 15 | run: | 16 | sudo apt-get update 17 | sudo apt-get install -y \ 18 | cmake \ 19 | gcc \ 20 | g++ \ 21 | libwayland-dev \ 22 | wayland-protocols \ 23 | libgl1-mesa-dev \ 24 | libegl1-mesa-dev \ 25 | libfontconfig1-dev \ 26 | libxkbcommon-dev \ 27 | libsdbus-c++-dev \ 28 | libpipewire-0.3-dev \ 29 | pkg-config 30 | 31 | - name: Configure CMake 32 | run: cmake -B build -DCMAKE_BUILD_TYPE=Release 33 | 34 | - name: Build 35 | run: cmake --build build --config Release 36 | 37 | - name: Package 38 | run: | 39 | cd build 40 | cpack 41 | 42 | - name: Upload artifacts 43 | uses: actions/upload-artifact@v4 44 | with: 45 | name: ubuntu-packages 46 | path: | 47 | build/*.deb 48 | build/*.tar.gz 49 | 50 | build-arch: 51 | runs-on: ubuntu-latest 52 | container: archlinux:latest 53 | steps: 54 | - name: Install dependencies 55 | run: | 56 | pacman -Syu --noconfirm 57 | pacman -S --noconfirm \ 58 | base-devel \ 59 | cmake \ 60 | gcc \ 61 | git \ 62 | wayland \ 63 | wayland-protocols \ 64 | mesa \ 65 | fontconfig \ 66 | libxkbcommon \ 67 | sdbus-cpp \ 68 | pipewire \ 69 | pkgconf 70 | 71 | - uses: actions/checkout@v4 72 | with: 73 | submodules: recursive 74 | 75 | - name: Configure CMake 76 | run: cmake -B build -DCMAKE_BUILD_TYPE=Release 77 | 78 | - name: Build 79 | run: cmake --build build --config Release 80 | 81 | - name: Package 82 | run: | 83 | cd build 84 | cpack 85 | 86 | - name: Upload artifacts 87 | uses: actions/upload-artifact@v4 88 | with: 89 | name: arch-packages 90 | path: build/*.tar.gz 91 | 92 | build-centos: 93 | runs-on: ubuntu-latest 94 | container: quay.io/centos/centos:stream9 95 | steps: 96 | - name: Install dependencies 97 | run: | 98 | dnf install -y epel-release 99 | dnf config-manager --set-enabled crb 100 | dnf install -y \ 101 | cmake \ 102 | gcc \ 103 | gcc-c++ \ 104 | git \ 105 | wayland-devel \ 106 | wayland-protocols-devel \ 107 | mesa-libGL-devel \ 108 | mesa-libEGL-devel \ 109 | fontconfig-devel \ 110 | libxkbcommon-devel \ 111 | pipewire-devel \ 112 | pkgconfig \ 113 | rpm-build 114 | 115 | - uses: actions/checkout@v4 116 | with: 117 | submodules: recursive 118 | 119 | - name: Install sdbus-cpp (not in CentOS repos) 120 | run: | 121 | dnf install -y systemd-devel expat-devel 122 | git clone https://github.com/Kistler-Group/sdbus-cpp.git 123 | cd sdbus-cpp 124 | mkdir build && cd build 125 | cmake .. -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=/usr 126 | make -j$(nproc) 127 | make install 128 | 129 | - name: Configure CMake 130 | run: cmake -B build -DCMAKE_BUILD_TYPE=Release 131 | 132 | - name: Build 133 | run: cmake --build build --config Release 134 | 135 | - name: Package 136 | run: | 137 | cd build 138 | cpack 139 | 140 | - name: Upload artifacts 141 | uses: actions/upload-artifact@v4 142 | with: 143 | name: centos-packages 144 | path: | 145 | build/*.rpm 146 | build/*.tar.gz 147 | -------------------------------------------------------------------------------- /src/input.cpp: -------------------------------------------------------------------------------- 1 | #include "input.hpp" 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | std::mutex Input::mutex; 9 | ParseResult Input::parseArgv(int argc, const char* argv[]) { 10 | 11 | ParseResult result; 12 | 13 | if (argc <= 1) { 14 | return result; 15 | } 16 | 17 | // Check for --input mode 18 | if (std::string(argv[1]) == "--input" || std::string(argv[1]) == "--password") { 19 | result.mode = (std::string(argv[1]) == "--input") ? InputMode::INPUT : InputMode::PASSWORD; 20 | // If there's a second argument, use it as the hint 21 | if (argc > 2) { 22 | result.hint = argv[2]; 23 | } 24 | return result; 25 | } 26 | 27 | if (std::string(argv[1]) == "--wifi") { 28 | result.mode = InputMode::WIFI; 29 | return result; 30 | } 31 | 32 | if (std::string(argv[1]) == "--audio") { 33 | result.mode = InputMode::AUDIO; 34 | return result; 35 | } 36 | 37 | if (std::string(argv[1]) == "--custom") { 38 | result.mode = InputMode::CUSTOM; 39 | if (argc > 2) { 40 | result.configPath = argv[2]; 41 | } 42 | return result; 43 | } 44 | 45 | if (std::string(argv[1]) == "--wallpaper") { 46 | result.mode = InputMode::WALLPAPER; 47 | if (argc > 2) { 48 | result.wallpaperDir = Input::expandPath(argv[2]); 49 | } else { 50 | std::filesystem::path userWallpapers = Input::expandPath("~/.local/share/wallpapers"); 51 | if (std::filesystem::exists(userWallpapers)) { 52 | result.wallpaperDir = userWallpapers.string(); 53 | return result; 54 | } else if (std::filesystem::exists("/usr/share/wallpapers")) { 55 | result.wallpaperDir = "/usr/share/wallpapers"; 56 | return result; 57 | } 58 | } 59 | return result; 60 | } 61 | 62 | // Default MENU mode - parse all arguments as choices 63 | result.mode = InputMode::MENU; 64 | for (int i = 1; i < argc; ++i) { 65 | result.choices.push_back(parseLine(argv[i])); 66 | } 67 | 68 | // Ensure only one item is selected 69 | bool anySelected = 70 | std::any_of(result.choices.begin(), result.choices.end(), [](const Choice& item) { return item.selected; }); 71 | 72 | return result; 73 | } 74 | 75 | void Input::parseStdin(Callback callback) { 76 | 77 | std::thread inputThread([&]() { 78 | std::string line; 79 | while (std::getline(std::cin, line)) { 80 | if (line.empty()) { 81 | continue; 82 | } 83 | auto item = parseLine(line); 84 | { 85 | std::lock_guard lock(mutex); 86 | callback(item); 87 | } 88 | } 89 | }); 90 | 91 | inputThread.detach(); 92 | } 93 | 94 | Choice Input::parseLine(std::string line) { 95 | Choice item; 96 | std::string s = line; 97 | 98 | if (!s.empty() && s.back() == '*') { 99 | item.selected = true; 100 | s.pop_back(); 101 | } 102 | 103 | size_t colon = s.find(':'); 104 | if (colon != std::string::npos) { 105 | item.id = s.substr(0, colon); 106 | item.display = s.substr(colon + 1); 107 | } else { 108 | item.id = s; 109 | item.display = s; 110 | } 111 | 112 | return item; 113 | } 114 | 115 | std::filesystem::path Input::expandPath(const std::string& path) { 116 | if (path.empty()) 117 | return path; 118 | 119 | // ~ expansion 120 | if (path[0] == '~') { 121 | const char* home = std::getenv("HOME"); 122 | if (!home) { 123 | throw std::runtime_error("HOME environment variable not set"); 124 | } 125 | return std::filesystem::path(home) / path.substr(2); 126 | } 127 | 128 | if (path.find("$HOME") == 0) { 129 | const char* home = std::getenv("HOME"); 130 | if (!home) { 131 | throw std::runtime_error("HOME environment variable not set"); 132 | } 133 | return std::filesystem::path(home) / path.substr(6); 134 | } 135 | 136 | return path; 137 | } 138 | -------------------------------------------------------------------------------- /src/flows/wifi_flow.cpp: -------------------------------------------------------------------------------- 1 | #include "wifi_flow.hpp" 2 | 3 | // initializes frames 4 | WifiFlow::WifiFlow() { networkSelector = std::make_unique(); } 5 | 6 | WifiFlow::~WifiFlow() { 7 | if (scanThread.joinable()) { 8 | scanThread.join(); 9 | } 10 | } 11 | 12 | // starts scanning for networks 13 | void WifiFlow::start() { 14 | // load known networks 15 | std::vector knownNets = nm.listWifiNetworks(); 16 | for (const auto& net : knownNets) { 17 | networkDiscovered(net); 18 | } 19 | // scan for networks and add them 20 | scanThread = 21 | std::thread([this]() { nm.scanWifiNetworks([this](const WifiNetwork& net) { networkDiscovered(net); }, 5); }); 22 | } 23 | 24 | void WifiFlow::networkDiscovered(const WifiNetwork& network) { 25 | if (!networkSelector) 26 | return; 27 | 28 | std::string display = network.ssid + " (" + std::to_string(network.strength) + "%)"; 29 | if (auto* existing = networkSelector->findChoiceById(network.ssid)) { 30 | if (network.strength > existing->strength) { 31 | existing->strength = network.strength; 32 | existing->display = display; 33 | } 34 | } else { 35 | networkSelector->add(Choice{network.ssid, display, false, network.strength}); 36 | } 37 | } 38 | 39 | Frame* WifiFlow::getCurrentFrame() { 40 | switch (currentState) { 41 | case State::SELECT_NETWORK: 42 | return networkSelector.get(); 43 | case State::ENTER_PASSWORD: 44 | return passwordInput.get(); 45 | case State::CONNECTIING: 46 | return connectingFrame.get(); 47 | default: 48 | return nullptr; 49 | } 50 | } 51 | 52 | bool WifiFlow::handleResult(const FrameResult& result) { 53 | switch (currentState) { 54 | case State::SELECT_NETWORK: 55 | if (result.action == FrameResult::Action::SUBMIT) { 56 | selectedNetwork = result.value; 57 | // Create password input with network name in hint 58 | passwordInput = std::make_unique("Passphrase for " + selectedNetwork, true); 59 | currentState = State::ENTER_PASSWORD; 60 | return true; 61 | } else if (result.action == FrameResult::Action::CANCEL) { 62 | done = true; 63 | nm.stopScanning(); 64 | return false; 65 | } 66 | break; 67 | 68 | case State::ENTER_PASSWORD: 69 | if (result.action == FrameResult::Action::SUBMIT) { 70 | password = result.value; 71 | 72 | connectingFrame = 73 | std::make_unique("Connecting to " + selectedNetwork + "...", ImVec4(0.7f, 0.7f, 1.0f, 1.0f)); 74 | currentState = State::CONNECTIING; 75 | 76 | nm.connectToNetwork(selectedNetwork, password, [this](ConnectionState state, const std::string& message) { 77 | if (!connectingFrame) 78 | return; 79 | switch (state) { 80 | case ConnectionState::ACTIVATING: 81 | case ConnectionState::AUTHENTICATING: 82 | case ConnectionState::CONFIGURING: 83 | connectingFrame->setText(message, ImVec4(0.7f, 0.7f, 1.0f, 1.0f)); 84 | break; 85 | case ConnectionState::ACTIVATED: 86 | connectingFrame->setText(message, ImVec4(0.0f, 1.0f, 0.0f, 1.0f)); 87 | done = true; 88 | connectingFrame->done(); // HACK: how else to exit frame? 89 | break; 90 | case ConnectionState::DISCONNECTED: 91 | case ConnectionState::FAILED: 92 | case ConnectionState::UNKNOWN: 93 | connectingFrame->setText(message, ImVec4(1.0f, 0.0f, 0.0f, 1.0f)); 94 | done = true; 95 | break; 96 | } 97 | }); 98 | 99 | return true; 100 | } else if (result.action == FrameResult::Action::CANCEL) { 101 | // Go back to network selection 102 | currentState = State::SELECT_NETWORK; 103 | return true; 104 | } 105 | break; 106 | case State::CONNECTIING: 107 | if (result.action == FrameResult::Action::CANCEL) { 108 | done = true; 109 | return false; 110 | } 111 | break; 112 | } 113 | 114 | return true; 115 | } 116 | 117 | bool WifiFlow::isDone() const { return done; } 118 | 119 | std::string WifiFlow::getResult() const { 120 | if (!selectedNetwork.empty() && !password.empty()) { 121 | return selectedNetwork + ":" + password; 122 | } 123 | return ""; 124 | } 125 | 126 | std::string WifiFlow::getSelectedNetwork() const { return selectedNetwork; } 127 | 128 | std::string WifiFlow::getPassword() const { return password; } 129 | -------------------------------------------------------------------------------- /src/frames/selector.cpp: -------------------------------------------------------------------------------- 1 | #include "selector.hpp" 2 | #include "../flows/flow.hpp" 3 | #include "../input.hpp" 4 | #include "imgui_internal.h" 5 | #include 6 | 7 | bool Selector::RoundedSelectableFullWidth(const char* label, bool selected, float rounding) { 8 | ImGuiWindow* window = ImGui::GetCurrentWindow(); 9 | if (window->SkipItems) 10 | return false; 11 | 12 | ImVec2 pos = ImGui::GetCursorScreenPos(); 13 | ImVec2 padding = ImGui::GetStyle().FramePadding; 14 | ImVec2 labelSize = ImGui::CalcTextSize(label); 15 | 16 | // use the available content width instead of just text width 17 | float availableWidth = ImGui::GetContentRegionAvail().x; 18 | float minWidth = labelSize.x + padding.x * 2; 19 | float fullWidth = std::max(availableWidth, minWidth); 20 | 21 | ImVec2 size = ImVec2(fullWidth, labelSize.y + padding.y * 2); 22 | 23 | // create an invisible button first to handle interaction 24 | bool clicked = ImGui::InvisibleButton(label, size); 25 | bool hovered = ImGui::IsItemHovered(); 26 | 27 | // draw the background if hovered or selected 28 | if (hovered || selected) { 29 | ImU32 color = ImGui::GetColorU32(selected && hovered ? hoverColor : selected ? activeColor : hoverColor); 30 | ImGui::GetWindowDrawList()->AddRectFilled(pos, ImVec2(pos.x + size.x, pos.y + size.y), color, rounding); 31 | } 32 | 33 | // draw the text centered vertically, left-aligned horizontally 34 | ImVec2 textPos = ImVec2(pos.x + padding.x, pos.y + (size.y - labelSize.y) * 0.5f); 35 | ImGui::GetWindowDrawList()->AddText(textPos, ImGui::GetColorU32(ImGuiCol_Text), label); 36 | 37 | return clicked; 38 | } 39 | 40 | FrameResult Selector::render() { 41 | 42 | // lock for streaming stdin 43 | std::lock_guard lock(Input::mutex); 44 | 45 | // Pre-calculate desired size based on content 46 | if (choices.size() > 0) { 47 | float maxTextWidth = 0; 48 | float totalHeight = 0; 49 | ImGuiStyle& style = ImGui::GetStyle(); 50 | ImVec2 framePadding = style.FramePadding; 51 | ImVec2 windowPadding = style.WindowPadding; 52 | ImVec2 itemSpacing = style.ItemSpacing; 53 | 54 | for (const auto& choice : choices) { 55 | ImVec2 textSize = ImGui::CalcTextSize(choice.display.c_str()); 56 | maxTextWidth = std::max(maxTextWidth, textSize.x); 57 | // Each item: text height + frame padding + item spacing 58 | totalHeight += textSize.y + framePadding.y * 2; 59 | } 60 | 61 | // Add spacing between items (n-1 spacings for n items) 62 | if (choices.size() > 1) { 63 | totalHeight += itemSpacing.y * (choices.size() - 1); 64 | } 65 | 66 | // Add window padding (top and bottom) and some extra margin 67 | float desiredWidth = maxTextWidth + framePadding.x * 2 + windowPadding.x * 2; 68 | float desiredHeight = totalHeight + windowPadding.y * 2; 69 | 70 | lastSize = ImVec2(desiredWidth, desiredHeight); 71 | } 72 | 73 | // Set the window to fill the entire display 74 | ImGui::SetNextWindowSize(ImGui::GetIO().DisplaySize); 75 | ImGui::SetNextWindowPos(ImVec2(0, 0)); 76 | 77 | ImGui::Begin("Select", 78 | nullptr, 79 | ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoSavedSettings | 80 | ImGuiWindowFlags_NoResize); 81 | 82 | int clicked = -1; 83 | 84 | if (choices.size() == 0) { 85 | ImGui::Text("Loading..."); 86 | lastSize = ImVec2(200, 50); // Fallback size for loading 87 | } else { 88 | for (int i = 0; i < choices.size(); i++) { 89 | bool isSelected = (selected == i); 90 | if (RoundedSelectableFullWidth(choices[i].display.c_str(), isSelected)) { 91 | selected = i; 92 | clicked = i; 93 | } 94 | } 95 | // Update with actual rendered size 96 | ImVec2 actualSize = ImGui::GetWindowSize(); 97 | 98 | if (actualSize.x > lastSize.x * 0.8f && actualSize.y > lastSize.y * 0.8f) { 99 | lastSize = actualSize; 100 | } 101 | } 102 | 103 | bool escPressed = ImGui::IsKeyPressed(ImGuiKey_Escape); 104 | 105 | ImGui::End(); 106 | 107 | // if an item was clicked, return its id 108 | if (clicked >= 0) { 109 | return FrameResult::Submit(choices[clicked].id); 110 | } 111 | 112 | // if esc was pressed, cancel 113 | if (escPressed) { 114 | return FrameResult::Cancel(); 115 | } 116 | 117 | return FrameResult::Continue(); 118 | } 119 | 120 | void Selector::applyTheme(const Config& config) { 121 | hoverColor = config.getColor("theme", "hover_color", "#3366B3FF"); 122 | activeColor = config.getColor("theme", "active_color", "#3366B366"); 123 | } 124 | -------------------------------------------------------------------------------- /src/frames/custom.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "../config.hpp" 4 | #include "../ui.hpp" 5 | #include "imgui.h" 6 | #include 7 | #include 8 | #include 9 | 10 | namespace YAML { 11 | class Node; 12 | } 13 | 14 | class CustomFrame : public Frame { 15 | public: 16 | CustomFrame(const std::string& configPath); 17 | ~CustomFrame() override = default; 18 | 19 | FrameResult render() override; 20 | Vec2 getSize() override; 21 | void applyTheme(const Config& config) override; 22 | 23 | private: 24 | // action types that can be triggered 25 | enum class ActionType { 26 | Execute, // execute shell command 27 | Submit, // return value 28 | Cancel, // close/cancel 29 | SubMenu, // navigate to submenu 30 | Back, // go back to parent menu 31 | SetState // internal state change 32 | }; 33 | 34 | struct Action { 35 | ActionType type; 36 | std::string command; // Execute 37 | std::string value; // Submit 38 | std::string submenuPath; // SubMenu 39 | bool closeOnSuccess = true; // Execute 40 | 41 | enum TriggerType { OnClick, OnChange, OnRelease } trigger = OnClick; 42 | }; 43 | 44 | struct Widget { 45 | std::string id; 46 | virtual ~Widget() = default; 47 | virtual FrameResult render(CustomFrame& frame) = 0; 48 | virtual float getHeight() const = 0; 49 | }; 50 | 51 | // widget implementations 52 | struct TextWidget : Widget { 53 | std::string content; 54 | enum Style { Normal, Bold, Italic } style = Normal; 55 | FrameResult render(CustomFrame& frame) override; 56 | float getHeight() const override; 57 | }; 58 | 59 | struct SeparatorWidget : Widget { 60 | FrameResult render(CustomFrame& frame) override; 61 | float getHeight() const override; 62 | }; 63 | 64 | struct ButtonWidget : Widget { 65 | std::string label; 66 | Action action; 67 | FrameResult render(CustomFrame& frame) override; 68 | float getHeight() const override; 69 | }; 70 | 71 | struct SelectableItem { 72 | std::string id; 73 | std::string label; 74 | bool selected; 75 | Action action; 76 | }; 77 | 78 | struct SelectableListWidget : Widget { 79 | std::vector items; 80 | int selectedIndex = -1; 81 | FrameResult render(CustomFrame& frame) override; 82 | float getHeight() const override; 83 | }; 84 | 85 | struct InputWidget : Widget { 86 | std::string hint; 87 | std::string value; 88 | bool password = false; 89 | Action action; 90 | char buffer[256] = {0}; 91 | FrameResult render(CustomFrame& frame) override; 92 | float getHeight() const override; 93 | }; 94 | 95 | struct CheckboxWidget : Widget { 96 | std::string label; 97 | bool checked = false; 98 | Action action; 99 | FrameResult render(CustomFrame& frame) override; 100 | float getHeight() const override; 101 | }; 102 | 103 | struct SliderWidget : Widget { 104 | std::string label; 105 | float value = 0.0f; 106 | float minValue = 0.0f; 107 | float maxValue = 100.0f; 108 | Action action; 109 | FrameResult render(CustomFrame& frame) override; 110 | float getHeight() const override; 111 | }; 112 | 113 | struct ComboWidget : Widget { 114 | std::string label; 115 | std::vector items; 116 | int currentIndex = 0; 117 | Action action; 118 | FrameResult render(CustomFrame& frame) override; 119 | float getHeight() const override; 120 | }; 121 | 122 | struct ColorPickerWidget : Widget { 123 | std::string label; 124 | float color[3] = {0.2f, 0.4f, 0.7f}; 125 | Action action; 126 | FrameResult render(CustomFrame& frame) override; 127 | float getHeight() const override; 128 | }; 129 | 130 | bool loadConfig(const std::string& path); 131 | std::unique_ptr parseWidget(const YAML::Node& node); 132 | Action parseAction(const YAML::Node& node); 133 | FrameResult executeAction(const Action& action, const std::string& value = ""); 134 | std::string replaceTokens(const std::string& str, const std::string& value); 135 | 136 | std::vector> widgets; 137 | std::string title; 138 | float fixedWidth = 0.0f; 139 | float fixedHeight = 0.0f; 140 | ImVec2 lastSize = ImVec2(400, 300); 141 | 142 | // theme colors 143 | ImVec4 hoverColor; 144 | ImVec4 activeColor; 145 | 146 | // keyboard shortcuts 147 | struct Shortcut { 148 | ImGuiKey key; 149 | bool ctrl = false; 150 | bool shift = false; 151 | bool alt = false; 152 | Action action; 153 | }; 154 | std::vector shortcuts; 155 | 156 | bool firstFrame = true; 157 | }; 158 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.15) 2 | project(hyprwat CXX C) 3 | 4 | set(CMAKE_CXX_STANDARD 20) 5 | set(CMAKE_CXX_STANDARD_REQUIRED ON) 6 | 7 | find_package(PkgConfig REQUIRED) 8 | 9 | # Find Wayland 10 | find_package(Wayland QUIET COMPONENTS Client Egl) 11 | if(NOT Wayland_FOUND) 12 | # Fallback to pkg-config if Wayland CMake package is not found 13 | pkg_check_modules(WAYLAND REQUIRED IMPORTED_TARGET wayland-client wayland-egl) 14 | endif() 15 | 16 | find_package(OpenGL REQUIRED COMPONENTS OpenGL EGL) 17 | 18 | # Fontconfig 19 | pkg_check_modules(Fontconfig REQUIRED fontconfig) 20 | 21 | # xkbcommon 22 | pkg_check_modules(XKBCOMMON REQUIRED IMPORTED_TARGET xkbcommon) 23 | 24 | # sdbus 25 | pkg_check_modules(SDBUSCPP REQUIRED sdbus-c++) 26 | 27 | # pipewire 28 | pkg_check_modules(PIPEWIRE REQUIRED libpipewire-0.3) 29 | 30 | 31 | # ImGui sources 32 | set(IMGUI_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ext/imgui") 33 | set(IMGUI_SOURCES 34 | ${IMGUI_DIR}/imgui.cpp 35 | ${IMGUI_DIR}/imgui_demo.cpp 36 | ${IMGUI_DIR}/imgui_draw.cpp 37 | ${IMGUI_DIR}/imgui_tables.cpp 38 | ${IMGUI_DIR}/imgui_widgets.cpp 39 | ${IMGUI_DIR}/backends/imgui_impl_opengl3.cpp 40 | ) 41 | 42 | # Wayland protocol sources 43 | set(WAYLAND_PROTOCOLS 44 | src/wayland/protocols/wlr-layer-shell-unstable-v1-client-protocol.c 45 | src/wayland/protocols/xdg-shell-client-protocol.c 46 | ) 47 | 48 | # INIH 49 | include(FetchContent) 50 | FetchContent_Declare( 51 | inih 52 | GIT_REPOSITORY https://github.com/benhoyt/inih.git 53 | GIT_TAG r58 54 | ) 55 | FetchContent_MakeAvailable(inih) 56 | 57 | # stb_image 58 | FetchContent_Declare( 59 | stb 60 | GIT_REPOSITORY https://github.com/nothings/stb.git 61 | GIT_TAG master 62 | ) 63 | FetchContent_MakeAvailable(stb) 64 | 65 | 66 | add_library(inih STATIC 67 | ${inih_SOURCE_DIR}/ini.c 68 | ${inih_SOURCE_DIR}/cpp/INIReader.cpp 69 | ) 70 | target_include_directories(inih PUBLIC 71 | ${XKBCOMMON_INCLUDE_DIRS} 72 | ${inih_SOURCE_DIR} 73 | ${PIPEWIRE_INCLUDE_DIRS} 74 | ${SDBUSCPP_INCLUDE_DIRS} 75 | ${inih_SOURCE_DIR}/cpp 76 | ${stb_SOURCE_DIR} 77 | ) 78 | 79 | # yaml-cpp 80 | FetchContent_Declare( 81 | yaml-cpp 82 | GIT_REPOSITORY https://github.com/jbeder/yaml-cpp.git 83 | GIT_TAG master 84 | ) 85 | 86 | set(YAML_CPP_BUILD_TESTS OFF CACHE BOOL "" FORCE) 87 | set(YAML_CPP_BUILD_TOOLS OFF CACHE BOOL "" FORCE) 88 | set(YAML_CPP_BUILD_CONTRIB OFF CACHE BOOL "" FORCE) 89 | set(YAML_CPP_INSTALL OFF CACHE BOOL "" FORCE) 90 | 91 | FetchContent_MakeAvailable(yaml-cpp) 92 | 93 | 94 | # Main executable 95 | add_executable(hyprwat 96 | src/main.cpp 97 | src/input.cpp 98 | src/ui.cpp 99 | src/util.cpp 100 | src/hyprland/ipc.cpp 101 | src/wayland/wayland.cpp 102 | src/wayland/display.cpp 103 | src/wayland/layer_surface.cpp 104 | src/wayland/input.cpp 105 | src/renderer/egl_context.cpp 106 | src/font/font.cpp 107 | src/frames/selector.cpp 108 | src/frames/input.cpp 109 | src/frames/text.cpp 110 | src/frames/custom.cpp 111 | src/frames/images.cpp 112 | src/flows/simple_flows.cpp 113 | src/flows/wifi_flow.cpp 114 | src/flows/audio_flow.cpp 115 | src/flows/custom_flow.cpp 116 | src/flows/wallpaper_flow.cpp 117 | src/net/network_manager.cpp 118 | src/audio/audio.cpp 119 | src/wallpaper/thumbnail.cpp 120 | src/wallpaper/wallpaper.cpp 121 | ${IMGUI_SOURCES} 122 | ${WAYLAND_PROTOCOLS} 123 | ) 124 | 125 | target_compile_options(hyprwat PRIVATE 126 | ${PIPEWIRE_CFLAGS_OTHER} 127 | ${SDBUS_CPP_CFLAGS_OTHER} 128 | ) 129 | 130 | 131 | # Include directories 132 | target_include_directories(hyprwat PRIVATE 133 | ${CMAKE_CURRENT_SOURCE_DIR} 134 | ${IMGUI_DIR} 135 | ${IMGUI_DIR}/backends 136 | ) 137 | 138 | if(Wayland_FOUND) 139 | target_link_libraries(hyprwat PRIVATE 140 | Wayland::Client 141 | Wayland::Egl 142 | ) 143 | else() 144 | target_link_libraries(hyprwat PRIVATE 145 | PkgConfig::WAYLAND 146 | ) 147 | endif() 148 | 149 | target_link_libraries(hyprwat PRIVATE 150 | OpenGL::OpenGL 151 | OpenGL::EGL 152 | ${Fontconfig_LIBRARIES} 153 | PkgConfig::XKBCOMMON 154 | ${PIPEWIRE_LIBRARIES} 155 | ${SDBUSCPP_LIBRARIES} 156 | inih 157 | yaml-cpp 158 | ${CMAKE_DL_LIBS} 159 | m 160 | pthread 161 | ) 162 | 163 | install(TARGETS hyprwat 164 | RUNTIME DESTINATION bin) 165 | 166 | install(FILES man/hyprwat.6 167 | DESTINATION share/man/man6) 168 | 169 | # generate .deb .rpm and .tgz 170 | include(InstallRequiredSystemLibraries) 171 | 172 | set(CPACK_PACKAGE_NAME "hyprwat") 173 | set(CPACK_PACKAGE_VERSION "0.9.2") 174 | set(CPACK_PACKAGE_CONTACT "zack@bartel.com") 175 | set(CPACK_GENERATOR "DEB;RPM;TGZ") 176 | 177 | set(CPACK_DEBIAN_PACKAGE_MAINTAINER "Zack Bartel") 178 | set(CPACK_RPM_PACKAGE_RELEASE 1) 179 | 180 | # deb and rpm dependencies 181 | set(CPACK_DEBIAN_PACKAGE_DEPENDS 182 | "libwayland-client0, wayland-protocols, libegl1-mesa, libgl1-mesa-glx, libfontconfig1, libxkbcommon0, libpipewire-0.3-0, libsdbus-c++-1") 183 | set(CPACK_RPM_PACKAGE_REQUIRES 184 | "wayland-libs, wayland-protocols, mesa-libEGL, mesa-libGL, fontconfig, libxkbcommon, pipewire, sdbus-c++") 185 | 186 | 187 | # remove the "-Linux" suffix 188 | set(CPACK_PACKAGE_FILE_NAME "${CPACK_PACKAGE_NAME}-${CPACK_PACKAGE_VERSION}") 189 | 190 | include(CPack) 191 | 192 | 193 | -------------------------------------------------------------------------------- /src/hyprland/ipc.cpp: -------------------------------------------------------------------------------- 1 | #include "ipc.hpp" 2 | #include "../debug/log.hpp" 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | namespace hyprland { 11 | 12 | static std::string getSocketPath(const char* filename) { 13 | const char* runtime = std::getenv("XDG_RUNTIME_DIR"); 14 | const char* sig = std::getenv("HYPRLAND_INSTANCE_SIGNATURE"); 15 | 16 | if (!runtime || !sig) { 17 | throw std::runtime_error("Not running inside Hyprland (env vars missing)"); 18 | } 19 | return std::string(runtime) + "/hypr/" + sig + "/" + filename; 20 | } 21 | 22 | // Control 23 | Control::Control() : Control(getSocketPath(".socket.sock")) {} 24 | Control::Control(const std::string& socketPath) : socketPath(socketPath) {} 25 | 26 | Control::~Control() {} 27 | 28 | std::string Control::send(const std::string& command) { 29 | int wfd = socket(AF_UNIX, SOCK_STREAM, 0); 30 | if (wfd < 0) 31 | throw std::runtime_error("Failed to create socket"); 32 | 33 | sockaddr_un addr{}; 34 | addr.sun_family = AF_UNIX; 35 | std::strncpy(addr.sun_path, socketPath.c_str(), sizeof(addr.sun_path) - 1); 36 | 37 | if (connect(wfd, reinterpret_cast(&addr), sizeof(addr)) < 0) { 38 | close(wfd); 39 | throw std::runtime_error("Failed to connect to control socket"); 40 | } 41 | 42 | // send command 43 | write(wfd, command.c_str(), command.size()); 44 | 45 | // read response 46 | char buf[4096]; 47 | ssize_t n = read(wfd, buf, sizeof(buf) - 1); 48 | if (n < 0) { 49 | close(wfd); 50 | throw std::runtime_error("Failed to read response"); 51 | } 52 | buf[n] = '\0'; 53 | 54 | close(wfd); 55 | return std::string(buf); 56 | } 57 | 58 | Vec2 Control::cursorPos() { 59 | std::string response = send("cursorpos"); 60 | int x = 0, y = 0; 61 | if (sscanf(response.c_str(), "%d, %d", &x, &y) != 2) { 62 | throw std::runtime_error("Failed to parse cursor position"); 63 | } 64 | return {(float)x, (float)y}; 65 | } 66 | 67 | float Control::scale() { 68 | std::string response = send("monitors"); 69 | 70 | // "scale: " followed by a number 71 | size_t pos = response.find("scale: "); 72 | if (pos != std::string::npos) { 73 | float scale; 74 | if (sscanf(response.c_str() + pos + 7, "%f", &scale) == 1) { 75 | return scale; 76 | } 77 | } 78 | 79 | return 1.0f; // fallback 80 | } 81 | 82 | void Control::setWallpaper(const std::string& path) { 83 | std::string response = send("/keyword exec hyprctl hyprpaper preload \"" + path + "\""); 84 | if (response != "ok") { 85 | debug::log(ERR, "Failed to preload wallpaper: {}", response); 86 | } 87 | response = send("/keyword exec hyprctl hyprpaper wallpaper \"," + path + "\""); 88 | if (response != "ok") { 89 | debug::log(ERR, "Failed to set wallpaper: {}", response); 90 | } 91 | send("/keyword exec hyprctl hyprpaper unload unused"); 92 | } 93 | 94 | // Events 95 | 96 | Events::Events() : Events(getSocketPath(".socket2.sock")) {} 97 | 98 | Events::Events(const std::string& socketPath) : socketPath(socketPath) {} 99 | 100 | Events::~Events() { stop(); } 101 | 102 | void Events::start(EventCallback cb) { 103 | if (running) 104 | return; 105 | running = true; 106 | thread = std::thread(&Events::run, this, cb); 107 | } 108 | 109 | void Events::stop() { 110 | if (!running) { 111 | return; 112 | } 113 | running = false; 114 | 115 | { 116 | std::lock_guard lock(mtx); 117 | if (fd != -1) { 118 | shutdown(fd, SHUT_RD); 119 | fd = -1; 120 | } 121 | } 122 | 123 | if (thread.joinable()) { 124 | thread.join(); 125 | } 126 | } 127 | 128 | void Events::run(EventCallback cb) { 129 | int localFd = socket(AF_UNIX, SOCK_STREAM, 0); 130 | if (localFd < 0) { 131 | debug::log(ERR, "Failed to create event socket"); 132 | return; 133 | } 134 | 135 | sockaddr_un addr{}; 136 | addr.sun_family = AF_UNIX; 137 | std::strncpy(addr.sun_path, socketPath.c_str(), sizeof(addr.sun_path) - 1); 138 | 139 | if (connect(localFd, reinterpret_cast(&addr), sizeof(addr)) < 0) { 140 | debug::log(ERR, "Failed to connect to event socket: {}", std::strerror(errno)); 141 | close(fd); 142 | return; 143 | } 144 | 145 | { 146 | std::lock_guard lock(mtx); 147 | fd = localFd; 148 | } 149 | 150 | char buf[1024]; 151 | std::string line; 152 | 153 | while (running) { 154 | ssize_t n = read(fd, buf, sizeof(buf)); 155 | if (n <= 0) 156 | break; // socket closed or error 157 | line.append(buf, n); 158 | 159 | // simple line splitting 160 | size_t pos; 161 | while ((pos = line.find('\n')) != std::string::npos) { 162 | std::string event = line.substr(0, pos); 163 | line.erase(0, pos + 1); 164 | if (!event.empty()) 165 | cb(event); 166 | } 167 | } 168 | 169 | { 170 | std::lock_guard lock(mtx); 171 | if (fd != -1) { 172 | close(fd); 173 | } 174 | } 175 | } 176 | 177 | } // namespace hyprland 178 | -------------------------------------------------------------------------------- /src/main.cpp: -------------------------------------------------------------------------------- 1 | #include "flows/audio_flow.hpp" 2 | #include "flows/custom_flow.hpp" 3 | #include "flows/flow.hpp" 4 | #include "flows/simple_flows.hpp" 5 | #include "flows/wallpaper_flow.hpp" 6 | #include "flows/wifi_flow.hpp" 7 | #include "hyprland/ipc.hpp" 8 | #include "input.hpp" 9 | #include "ui.hpp" 10 | #include "wayland/wayland.hpp" 11 | 12 | #include 13 | #include 14 | #include 15 | 16 | void usage() { 17 | fprintf(stderr, R"(Usage: 18 | hyprwat [OPTIONS] [id[:displayName][*]]... 19 | hyprwat --input [hint] 20 | hyprwat --password [hint] 21 | hyprwat --wifi 22 | hyprwat --audio 23 | hyprwat --wallpaper 24 | 25 | Description: 26 | A simple Wayland panel to present selectable options or text input, connect to wifi networks, update audio sinks/sources, and more. 27 | 28 | MENU MODE (default): 29 | You can pass a list of items directly as command-line arguments, where each 30 | item is a tuple in the form: 31 | 32 | id[:displayName][*] 33 | 34 | - `id` : Required identifier string (used internally) 35 | - `displayName` : Optional label to show in the UI (defaults to id) 36 | - `*` : Optional suffix to mark this item as initially selected 37 | 38 | Examples: 39 | hyprwat performance:Performance* balanced:Balanced powersave:PowerSaver 40 | hyprwat wifi0:Home wifi1:Work wifi2:Other 41 | 42 | Alternatively, if no arguments are passed, options can be provided via stdin: 43 | 44 | echo "wifi0:Home*" | hyprwat 45 | echo -e "wifi0:Home*\nwifi1:Work\nwifi2:Other" | hyprwat 46 | 47 | INPUT MODE: 48 | Use --input to show a text input field instead of a menu. 49 | 50 | Examples: 51 | hyprwat --input passphrase 52 | hyprwat --input "Enter your name" 53 | 54 | WIFI MODE: 55 | Use --wifi to show available WiFi networks, select one, and enter the password if required. 56 | 57 | AUDIO MODE: 58 | Use --audio to show available audio input/output devices and select one. 59 | 60 | CUSTOM MODE: 61 | Use --custom to render a fully custom menu from a YAML configuration file. 62 | 63 | WALLPAPER MODE: 64 | Use --wallpaper to select wallpapers from a specified directory and set them using hyprpaper. 65 | 66 | Options: 67 | -h, --help Show this help message 68 | --input [hint] Show text input mode with optional hint text 69 | --password [hint] Show password input mode with optional hint text 70 | --wifi Show WiFi network selection mode 71 | --audio Show audio input/output device selection mode 72 | --custom Load a custom flow from the specified configuration file 73 | --wallpaper Select wallpapers from the specified directory and set using hyprpaper 74 | )"); 75 | } 76 | 77 | int main(const int argc, const char** argv) { 78 | 79 | // check for help flag 80 | if (argc == 2 && !strncmp(argv[1], "--help", strlen(argv[1]))) { 81 | usage(); 82 | return 1; 83 | } 84 | 85 | // initialize Wayland connection 86 | wl::Wayland wayland; 87 | 88 | // setup ui with Wayland 89 | UI ui(wayland); 90 | 91 | // find cursor position for meny x/y 92 | hyprland::Control hyprctl; 93 | Vec2 pos = hyprctl.cursorPos(); 94 | 95 | // deal with hyprland fractional scaling vs wayland integer scaling 96 | float hyprlandScale = hyprctl.scale(); 97 | int waylandScale = wayland.display().getMaxScale(); 98 | auto [displayWidth, displayHeight] = wayland.display().getOutputSize(); 99 | 100 | // convert hyprland logical->physical->wayland logical 101 | int x_physical = (int)(pos.x * hyprlandScale); 102 | int y_physical = (int)(pos.y * hyprlandScale); 103 | int x_wayland = x_physical / waylandScale; 104 | int y_wayland = y_physical / waylandScale; 105 | int logicalDisplayWidth = displayWidth / hyprlandScale; 106 | int logicalDisplayHeight = displayHeight / hyprlandScale; 107 | 108 | // load config 109 | Config config("~/.config/hyprwat/hyprwat.conf"); 110 | 111 | // initialize UI at wayland scaled cursor position 112 | ui.init(pos.x, pos.y, hyprlandScale); 113 | 114 | // apply theme to UI 115 | ui.applyTheme(config); 116 | 117 | // parse command line arguments 118 | auto args = Input::parseArgv(argc, argv); 119 | 120 | // find which flow to run 121 | std::unique_ptr flow; 122 | 123 | // INPUT or PASSWORD mode 124 | switch (args.mode) { 125 | case InputMode::INPUT: 126 | flow = std::make_unique(args.hint, false); 127 | break; 128 | case InputMode::PASSWORD: 129 | flow = std::make_unique(args.hint, true); 130 | break; 131 | case InputMode::WIFI: { 132 | auto wifiFlow = std::make_unique(); 133 | wifiFlow->start(); 134 | flow = std::move(wifiFlow); 135 | break; 136 | } 137 | case InputMode::AUDIO: 138 | flow = std::make_unique(); 139 | break; 140 | case InputMode::CUSTOM: 141 | flow = std::make_unique(args.configPath); 142 | break; 143 | case InputMode::WALLPAPER: 144 | flow = std::make_unique(hyprctl, args.wallpaperDir, logicalDisplayWidth, logicalDisplayHeight); 145 | break; 146 | case InputMode::MENU: 147 | if (args.choices.size() > 0) { 148 | // use choices from argv 149 | flow = std::make_unique(args.choices); 150 | } else { 151 | // parse stdin asynchronously for choices 152 | flow = std::make_unique(); 153 | MenuFlow* menuFlow = static_cast(flow.get()); 154 | Input::parseStdin([menuFlow](Choice choice) { menuFlow->addChoice(choice); }); 155 | } 156 | break; 157 | } 158 | 159 | // run the flow 160 | ui.runFlow(*flow); 161 | 162 | // print result if flow completed successfully 163 | std::string result = flow->getResult(); 164 | if (!result.empty()) { 165 | std::cout << result << std::endl; 166 | std::cout.flush(); 167 | } 168 | 169 | return 0; 170 | } 171 | -------------------------------------------------------------------------------- /src/wayland/display.cpp: -------------------------------------------------------------------------------- 1 | #include "display.hpp" 2 | #include "../debug/log.hpp" 3 | #include 4 | #include 5 | 6 | namespace wl { 7 | Display::Display() {} 8 | 9 | Display::~Display() { 10 | for (auto& output : outputs_) { 11 | if (output.output) 12 | wl_output_destroy(output.output); 13 | } 14 | if (seat_) 15 | wl_seat_destroy(seat_); 16 | if (layerShell_) 17 | zwlr_layer_shell_v1_destroy(layerShell_); 18 | if (compositor_) 19 | wl_compositor_destroy(compositor_); 20 | if (registry) 21 | wl_registry_destroy(registry); 22 | if (display_) 23 | wl_display_disconnect(display_); 24 | } 25 | 26 | bool Display::connect() { 27 | display_ = wl_display_connect(nullptr); 28 | if (!display_) { 29 | debug::log(ERR, "Failed to connect to Wayland display"); 30 | return false; 31 | } 32 | 33 | registry = wl_display_get_registry(display_); 34 | wl_registry_listener listener = {.global = registryHandler, .global_remove = registryRemover}; 35 | wl_registry_add_listener(registry, &listener, this); 36 | wl_display_roundtrip(display_); 37 | 38 | if (!compositor_ || !layerShell_) { 39 | debug::log(ERR, "Compositor or layer shell not available"); 40 | return false; 41 | } 42 | 43 | return true; 44 | } 45 | 46 | void Display::dispatch() { wl_display_dispatch(display_); } 47 | 48 | void Display::dispatchPending() { wl_display_dispatch_pending(display_); } 49 | 50 | void Display::roundtrip() { wl_display_roundtrip(display_); } 51 | 52 | void Display::prepareRead() { 53 | while (wl_display_prepare_read(display_) != 0) { 54 | wl_display_dispatch_pending(display_); 55 | } 56 | } 57 | 58 | void Display::readEvents() { wl_display_read_events(display_); } 59 | 60 | void Display::flush() { wl_display_flush(display_); } 61 | 62 | void Display::registryHandler( 63 | void* data, wl_registry* registry, uint32_t id, const char* interface, uint32_t version) { 64 | Display* self = static_cast(data); 65 | 66 | if (strcmp(interface, wl_compositor_interface.name) == 0) { 67 | self->compositor_ = 68 | static_cast(wl_registry_bind(registry, id, &wl_compositor_interface, 4)); 69 | } else if (strcmp(interface, zwlr_layer_shell_v1_interface.name) == 0) { 70 | self->layerShell_ = 71 | static_cast(wl_registry_bind(registry, id, &zwlr_layer_shell_v1_interface, 1)); 72 | } else if (strcmp(interface, wl_seat_interface.name) == 0) { 73 | self->seat_ = static_cast(wl_registry_bind(registry, id, &wl_seat_interface, 5)); 74 | } else if (strcmp(interface, wl_output_interface.name) == 0) { 75 | wl_output* output = static_cast(wl_registry_bind(registry, id, &wl_output_interface, 4)); 76 | 77 | static const wl_output_listener output_listener = {.geometry = outputGeometry, 78 | .mode = outputMode, 79 | .done = outputDone, 80 | .scale = outputScale, 81 | .name = outputName, 82 | .description = outputDescription}; 83 | wl_output_add_listener(output, &output_listener, self); 84 | 85 | self->outputs_.push_back({output, 1, id, 0, 0}); 86 | } 87 | } 88 | 89 | int32_t Display::getMaxScale() const { 90 | int32_t max = 1; 91 | for (const auto& output : outputs_) { 92 | if (output.scale > max) { 93 | max = output.scale; 94 | } 95 | } 96 | return max; 97 | } 98 | 99 | std::pair Display::getOutputSize() const { 100 | if (outputs_.empty()) { 101 | return {1920, 1080}; // fallback? 102 | } 103 | // TODO: how do we handle multiple outputs with different sizes? 104 | return {outputs_[0].width, outputs_[0].height}; 105 | } 106 | 107 | void Display::registryRemover(void* data, wl_registry*, uint32_t id) { 108 | Display* self = static_cast(data); 109 | 110 | // Remove output if it was removed 111 | auto it = std::find_if( 112 | self->outputs_.begin(), self->outputs_.end(), [id](const Output& output) { return output.id == id; }); 113 | if (it != self->outputs_.end()) { 114 | if (it->output) 115 | wl_output_destroy(it->output); 116 | self->outputs_.erase(it); 117 | 118 | // Notify about scale change 119 | if (self->scaleCallback) { 120 | self->scaleCallback(self->getMaxScale()); 121 | } 122 | } 123 | } 124 | 125 | // Output event handlers 126 | void Display::outputGeometry( 127 | void*, wl_output*, int32_t, int32_t, int32_t, int32_t, int32_t, const char*, const char*, int32_t) {} 128 | 129 | void Display::outputMode( 130 | void* data, wl_output* output, uint32_t flags, int32_t width, int32_t height, int32_t refresh) { 131 | if (flags & WL_OUTPUT_MODE_CURRENT) { 132 | Display* self = static_cast(data); 133 | 134 | // update output dimensions 135 | for (auto& out : self->outputs_) { 136 | if (out.output == output) { 137 | out.width = width; 138 | out.height = height; 139 | break; 140 | } 141 | } 142 | } 143 | } 144 | 145 | void Display::outputDone(void*, wl_output*) {} 146 | 147 | void Display::outputScale(void* data, wl_output* output, int32_t factor) { 148 | Display* self = static_cast(data); 149 | 150 | // Find and update the output scale 151 | for (auto& out : self->outputs_) { 152 | if (out.output == output) { 153 | out.scale = factor; 154 | 155 | // Notify about scale change 156 | if (self->scaleCallback) { 157 | self->scaleCallback(self->getMaxScale()); 158 | } 159 | break; 160 | } 161 | } 162 | } 163 | 164 | void Display::outputName(void*, wl_output*, const char*) {} 165 | void Display::outputDescription(void*, wl_output*, const char*) {} 166 | } // namespace wl 167 | -------------------------------------------------------------------------------- /src/wayland/protocols/xdg-shell-client-protocol.c: -------------------------------------------------------------------------------- 1 | /* Generated by wayland-scanner 1.24.0 */ 2 | 3 | /* 4 | * Copyright © 2008-2013 Kristian Høgsberg 5 | * Copyright © 2013 Rafael Antognolli 6 | * Copyright © 2013 Jasper St. Pierre 7 | * Copyright © 2010-2013 Intel Corporation 8 | * Copyright © 2015-2017 Samsung Electronics Co., Ltd 9 | * Copyright © 2015-2017 Red Hat Inc. 10 | * 11 | * Permission is hereby granted, free of charge, to any person obtaining a 12 | * copy of this software and associated documentation files (the "Software"), 13 | * to deal in the Software without restriction, including without limitation 14 | * the rights to use, copy, modify, merge, publish, distribute, sublicense, 15 | * and/or sell copies of the Software, and to permit persons to whom the 16 | * Software is furnished to do so, subject to the following conditions: 17 | * 18 | * The above copyright notice and this permission notice (including the next 19 | * paragraph) shall be included in all copies or substantial portions of the 20 | * Software. 21 | * 22 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 23 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 24 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 25 | * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 26 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 27 | * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 28 | * DEALINGS IN THE SOFTWARE. 29 | */ 30 | 31 | #include 32 | #include 33 | #include 34 | #include "wayland-util.h" 35 | 36 | #ifndef __has_attribute 37 | # define __has_attribute(x) 0 /* Compatibility with non-clang compilers. */ 38 | #endif 39 | 40 | #if (__has_attribute(visibility) || defined(__GNUC__) && __GNUC__ >= 4) 41 | #define WL_PRIVATE __attribute__ ((visibility("hidden"))) 42 | #else 43 | #define WL_PRIVATE 44 | #endif 45 | 46 | extern const struct wl_interface wl_output_interface; 47 | extern const struct wl_interface wl_seat_interface; 48 | extern const struct wl_interface wl_surface_interface; 49 | extern const struct wl_interface xdg_popup_interface; 50 | extern const struct wl_interface xdg_positioner_interface; 51 | extern const struct wl_interface xdg_surface_interface; 52 | extern const struct wl_interface xdg_toplevel_interface; 53 | 54 | static const struct wl_interface *xdg_shell_types[] = { 55 | NULL, 56 | NULL, 57 | NULL, 58 | NULL, 59 | &xdg_positioner_interface, 60 | &xdg_surface_interface, 61 | &wl_surface_interface, 62 | &xdg_toplevel_interface, 63 | &xdg_popup_interface, 64 | &xdg_surface_interface, 65 | &xdg_positioner_interface, 66 | &xdg_toplevel_interface, 67 | &wl_seat_interface, 68 | NULL, 69 | NULL, 70 | NULL, 71 | &wl_seat_interface, 72 | NULL, 73 | &wl_seat_interface, 74 | NULL, 75 | NULL, 76 | &wl_output_interface, 77 | &wl_seat_interface, 78 | NULL, 79 | &xdg_positioner_interface, 80 | NULL, 81 | }; 82 | 83 | static const struct wl_message xdg_wm_base_requests[] = { 84 | { "destroy", "", xdg_shell_types + 0 }, 85 | { "create_positioner", "n", xdg_shell_types + 4 }, 86 | { "get_xdg_surface", "no", xdg_shell_types + 5 }, 87 | { "pong", "u", xdg_shell_types + 0 }, 88 | }; 89 | 90 | static const struct wl_message xdg_wm_base_events[] = { 91 | { "ping", "u", xdg_shell_types + 0 }, 92 | }; 93 | 94 | WL_PRIVATE const struct wl_interface xdg_wm_base_interface = { 95 | "xdg_wm_base", 7, 96 | 4, xdg_wm_base_requests, 97 | 1, xdg_wm_base_events, 98 | }; 99 | 100 | static const struct wl_message xdg_positioner_requests[] = { 101 | { "destroy", "", xdg_shell_types + 0 }, 102 | { "set_size", "ii", xdg_shell_types + 0 }, 103 | { "set_anchor_rect", "iiii", xdg_shell_types + 0 }, 104 | { "set_anchor", "u", xdg_shell_types + 0 }, 105 | { "set_gravity", "u", xdg_shell_types + 0 }, 106 | { "set_constraint_adjustment", "u", xdg_shell_types + 0 }, 107 | { "set_offset", "ii", xdg_shell_types + 0 }, 108 | { "set_reactive", "3", xdg_shell_types + 0 }, 109 | { "set_parent_size", "3ii", xdg_shell_types + 0 }, 110 | { "set_parent_configure", "3u", xdg_shell_types + 0 }, 111 | }; 112 | 113 | WL_PRIVATE const struct wl_interface xdg_positioner_interface = { 114 | "xdg_positioner", 7, 115 | 10, xdg_positioner_requests, 116 | 0, NULL, 117 | }; 118 | 119 | static const struct wl_message xdg_surface_requests[] = { 120 | { "destroy", "", xdg_shell_types + 0 }, 121 | { "get_toplevel", "n", xdg_shell_types + 7 }, 122 | { "get_popup", "n?oo", xdg_shell_types + 8 }, 123 | { "set_window_geometry", "iiii", xdg_shell_types + 0 }, 124 | { "ack_configure", "u", xdg_shell_types + 0 }, 125 | }; 126 | 127 | static const struct wl_message xdg_surface_events[] = { 128 | { "configure", "u", xdg_shell_types + 0 }, 129 | }; 130 | 131 | WL_PRIVATE const struct wl_interface xdg_surface_interface = { 132 | "xdg_surface", 7, 133 | 5, xdg_surface_requests, 134 | 1, xdg_surface_events, 135 | }; 136 | 137 | static const struct wl_message xdg_toplevel_requests[] = { 138 | { "destroy", "", xdg_shell_types + 0 }, 139 | { "set_parent", "?o", xdg_shell_types + 11 }, 140 | { "set_title", "s", xdg_shell_types + 0 }, 141 | { "set_app_id", "s", xdg_shell_types + 0 }, 142 | { "show_window_menu", "ouii", xdg_shell_types + 12 }, 143 | { "move", "ou", xdg_shell_types + 16 }, 144 | { "resize", "ouu", xdg_shell_types + 18 }, 145 | { "set_max_size", "ii", xdg_shell_types + 0 }, 146 | { "set_min_size", "ii", xdg_shell_types + 0 }, 147 | { "set_maximized", "", xdg_shell_types + 0 }, 148 | { "unset_maximized", "", xdg_shell_types + 0 }, 149 | { "set_fullscreen", "?o", xdg_shell_types + 21 }, 150 | { "unset_fullscreen", "", xdg_shell_types + 0 }, 151 | { "set_minimized", "", xdg_shell_types + 0 }, 152 | }; 153 | 154 | static const struct wl_message xdg_toplevel_events[] = { 155 | { "configure", "iia", xdg_shell_types + 0 }, 156 | { "close", "", xdg_shell_types + 0 }, 157 | { "configure_bounds", "4ii", xdg_shell_types + 0 }, 158 | { "wm_capabilities", "5a", xdg_shell_types + 0 }, 159 | }; 160 | 161 | WL_PRIVATE const struct wl_interface xdg_toplevel_interface = { 162 | "xdg_toplevel", 7, 163 | 14, xdg_toplevel_requests, 164 | 4, xdg_toplevel_events, 165 | }; 166 | 167 | static const struct wl_message xdg_popup_requests[] = { 168 | { "destroy", "", xdg_shell_types + 0 }, 169 | { "grab", "ou", xdg_shell_types + 22 }, 170 | { "reposition", "3ou", xdg_shell_types + 24 }, 171 | }; 172 | 173 | static const struct wl_message xdg_popup_events[] = { 174 | { "configure", "iiii", xdg_shell_types + 0 }, 175 | { "popup_done", "", xdg_shell_types + 0 }, 176 | { "repositioned", "3u", xdg_shell_types + 0 }, 177 | }; 178 | 179 | WL_PRIVATE const struct wl_interface xdg_popup_interface = { 180 | "xdg_popup", 7, 181 | 3, xdg_popup_requests, 182 | 3, xdg_popup_events, 183 | }; 184 | 185 | -------------------------------------------------------------------------------- /src/frames/images.cpp: -------------------------------------------------------------------------------- 1 | #include "images.hpp" 2 | #include "imgui.h" 3 | 4 | // #define STB_IMAGE_IMPLEMENTATION 5 | #include 6 | // #define STB_IMAGE_RESIZE_IMPLEMENTATION 7 | #include 8 | // #define STB_IMAGE_WRITE_IMPLEMENTATION 9 | #include 10 | 11 | ImageList::ImageList(const int logicalWidth, const int logicalHeight) 12 | : Frame(), wallpapers(), logicalWidth(logicalWidth), logicalHeight(logicalHeight) {} 13 | 14 | void ImageList::addImages(const std::vector& newWallpapers) { 15 | pendingWallpapers.insert(pendingWallpapers.end(), newWallpapers.begin(), newWallpapers.end()); 16 | } 17 | 18 | // https://github.com/ocornut/imgui/wiki/Image-Loading-and-Displaying-Examples#example-for-opengl-users 19 | FrameResult ImageList::render() { 20 | 21 | // process any new wallpapers to load their textures on the main thread 22 | processPendingWallpapers(); 23 | 24 | if (ImGui::IsKeyPressed(ImGuiKey_LeftArrow) || ImGui::IsKeyPressed(ImGuiKey_H)) { 25 | navigate(-1); 26 | } 27 | if (ImGui::IsKeyPressed(ImGuiKey_RightArrow) || ImGui::IsKeyPressed(ImGuiKey_L)) { 28 | navigate(1); 29 | } 30 | if (ImGui::IsKeyPressed(ImGuiKey_Enter) || ImGui::IsKeyPressed(ImGuiKey_Space)) { 31 | if (selectedIndex >= 0 && selectedIndex < wallpapers.size()) { 32 | return FrameResult::Submit(wallpapers[selectedIndex].path); 33 | } 34 | } 35 | if (ImGui::IsKeyPressed(ImGuiKey_Escape)) { 36 | return FrameResult::Cancel(); 37 | } 38 | 39 | ImGuiIO& io = ImGui::GetIO(); 40 | Vec2 viewportSize = getSize(); 41 | 42 | float width = viewportSize.x; 43 | float height = viewportSize.y; 44 | 45 | // window padding 46 | float edge_padding = 20.0f; 47 | ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(edge_padding, edge_padding)); 48 | 49 | ImGui::SetNextWindowPos(ImVec2(0, 0)); 50 | ImGui::SetNextWindowSize(ImVec2(width, height), ImGuiCond_Always); 51 | 52 | ImGui::Begin("Wallpapers", 53 | nullptr, 54 | ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoSavedSettings | 55 | ImGuiWindowFlags_NoResize); 56 | 57 | // image display area 58 | ImVec2 contentRegion = ImGui::GetContentRegionAvail(); 59 | float imageAreaHeight = contentRegion.y; 60 | 61 | // image size 62 | float imageHeight = 225; // image_area_height - 40.0f; // padding 63 | float imageWidth = 400; // image_height * 0.75f; 64 | float spacing = 20.0f; 65 | float totalWidthPerImage = imageWidth + spacing; 66 | 67 | // smooth scroll to selected image 68 | float targetScroll = selectedIndex * totalWidthPerImage - (contentRegion.x - imageWidth) * 0.5f; 69 | scrollOffset += (targetScroll - scrollOffset) * 0.15f; // smooth interpolation 70 | 71 | std::lock_guard lock(wallpapersMutex); 72 | 73 | if (textures.empty()) { 74 | ImGui::SetWindowFontScale(2.0f); 75 | 76 | const char* text = "Generating thumbnails..."; 77 | ImVec2 textSize = ImGui::CalcTextSize(text); 78 | ImVec2 windowSize = ImGui::GetContentRegionAvail(); 79 | ImGui::SetCursorPosX((windowSize.x - textSize.x) * 0.5f); 80 | ImGui::SetCursorPosY((windowSize.y - textSize.y) * 0.5f); 81 | ImGui::Text("%s", text); 82 | 83 | ImGui::SetWindowFontScale(1.0f); 84 | } else { 85 | 86 | ImGui::BeginChild("ScrollRegion", 87 | ImVec2(0, imageAreaHeight), 88 | false, 89 | ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse); 90 | 91 | ImGui::SetScrollX(scrollOffset); 92 | 93 | // render images horizontally 94 | for (int i = 0; i < textures.size(); i++) { 95 | if (i > 0) 96 | ImGui::SameLine(); 97 | 98 | ImGui::BeginGroup(); 99 | 100 | // highlight selected image 101 | bool is_selected = (i == selectedIndex); 102 | 103 | if (is_selected) { 104 | ImVec2 p_min = ImGui::GetCursorScreenPos(); 105 | ImVec2 p_max = ImVec2(p_min.x + imageWidth, p_min.y + imageHeight); 106 | ImU32 color = ImGui::GetColorU32(hoverColor); 107 | // draw on foreground layer to avoid child clipping 108 | ImGui::GetForegroundDrawList()->AddRect(p_min, p_max, color, imageRounding, 0, 4.0f); 109 | } 110 | 111 | // make images clickable 112 | ImGui::PushID(i); 113 | // ImGui::Image((void*)(intptr_t)textures[i], ImVec2(image_width, image_height)); 114 | ImVec2 p_min = ImGui::GetCursorScreenPos(); 115 | ImVec2 p_max = ImVec2(p_min.x + imageWidth, p_min.y + imageHeight); 116 | ImGui::GetWindowDrawList()->AddImageRounded( 117 | (void*)(intptr_t)textures[i], p_min, p_max, ImVec2(0, 0), ImVec2(1, 1), IM_COL32_WHITE, imageRounding); 118 | ImGui::Dummy(ImVec2(imageWidth, imageHeight)); 119 | ImGui::PopID(); 120 | 121 | ImGui::EndGroup(); 122 | 123 | // add spacing 124 | if (i < textures.size() - 1) { 125 | ImGui::SameLine(0.0f, spacing); 126 | } 127 | } 128 | 129 | ImGui::EndChild(); 130 | } 131 | 132 | ImGui::End(); 133 | 134 | ImGui::PopStyleVar(); 135 | return FrameResult::Continue(); 136 | } 137 | 138 | void ImageList::processPendingWallpapers() { 139 | std::lock_guard lock(wallpapersMutex); 140 | 141 | for (const auto& wallpaper : pendingWallpapers) { 142 | GLuint texture = LoadTextureFromFile(wallpaper.thumbnailPath.c_str()); 143 | if (texture != 0) { 144 | textures.push_back(texture); 145 | } else { 146 | debug::log(ERR, "Failed to load texture: {}", wallpaper.thumbnailPath); 147 | textures.push_back(0); 148 | } 149 | wallpapers.push_back(wallpaper); 150 | } 151 | 152 | pendingWallpapers.clear(); 153 | } 154 | 155 | void ImageList::navigate(int direction) { 156 | if (textures.empty()) 157 | return; 158 | selectedIndex += direction; 159 | if (selectedIndex < 0) 160 | selectedIndex = 0; 161 | if (selectedIndex >= textures.size()) 162 | selectedIndex = textures.size() - 1; 163 | } 164 | 165 | Vec2 ImageList::getSize() { 166 | float w = (float)logicalWidth * widthRatio; 167 | float edgePadding = 20.0f; // padding we want on all sides 168 | float contentHeight = 225.0f; // height of the image area 169 | 170 | // add padding to width and height to account for the space we need 171 | return Vec2{w + (edgePadding * 2), contentHeight + (edgePadding * 2)}; 172 | } 173 | 174 | // load image and create an OpenGL texture 175 | GLuint ImageList::LoadTextureFromFile(const char* filename) { 176 | 177 | int width, height, channels; 178 | unsigned char* data = stbi_load(filename, &width, &height, &channels, 4); 179 | 180 | if (data == nullptr) { 181 | return 0; 182 | } 183 | 184 | // create OpenGL texture 185 | GLuint texture; 186 | glGenTextures(1, &texture); 187 | glBindTexture(GL_TEXTURE_2D, texture); 188 | 189 | glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); 190 | glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); 191 | glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); 192 | glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); 193 | 194 | // upload pixels to texture 195 | glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, data); 196 | 197 | stbi_image_free(data); 198 | 199 | return texture; 200 | } 201 | 202 | void ImageList::applyTheme(const Config& config) { 203 | hoverColor = config.getColor("theme", "hover_color", "#3366B366"); 204 | imageRounding = config.getFloat("theme", "frame_rounding", 8.0); 205 | widthRatio = config.getFloat("theme", "wallpaper_width_ratio", 0.8f); 206 | } 207 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # hyprwat 2 | 3 | A Hyprland menu utility to present selectable options with a customizable interface. 4 | 5 | ## Description 6 | 7 | hyprwat creates a popup menu at your cursor position where you can select from a list of options. It also has built-in support for WiFi networks, audio devices, wallpapers, and custom menus. Custom menus can be defined using simple YAML configuration files. 8 | 9 | ## Features 10 | 11 | - **Wayland native**: Built specifically for Wayland compositors 12 | - **Hyprland integration**: Designed to work with Hyprland compositor 13 | - **Cursor positioning**: Automatically appears at your current cursor position 14 | - **WiFi selector**: Built-in support for selecting WiFi networks (via dbus and NetworkManager 15 | - **Audio selector**: Built-in support for selecting audio input/output devices (via Pipewire) 16 | - **Wallpaper selection**: Easily select a wallpaper from a directory of images (hyprpaper only) 17 | - **Custom menus**: Define your own menus using simple YAML configuration files 18 | - **Theming**: Customize the appearance with a configuration file 19 | 20 | ### Examples 21 | 22 | #### WiFi Network Selector 23 | 24 | ![wifi](examples/img/wifi.png) 25 | 26 | #### Wallpaper Selector 27 | 28 | ![wallpapers](examples/img/wallpapers.png) 29 | 30 | #### Power Profile Selector 31 | 32 | ![powerprofilesctl](examples/img/powerprofiles.png) 33 | 34 | See the [examples](examples) directory for more. 35 | 36 | ## Installation 37 | 38 | ``` 39 | yay -S hyprwat 40 | ``` 41 | Or build from source (see Build Instructions below). 42 | 43 | ## Usage 44 | 45 | ``` 46 | hyprwat [OPTIONS] [id[:displayName][*]]... 47 | ``` 48 | 49 | ### Command Line Arguments 50 | 51 | You can pass a list of items directly as command-line arguments, where each item is a tuple in the form: 52 | 53 | ``` 54 | id[:displayName][*] 55 | ``` 56 | 57 | - `id`: Required identifier string (used internally) 58 | - `displayName`: Optional label to show in the UI (defaults to id) 59 | - `*`: Optional suffix to mark this item as initially selected 60 | 61 | If no arguments are provided, hyprwat will read from stdin, expecting one item per line in the same format. 62 | 63 | ### Options 64 | 65 | - `-h, --help`: Show help message 66 | - `--input `: Show an input prompt instead of a selection menu with optional hint text 67 | - `--password `: Show a password input prompt (masked input) with optional hint text 68 | - `--audio`: Show audio input/output device selector (requires pipewire) 69 | - `--wifi`: Show WiFi network selection 70 | - `--custom `: Load a custom menu from a YAML configuration file 71 | - `--wallpaper `: Select a wallpaper from the specified directory (for hyprpaper) 72 | 73 | 74 | ### More Examples 75 | 76 | ```bash 77 | # Simple options with custom display names and pre-selection 78 | hyprwat performance:Performance* balanced:Balanced powersave:PowerSaver 79 | 80 | # Using stdin input 81 | echo -e "wifi0:Home*\nwifi1:Work\nwifi2:Other" | hyprwat 82 | 83 | # Input prompt 84 | hyprwat --input "Enter Name" 85 | 86 | # Password prompt 87 | hyprwat --password "Enter Passphrase" 88 | 89 | # WiFi network selection 90 | hyprwat --wifi 91 | 92 | # Audio device selection 93 | hyprwat --audio 94 | 95 | # Custom menu defined in yaml config files 96 | hyprwat --custom ~/.config/hyprwat/menus/powermenu.yaml 97 | 98 | # Wallpaper selection from a directory 99 | hyprwat --wallpaper ~/.local/share/wallpapers 100 | 101 | ``` 102 | See the [examples](examples) directory for more. 103 | 104 | 105 | ## Theming 106 | You can customize the appearance of the UI by modifying the configuration file located at `~/.config/hyprwat/hyprwat.conf`. The file uses a simple INI format to define colors and styles for various UI elements. 107 | 108 | Example `hyprwat.conf`: 109 | 110 | ```ini 111 | [theme] 112 | font_color = #cdd6f4 113 | font_path = ~/.local/share/fonts/MesloLGSDZNerdFont-Regular.ttf 114 | font_size = 14.0 115 | background_color = #1e1e2e 116 | window_rounding = 10.0 117 | frame_rounding = 6.0 118 | background_blur = 0.95 119 | hover_color = #3366b3ff 120 | active_color = #3366b366 121 | wallpaper_width_ratio = 0.8 122 | ``` 123 | 124 | ## Build Instructions 125 | 126 | ### Dependencies 127 | 128 | #### Arch Linux 129 | 130 | ```bash 131 | sudo pacman -S cmake make gcc wayland wayland-protocols mesa fontconfig pkgconf libxkbcommon pipewire sdbus-c++ 132 | ``` 133 | 134 | #### Debian/Ubuntu 135 | 136 | ```bash 137 | sudo apt update 138 | sudo apt install cmake make g++ libwayland-dev wayland-protocols \ 139 | libegl1-mesa-dev libgl1-mesa-dev libfontconfig1-dev \ 140 | pkg-config libxkbcommon-dev libsdbus-c++-dev libpipewire-0.3-dev 141 | ``` 142 | 143 | ### Building 144 | 145 | 1. **Clone the repository**: 146 | ```bash 147 | git clone git@github.com:zackb/hyprwat.git 148 | cd hyprwat 149 | git submodule update --init --recursive 150 | ``` 151 | 152 | 2. **Build the project**: 153 | ```bash 154 | # Debug build (default) 155 | make debug 156 | 157 | # Or release build 158 | make release 159 | ``` 160 | 161 | 3. **Install** (optional): 162 | ```bash 163 | make install 164 | ``` 165 | 166 | ### Manual Build with CMake 167 | 168 | If you prefer to use CMake directly: 169 | 170 | ```bash 171 | # Configure 172 | cmake --preset debug 173 | # or: cmake --preset release 174 | 175 | # Build 176 | cmake --build --preset debug 177 | # or: cmake --build --preset release 178 | ``` 179 | 180 | ## Development 181 | 182 | ### Project Structure 183 | 184 | - `src/`: Main source code 185 | - `main.cpp`: Entry point and argument parsing 186 | - `ui.cpp`: User interface logic 187 | - `wayland/`: Wayland protocol implementations 188 | - `renderer/`: EGL/OpenGL rendering context 189 | - `selection/`: Selection/Menu handling logic and UI 190 | - `hyprland/`: Hyprland IPC integration 191 | - `audio/`: Pipewire audio device handling 192 | - `wifi/`: WiFi network handling via DBus and NetworkManager 193 | - `frames/`: UI frame components 194 | - `flows/`: UI flow definitions (select network -> input password) 195 | - `ext/imgui/`: ImGui library (git submodule) 196 | - `CMakeLists.txt`: Build configuration 197 | - `Makefile`: Convenience build targets 198 | 199 | ## Integration Examples 200 | 201 | ### Power Profile Selector 202 | 203 | The included `powerprofiles.sh` script demonstrates integration with powerprofilesctl: 204 | 205 | ```bash 206 | #!/bin/bash 207 | 208 | # Define profiles: id -> display name 209 | declare -A profiles=( 210 | ["performance"]="⚡ Performance" 211 | ["balanced"]="⚖ Balanced" 212 | ["power-saver"]="▽ Power Saver" 213 | ) 214 | 215 | # Get the current active profile 216 | current_profile=$(powerprofilesctl get) 217 | 218 | # Build hyprwat arguments 219 | args=() 220 | for id in "${!profiles[@]}"; do 221 | label="${profiles[$id]}" 222 | if [[ "$id" == "$current_profile" ]]; then 223 | args+=("${id}:${label}*") 224 | else 225 | args+=("${id}:${label}") 226 | fi 227 | done 228 | 229 | # Launch hyprwat and capture the selection 230 | selection=$(hyprwat "${args[@]}") 231 | 232 | # If user made a selection, apply it 233 | if [[ -n "$selection" ]]; then 234 | powerprofilesctl set "$selection" 235 | fi 236 | ``` 237 | 238 | ### Wallpaper Selector with hyprpaper and hyprlock persistence 239 | 240 | The included [wallpaper.sh](examples/wallpaper.sh) script demonstrates how to use hyprwat to select a wallpaper and update the hyprpaper and hyprlock configuration files: 241 | 242 | ``` 243 | #!/bin/bash 244 | wall=$(hyprwat --wallpaper ~/.local/share/wallpapers) 245 | 246 | if [ -n "$wall" ]; then 247 | sed -i "s|^\$image = .*|\$image = $wall|" ~/.config/hypr/hyprlock.conf 248 | sed -i "s|^preload = .*|preload = $wall|" ~/.config/hypr/hyprpaper.conf 249 | sed -i "s|^wallpaper =.*,.*|wallpaper = , $wall|" ~/.config/hypr/hyprpaper.conf 250 | fi 251 | ``` 252 | 253 | ### Passphrase Input Prompt 254 | ```bash 255 | PASSPHRASE=$(hyprwat --input Passphrase) 256 | echo $PASSPHRASE 257 | ``` 258 | 259 | ## Requirements 260 | 261 | - **Wayland compositor** (tested with Hyprland) 262 | - **C++20 compatible compiler** 263 | - **CMake 3.15+** 264 | - **OpenGL/EGL support** 265 | - **Fontconfig** 266 | - **xkbcommon** 267 | - **Pipewire** 268 | - **sdbus-c++** 269 | 270 | 271 | ## Why? 272 | I created hyprwat to fill a gap in the Wayland ecosystem for a simple, flexible, and visually appealing menu system that can be easily integrated into various use cases. Particularly, for waybar I needed a menu selection for wifi networks, power profiles, pavcontrol, etc. 273 | 274 | [My first attempt](https://github.com/zackb/code/tree/master/cpp/wat) used SDL2/3 and ImGui, but I didn't like the menu being a window (there's no layer shell support in SDL). So I rewrote it using pure Wayland protocols and EGL for rendering. 275 | 276 | If nothing else a pretty cool [cpp wrapper for Wayland protocols](src/wayland). 277 | 278 | As time passed I added more features and built-in selectors for things I couldn't find elsewhere. I didn't want to rice rofi to make a wallpaper selector, so I added that too (hyprpaper only)! 279 | 280 | ## License 281 | 282 | [MIT](LICENSE) 283 | 284 | ## Contributing 285 | 286 | Get at me on [GitHub](https://github.com/zackb) or [BlueSky](https://bsky.app/profile/zackzbz.bsky.social) if you have ideas, issues, or want to contribute! 287 | -------------------------------------------------------------------------------- /man/hyprwat.6: -------------------------------------------------------------------------------- 1 | .TH HYPRWAT 6 "November 2025" "hyprwat 0.9.2" "User Commands" 2 | .SH NAME 3 | hyprwat \- A Hyprland menu utility to present selectable options with a customizable interface 4 | .SH SYNOPSIS 5 | .B hyprwat 6 | [\fIOPTIONS\fR] [\fIid[:displayName][*]\fR]... 7 | .br 8 | .B hyprwat 9 | --input [\fIhint\fR] 10 | .br 11 | .B hyprwat 12 | --password [\fIhint\fR] 13 | .br 14 | .B hyprwat 15 | --wifi 16 | .br 17 | .B hyprwat 18 | --audio 19 | .br 20 | .B hyprwat 21 | --custom \fIconfig.yaml\fR 22 | .br 23 | .B hyprwat 24 | --wallpaper \fI~/.local/share/wallpapers\fR 25 | .SH DESCRIPTION 26 | .B hyprwat 27 | is a lightweight Wayland-native popup menu that presents selectable options 28 | or input prompts at the current cursor position. It integrates cleanly with 29 | the Hyprland compositor and can be used to build interactive menus for 30 | system controls, Wi-Fi networks, audio devices, or any scriptable UI 31 | selection task. 32 | In its default menu mode, 33 | .B hyprwat 34 | displays a list of options, one of which may be preselected. 35 | It can also present text input or password dialogs, automatically populate 36 | menus from system resources such as Wi-Fi networks or PipeWire audio devices, 37 | or render fully custom menus from YAML configuration files with support for 38 | hierarchical submenus, multiple widget types, and flexible actions. 39 | .SH MODES 40 | .TP 41 | .B MENU MODE (default) 42 | You can pass a list of items as command-line arguments in the form: 43 | .RS 44 | .nf 45 | id[:displayName][*] 46 | .fi 47 | .RE 48 | .RS 49 | .IP \(bu 2 50 | \fBid\fR: Required identifier (used internally) 51 | .IP \(bu 2 52 | \fBdisplayName\fR: Optional text label (defaults to id) 53 | .IP \(bu 2 54 | \fB*\fR: Optional suffix marking this item as initially selected 55 | .RE 56 | If no arguments are passed, menu items may be provided on standard input, 57 | one per line, in the same format. 58 | .EX 59 | $ hyprwat performance:Performance* balanced:Balanced powersave:PowerSaver 60 | $ echo -e "wifi0:Home*\\nwifi1:Work\\nwifi2:Other" | hyprwat 61 | .EE 62 | .TP 63 | .B INPUT MODE 64 | Show a text input prompt with optional hint text. 65 | .EX 66 | $ hyprwat --input "Enter your name" 67 | .EE 68 | .TP 69 | .B PASSWORD MODE 70 | Show a password input prompt (masked input). 71 | .EX 72 | $ hyprwat --password "WiFi Passphrase" 73 | .EE 74 | .TP 75 | .B WIFI MODE 76 | Scan and display available Wi-Fi networks for selection. 77 | If a network requires authentication, a password prompt will follow. 78 | .EX 79 | $ hyprwat --wifi 80 | .EE 81 | .TP 82 | .B AUDIO MODE 83 | Show available audio input/output devices (PipeWire) and select one. 84 | .EX 85 | $ hyprwat --audio 86 | .EE 87 | .TP 88 | .B WALLPAPER MODE 89 | Select an image file from the specified directory to set as the desktop wallpaper. This requires hyprpaper. 90 | .EX 91 | $ hyprwat --wallpaper ~/.local/share/wallpapers 92 | .EE 93 | .TP 94 | .B CUSTOM MODE 95 | Render a fully custom menu from a YAML configuration file. Custom menus 96 | support multiple widget types (buttons, inputs, sliders, checkboxes, color 97 | pickers, etc.), hierarchical submenus, keyboard shortcuts, and various 98 | action types including command execution and submenu navigation. 99 | .EX 100 | $ hyprwat --custom ~/.config/hyprwat/main.yaml 101 | .EE 102 | .SH OPTIONS 103 | .TP 104 | .BR -h , " --help" 105 | Show help message and exit. 106 | .TP 107 | .BR --input " [hint]" 108 | Display an input field with optional hint text. 109 | .TP 110 | .BR --password " [hint]" 111 | Display a password input prompt (masked). 112 | .TP 113 | .BR --wifi 114 | Show Wi-Fi network selection mode. 115 | .TP 116 | .BR --audio 117 | Show audio input/output device selection mode. 118 | .TP 119 | .BR --wallpaper 120 | Select an image file from the specified directory to set as the desktop wallpaper. 121 | .TP 122 | .BR --custom " \fIconfig.yaml\fR" 123 | Load and render a custom menu from the specified YAML configuration file. 124 | See 125 | .B CUSTOM MENU FORMAT 126 | below for configuration syntax. 127 | .SH CUSTOM MENU FORMAT 128 | Custom menus are defined in YAML format and support rich, interactive UIs 129 | with hierarchical navigation. The configuration file structure: 130 | .PP 131 | .B Basic Structure: 132 | .EX 133 | title: "Menu Title" 134 | width: 400 # Optional fixed width 135 | height: 300 # Optional fixed height 136 | 137 | theme: # Optional theme overrides 138 | hover_color: "#3366B3FF" 139 | active_color: "#3366B366" 140 | 141 | sections: # List of widgets 142 | - type: text 143 | content: "Label text" 144 | style: bold # normal, bold, italic 145 | 146 | - type: separator 147 | 148 | - type: button 149 | label: "Button Label" 150 | action: 151 | type: execute 152 | command: "command to run" 153 | close_on_success: true 154 | 155 | shortcuts: # Optional keyboard shortcuts 156 | - key: "Escape" 157 | action: 158 | type: cancel 159 | .EE 160 | .PP 161 | .B Supported Widget Types: 162 | .RS 163 | .IP \(bu 2 164 | .B text 165 | \- Static text label (supports bold/italic) 166 | .IP \(bu 2 167 | .B separator 168 | \- Visual divider line 169 | .IP \(bu 2 170 | .B button 171 | \- Clickable button 172 | .IP \(bu 2 173 | .B selectable_list 174 | \- List of selectable items 175 | .IP \(bu 2 176 | .B input 177 | \- Text input field 178 | .IP \(bu 2 179 | .B checkbox 180 | \- Boolean toggle 181 | .IP \(bu 2 182 | .B slider 183 | \- Numeric slider with min/max range 184 | .IP \(bu 2 185 | .B combo 186 | \- Dropdown selection box 187 | .IP \(bu 2 188 | .B color_picker 189 | \- Color selection widget 190 | .RE 191 | .PP 192 | .B Supported Action Types: 193 | .RS 194 | .IP \(bu 2 195 | .B execute 196 | \- Execute shell command 197 | .IP \(bu 2 198 | .B submit 199 | \- Return a value and exit 200 | .IP \(bu 2 201 | .B cancel 202 | \- Cancel and exit 203 | .IP \(bu 2 204 | .B submenu 205 | \- Navigate to another YAML config file 206 | .IP \(bu 2 207 | .B back 208 | \- Return to parent menu 209 | .RE 210 | .PP 211 | .B Submenu Example: 212 | .EX 213 | # main.yaml 214 | sections: 215 | - type: button 216 | label: "Power Settings" 217 | action: 218 | type: submenu 219 | path: "power.yaml" # Relative or absolute path 220 | 221 | # power.yaml 222 | title: "Power Settings" 223 | sections: 224 | - type: selectable_list 225 | items: 226 | - id: "performance" 227 | label: "Performance" 228 | action: 229 | type: execute 230 | command: "powerprofilesctl set performance" 231 | 232 | - type: button 233 | label: "← Back" 234 | action: 235 | type: back 236 | 237 | shortcuts: 238 | - key: "Escape" 239 | action: 240 | type: back 241 | .EE 242 | .PP 243 | Submenus support unlimited nesting depth. The Escape key navigates back 244 | through the menu hierarchy, exiting only from the root level. 245 | Action commands support token replacement: \fB{value}\fR, \fB{index}\fR, 246 | and \fB{state}\fR are replaced with widget values during execution. 247 | .SH EXAMPLES 248 | .TP 249 | Select a power profile: 250 | .EX 251 | $ hyprwat performance:"Performance"* balanced:"" power-saver:"Power Saver" 252 | .EE 253 | .TP 254 | Provide menu items via stdin: 255 | .EX 256 | $ echo -e "wifi0:Home*\\nwifi1:Work\\nwifi2:Other" | hyprwat 257 | .EE 258 | .TP 259 | Prompt for input: 260 | .EX 261 | $ NAME=$(hyprwat --input "Enter name") 262 | .EE 263 | .TP 264 | Select Wi-Fi or audio device: 265 | .EX 266 | $ hyprwat --wifi 267 | $ hyprwat --audio 268 | .EE 269 | .TP 270 | Launch a custom control panel: 271 | .EX 272 | $ hyprwat --custom ~/.config/hyprwat/control-panel.yaml 273 | .EE 274 | .TP 275 | Create a power management menu: 276 | .EX 277 | $ cat > ~/.config/hyprwat/power.yaml << 'EOF' 278 | title: "Power Management" 279 | sections: 280 | - type: text 281 | content: "Select power profile:" 282 | 283 | - type: selectable_list 284 | items: 285 | - id: "perf" 286 | label: "⚡ Performance" 287 | action: 288 | type: execute 289 | command: "powerprofilesctl set performance" 290 | close_on_success: true 291 | - id: "balanced" 292 | label: "⚖️ Balanced" 293 | selected: true 294 | action: 295 | type: execute 296 | command: "powerprofilesctl set balanced" 297 | close_on_success: true 298 | - id: "saver" 299 | label: "🔋 Power Saver" 300 | action: 301 | type: execute 302 | command: "powerprofilesctl set power-saver" 303 | close_on_success: true 304 | EOF 305 | 306 | $ hyprwat --custom ~/.config/hyprwat/power.yaml 307 | .EE 308 | .SH FILES 309 | .TP 310 | .I ~/.config/hyprwat/hyprwat.conf 311 | Optional configuration file for theme customization. 312 | Uses simple INI syntax to define fonts, colors, and window style. 313 | .TP 314 | .I ~/.config/hyprwat/*.yaml 315 | Custom menu configuration files. Can be organized hierarchically 316 | with submenus referencing other YAML files via relative or absolute paths. 317 | .SH REQUIREMENTS 318 | Wayland compositor (tested with Hyprland), 319 | C++20 compiler, EGL/OpenGL, Fontconfig, xkbcommon, 320 | PipeWire, sdbus-c++, and yaml-cpp. 321 | .SH SEE ALSO 322 | .BR hyprland (1), 323 | .BR waybar (1), 324 | .BR nmcli (1), 325 | .BR pactl (1) 326 | .SH AUTHOR 327 | Written by Zack Bartel 328 | .SH LICENSE 329 | MIT License. 330 | Source available at: 331 | .UR https://github.com/zackb/hyprwat 332 | .UE 333 | -------------------------------------------------------------------------------- /src/audio/audio.cpp: -------------------------------------------------------------------------------- 1 | #include "audio.hpp" 2 | #include "../debug/log.hpp" 3 | #include 4 | #include 5 | 6 | static const struct pw_registry_events registry_events = { 7 | /* version */ PW_VERSION_REGISTRY_EVENTS, 8 | /* global */ AudioManagerClient::registryEventGlobal, 9 | /* global_remove */ AudioManagerClient::registryEventGlobalRemove, 10 | }; 11 | 12 | static const struct pw_metadata_events metadata_events = { 13 | /* version */ PW_VERSION_METADATA_EVENTS, 14 | /* property */ AudioManagerClient::metadataProperty, 15 | }; 16 | 17 | AudioManagerClient::AudioManagerClient() 18 | : loop(nullptr) 19 | , context(nullptr) 20 | , core(nullptr) 21 | , registry(nullptr) 22 | , metadata_proxy(nullptr) 23 | , metadata(nullptr) 24 | , default_sink_id(0) 25 | , default_source_id(0) 26 | , initialized(false) { 27 | 28 | pw_init(nullptr, nullptr); 29 | 30 | loop = pw_thread_loop_new("audio-manager", nullptr); 31 | if (!loop) { 32 | debug::log(ERR, "Failed to create thread loop"); 33 | return; 34 | } 35 | 36 | context = pw_context_new(pw_thread_loop_get_loop(loop), nullptr, 0); 37 | if (!context) { 38 | debug::log(ERR, "Failed to create context"); 39 | return; 40 | } 41 | 42 | if (pw_thread_loop_start(loop) < 0) { 43 | debug::log(ERR, "Failed to start thread loop"); 44 | return; 45 | } 46 | 47 | pw_thread_loop_lock(loop); 48 | 49 | core = pw_context_connect(context, nullptr, 0); 50 | if (!core) { 51 | debug::log(ERR, "Failed to connect to PipeWire core"); 52 | pw_thread_loop_unlock(loop); 53 | return; 54 | } 55 | 56 | registry = pw_core_get_registry(core, PW_VERSION_REGISTRY, 0); 57 | pw_registry_add_listener(registry, ®istry_listener, ®istry_events, this); 58 | 59 | pw_thread_loop_unlock(loop); 60 | 61 | // TODO: can we not do this?? 62 | usleep(100000); 63 | 64 | initialized = true; 65 | } 66 | 67 | AudioManagerClient::~AudioManagerClient() { 68 | if (loop) { 69 | pw_thread_loop_stop(loop); 70 | } 71 | 72 | if (metadata_proxy) { 73 | spa_hook_remove(&metadata_listener); 74 | pw_proxy_destroy(metadata_proxy); 75 | } 76 | 77 | if (registry) { 78 | spa_hook_remove(®istry_listener); 79 | pw_proxy_destroy((struct pw_proxy*)registry); 80 | } 81 | 82 | if (core) { 83 | pw_core_disconnect(core); 84 | } 85 | 86 | if (context) { 87 | pw_context_destroy(context); 88 | } 89 | 90 | if (loop) { 91 | pw_thread_loop_destroy(loop); 92 | } 93 | 94 | pw_deinit(); 95 | } 96 | 97 | void AudioManagerClient::registryEventGlobal( 98 | void* data, uint32_t id, uint32_t permissions, const char* type, uint32_t version, const struct spa_dict* props) { 99 | auto* self = static_cast(data); 100 | 101 | if (strcmp(type, PW_TYPE_INTERFACE_Node) == 0) { 102 | const char* media_class = spa_dict_lookup(props, PW_KEY_MEDIA_CLASS); 103 | if (!media_class) 104 | return; 105 | 106 | AudioDevice dev; 107 | dev.id = id; 108 | dev.isDefault = false; 109 | 110 | const char* node_name = spa_dict_lookup(props, PW_KEY_NODE_NAME); 111 | dev.name = node_name ? node_name : ""; 112 | 113 | const char* node_desc = spa_dict_lookup(props, PW_KEY_NODE_DESCRIPTION); 114 | const char* node_nick = spa_dict_lookup(props, PW_KEY_NODE_NICK); 115 | dev.description = node_desc ? node_desc : (node_nick ? node_nick : dev.name); 116 | 117 | if (strcmp(media_class, "Audio/Sink") == 0) { 118 | self->sinks[id] = dev; 119 | } else if (strcmp(media_class, "Audio/Source") == 0) { 120 | self->sources[id] = dev; 121 | } 122 | } else if (strcmp(type, PW_TYPE_INTERFACE_Metadata) == 0) { 123 | const char* metadata_name = spa_dict_lookup(props, PW_KEY_METADATA_NAME); 124 | if (metadata_name && strcmp(metadata_name, "default") == 0) { 125 | pw_thread_loop_lock(self->loop); 126 | self->metadata_proxy = (struct pw_proxy*)pw_registry_bind(self->registry, id, type, PW_VERSION_METADATA, 0); 127 | if (self->metadata_proxy) { 128 | self->metadata = (struct pw_metadata*)self->metadata_proxy; 129 | pw_metadata_add_listener(self->metadata, &self->metadata_listener, &metadata_events, self); 130 | } 131 | pw_thread_loop_unlock(self->loop); 132 | } 133 | } 134 | } 135 | 136 | void AudioManagerClient::registryEventGlobalRemove(void* data, uint32_t id) { 137 | auto* self = static_cast(data); 138 | self->sinks.erase(id); 139 | self->sources.erase(id); 140 | } 141 | 142 | int AudioManagerClient::metadataProperty( 143 | void* data, uint32_t id, const char* key, const char* type, const char* value) { 144 | auto* self = static_cast(data); 145 | 146 | if (!key) 147 | return 0; 148 | 149 | if (strcmp(key, "default.audio.sink") == 0 && value) { 150 | struct spa_json it[2]; 151 | char key_buf[64]; 152 | char name_buf[256]; 153 | 154 | spa_json_init(&it[0], value, strlen(value)); 155 | if (spa_json_enter_object(&it[0], &it[1]) > 0) { 156 | while (spa_json_get_string(&it[1], key_buf, sizeof(key_buf)) > 0) { 157 | if (strcmp(key_buf, "name") == 0) { 158 | if (spa_json_get_string(&it[1], name_buf, sizeof(name_buf)) > 0) { 159 | std::string name(name_buf); 160 | for (auto& [id, dev] : self->sinks) { 161 | if (dev.name == name) { 162 | self->default_sink_id = id; 163 | break; 164 | } 165 | } 166 | } 167 | break; 168 | } else { 169 | spa_json_next(&it[1], nullptr); 170 | } 171 | } 172 | } 173 | } else if (strcmp(key, "default.audio.source") == 0 && value) { 174 | struct spa_json it[2]; 175 | char key_buf[64]; 176 | char name_buf[256]; 177 | 178 | spa_json_init(&it[0], value, strlen(value)); 179 | if (spa_json_enter_object(&it[0], &it[1]) > 0) { 180 | while (spa_json_get_string(&it[1], key_buf, sizeof(key_buf)) > 0) { 181 | if (strcmp(key_buf, "name") == 0) { 182 | if (spa_json_get_string(&it[1], name_buf, sizeof(name_buf)) > 0) { 183 | std::string name(name_buf); 184 | for (auto& [id, dev] : self->sources) { 185 | if (dev.name == name) { 186 | self->default_source_id = id; 187 | break; 188 | } 189 | } 190 | } 191 | break; 192 | } else { 193 | spa_json_next(&it[1], nullptr); 194 | } 195 | } 196 | } 197 | } 198 | 199 | return 0; 200 | } 201 | 202 | void AudioManagerClient::updateDevices() { 203 | if (!initialized) 204 | return; 205 | 206 | // trigger roundtrip to ensure we have latest data 207 | pw_thread_loop_lock(loop); 208 | pw_core_sync(core, 0, 0); 209 | pw_thread_loop_unlock(loop); 210 | 211 | // TODO: can we not do this?? 212 | usleep(50000); 213 | } 214 | 215 | std::vector AudioManagerClient::getDevices(const std::map& deviceMap) { 216 | std::vector devices; 217 | 218 | for (const auto& [id, dev] : deviceMap) { 219 | AudioDevice device = dev; 220 | device.isDefault = (id == default_sink_id) || (id == default_source_id); 221 | devices.push_back(device); 222 | } 223 | 224 | return devices; 225 | } 226 | 227 | std::vector AudioManagerClient::listInputDevices() { 228 | updateDevices(); 229 | return getDevices(sources); 230 | } 231 | 232 | std::vector AudioManagerClient::listOutputDevices() { 233 | updateDevices(); 234 | return getDevices(sinks); 235 | } 236 | 237 | bool AudioManagerClient::setDefault(uint32_t deviceId, const std::string& key) { 238 | if (!initialized || !metadata) { 239 | debug::log(ERR, "AudioManager not initialized or metadata not available"); 240 | return false; 241 | } 242 | 243 | // find device name 244 | std::string deviceName; 245 | auto& deviceMap = (key == "default.audio.sink") ? sinks : sources; 246 | 247 | auto it = deviceMap.find(deviceId); 248 | if (it == deviceMap.end()) { 249 | debug::log(ERR, "Device ID {} not found", deviceId); 250 | return false; 251 | } 252 | 253 | deviceName = it->second.name; 254 | 255 | std::string value = "{\"name\":\"" + deviceName + "\"}"; 256 | 257 | pw_thread_loop_lock(loop); 258 | int res = pw_metadata_set_property(metadata, PW_ID_CORE, key.c_str(), "Spa:String:JSON", value.c_str()); 259 | pw_thread_loop_unlock(loop); 260 | 261 | if (res < 0) { 262 | debug::log(ERR, "Failed to set default {}: {}", key, strerror(-res)); 263 | return false; 264 | } 265 | 266 | debug::log(INFO, "Set {} to device {} ({})", key, deviceId, deviceName); 267 | 268 | if (key == "default.audio.sink") { 269 | default_sink_id = deviceId; 270 | } else { 271 | default_source_id = deviceId; 272 | } 273 | 274 | return true; 275 | } 276 | 277 | bool AudioManagerClient::setDefaultInput(uint32_t deviceId) { return setDefault(deviceId, "default.audio.source"); } 278 | 279 | bool AudioManagerClient::setDefaultOutput(uint32_t deviceId) { return setDefault(deviceId, "default.audio.sink"); } 280 | -------------------------------------------------------------------------------- /src/ui.cpp: -------------------------------------------------------------------------------- 1 | #include "ui.hpp" 2 | #include "flows/flow.hpp" 3 | #include "imgui_impl_opengl3.h" 4 | #include "src/font/font.hpp" 5 | #include 6 | 7 | void UI::init(int x, int y, float scale) { 8 | 9 | // Create wl layer surface with small but reasonable initial size 10 | // Small enough to avoid flash, large enough for ImGui to work properly 11 | int initialWidth = 50; 12 | int initialHeight = 50; 13 | initialX = x; 14 | initialY = y; 15 | currentFractionalScale = scale; 16 | 17 | surface = std::make_unique(wayland.display().compositor(), wayland.display().layerShell()); 18 | surface->create(x, y, initialWidth, initialHeight); 19 | while (!surface->isConfigured()) { 20 | wayland.display().dispatch(); 21 | } 22 | 23 | // Get the current maximum (non-fractional) scale from all outputs 24 | currentScale = wayland.display().getMaxScale(); 25 | 26 | // Set up callback for dynamic scale changes 27 | wayland.display().setScaleChangeCallback([this](int32_t newScale) { updateScale(newScale); }); 28 | 29 | // Apply buffer scale for hidpi. Keep logical size for layer surface sizing, 30 | // but render buffers (EGL window) in pixel size. 31 | surface->bufferScale(currentScale); 32 | 33 | // Initialize EGL 34 | egl = std::make_unique(wayland.display().display()); 35 | 36 | // Create EGL window with buffer pixel size (logical * buffer_scale) 37 | const int buf_w = surface->width() * currentScale; 38 | const int buf_h = surface->height() * currentScale; 39 | if (!egl->createWindowSurface(surface->surface(), buf_w, buf_h)) { 40 | throw std::runtime_error("Failed to create EGL window surface"); 41 | } 42 | 43 | // enable blending for transparency 44 | glEnable(GL_BLEND); 45 | glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); 46 | 47 | // Initialize ImGui 48 | IMGUI_CHECKVERSION(); 49 | ImGui::CreateContext(); 50 | ImGui_ImplOpenGL3_Init("#version 100"); 51 | ImGuiIO& io = ImGui::GetIO(); 52 | io.IniFilename = nullptr; 53 | io.ConfigFlags |= ImGuiConfigFlags_NoMouseCursorChange; 54 | // Logical size in points 55 | io.DisplaySize = ImVec2((float)surface->width(), (float)surface->height()); 56 | 57 | // Framebuffer scale = buffer pixels / logical points 58 | io.DisplayFramebufferScale = ImVec2((float)currentScale, (float)currentScale); 59 | 60 | // Set up input handling wayland -> imgui 61 | wayland.input().setIO(&io); 62 | // Input bounds in logical units 63 | wayland.input().setWindowBounds(initialWidth, initialHeight); 64 | } 65 | 66 | // run a single frame until it returns a result 67 | FrameResult UI::run(Frame& frame) { 68 | FrameResult result = FrameResult::Continue(); 69 | 70 | while (running && !surface->shouldExit()) { 71 | // Process Wayland events 72 | wayland.display().prepareRead(); 73 | wayland.display().flush(); 74 | wayland.display().readEvents(); 75 | wayland.display().dispatchPending(); 76 | 77 | // Check if user clicked outside 78 | if (wayland.input().shouldExit()) { 79 | running = false; 80 | return FrameResult::Cancel(); 81 | } 82 | 83 | // Render ImGui frame and get result 84 | result = renderFrame(frame); 85 | 86 | // If frame returned a result (not CONTINUE), exit loop 87 | if (result.action != FrameResult::Action::CONTINUE) { 88 | break; 89 | } 90 | } 91 | 92 | return result; 93 | } 94 | 95 | // run a flow until completion 96 | void UI::runFlow(Flow& flow) { 97 | Frame* lastFrame = nullptr; 98 | 99 | while (!flow.isDone() && running && !surface->shouldExit()) { 100 | Frame* currentFrame = flow.getCurrentFrame(); 101 | if (!currentFrame) { 102 | break; 103 | } 104 | 105 | // apply theme to frame if it changed or if we have a config 106 | if (currentFrame != lastFrame && currentConfig) { 107 | currentFrame->applyTheme(*currentConfig); 108 | } 109 | 110 | lastFrame = currentFrame; 111 | 112 | // run the frame until it returns a result 113 | FrameResult result = run(*currentFrame); 114 | 115 | // Let flow handle the result 116 | if (!flow.handleResult(result)) { 117 | break; 118 | } 119 | } 120 | } 121 | 122 | FrameResult UI::renderFrame(Frame& frame) { 123 | 124 | ImGuiIO& io = ImGui::GetIO(); 125 | io.DeltaTime = 1.0f / 60.0f; 126 | // DisplaySize will be updated after potential resize 127 | io.DisplayFramebufferScale = ImVec2((float)currentScale, (float)currentScale); 128 | 129 | // Start ImGui frame 130 | ImGui_ImplOpenGL3_NewFrame(); 131 | ImGui::NewFrame(); 132 | ImGui::SetNextWindowPos(ImVec2(0, 0)); 133 | 134 | static Vec2 lastWindowSize; 135 | static int resizeStabilityCounter = 0; 136 | static int frameCount = 0; 137 | frameCount++; 138 | 139 | FrameResult result = frame.render(); 140 | Vec2 desiredSize = frame.getSize(); 141 | 142 | // Render (but don't swap yet) 143 | ImGui::Render(); 144 | 145 | const int RESIZE_STABILITY_FRAMES = (frameCount < 20) ? 0 : 3; // Resize immediately for first 20 frames 146 | 147 | // Check if size changed 148 | if (desiredSize != lastWindowSize) { 149 | resizeStabilityCounter = 0; // Reset counter on size change 150 | lastWindowSize = desiredSize; 151 | } else { 152 | resizeStabilityCounter++; 153 | } 154 | 155 | // Resize conditions: immediately for first frames, or after stability for later frames 156 | bool shouldResize = (resizeStabilityCounter >= RESIZE_STABILITY_FRAMES) && 157 | (desiredSize.x != surface->width() || desiredSize.y != surface->height()) && 158 | (desiredSize.x > 0 && desiredSize.y > 0); 159 | 160 | if (shouldResize) { 161 | // Clamp to reasonable bounds 162 | int newWidth = std::max(100, (int)desiredSize.x); 163 | int newHeight = std::max(50, (int)desiredSize.y); 164 | 165 | surface->resize(newWidth, newHeight, *egl); 166 | wayland.input().setWindowBounds(newWidth, newHeight); 167 | // wayland.display().flush(); 168 | 169 | // Update display size immediately for current frame 170 | io.DisplaySize = ImVec2((float)newWidth, (float)newHeight); 171 | 172 | // reposition window based on frame preferences 173 | if (frame.shouldRepositionOnResize()) { 174 | // cursor-based positioning for menus 175 | // compute viewport in Hyprland logical units 176 | auto [viewport_physical_w, viewport_physical_h] = wayland.display().getOutputSize(); 177 | int vw_hypr = (int)(viewport_physical_w / currentFractionalScale); 178 | int vh_hypr = (int)(viewport_physical_h / currentFractionalScale); 179 | 180 | // window size in Hyprland logical units 181 | int width_hypr = surface->width(); 182 | int height_hypr = surface->height(); 183 | 184 | surface->reposition(initialX, initialY, vw_hypr, vh_hypr, width_hypr, height_hypr); 185 | } else if (!frame.shouldPositionAtCursor()) { 186 | // center window for non-menu frames 187 | centerWindow(); 188 | } 189 | } else { 190 | // Ensure DisplaySize is always current 191 | io.DisplaySize = ImVec2((float)surface->width(), (float)surface->height()); 192 | } 193 | 194 | // Use buffer pixel size for viewport (after any resize) 195 | Vec2 bufSize = egl->getBufferSize(); 196 | glViewport(0, 0, (int)bufSize.x, (int)bufSize.y); 197 | glClearColor(0.0f, 0.0f, 0.0f, 0.0f); 198 | glClear(GL_COLOR_BUFFER_BIT); 199 | ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData()); 200 | 201 | // EGL buffer swap 202 | egl->swapBuffers(); 203 | 204 | return result; 205 | } 206 | 207 | void UI::updateScale(int32_t newScale) { 208 | 209 | if (newScale == currentScale || !surface || !egl) { 210 | return; 211 | } 212 | 213 | currentScale = newScale; 214 | 215 | // Update buffer scale 216 | surface->bufferScale(currentScale); 217 | 218 | // Resize EGL window with new buffer size 219 | const int buf_w = surface->width() * currentScale; 220 | const int buf_h = surface->height() * currentScale; 221 | if (egl->window()) { 222 | wl_egl_window_resize(egl->window(), buf_w, buf_h, 0, 0); 223 | } 224 | 225 | // ImGui will be updated in the next renderFrame() call 226 | } 227 | 228 | void UI::applyTheme(const Config& config) { 229 | currentConfig = const_cast(&config); 230 | 231 | ImGui::StyleColorsDark(); 232 | ImGuiStyle& style = ImGui::GetStyle(); 233 | ImGuiIO& io = ImGui::GetIO(); 234 | 235 | style.Colors[ImGuiCol_WindowBg] = config.getColor("theme", "background_color", "#1e1e2eF0"); 236 | style.Colors[ImGuiCol_Text] = config.getColor("theme", "font_color", "#cdd6f4"); 237 | 238 | style.WindowRounding = config.getFloat("theme", "window_rounding", 10.0f); 239 | style.FrameRounding = config.getFloat("theme", "frame_rounding", 6.0f); 240 | 241 | // TODO 242 | style.ItemSpacing = ImVec2(10, 6); 243 | style.WindowPadding = ImVec2(10, 10); 244 | style.FramePadding = ImVec2(8, 4); 245 | style.Colors[ImGuiCol_Border] = config.getColor("theme", "border_color", "#33ccffee"); 246 | 247 | // transparency 248 | style.Alpha = config.getFloat("theme", "background_blur", 0.95f); 249 | 250 | // set up font from config if specified, defaulting to fontconfig 251 | setupFont(io, config); 252 | } 253 | 254 | void UI::setupFont(ImGuiIO& io, const Config& config) { 255 | // load the user font if specified in config defaulting to fc 256 | std::string fontPath = config.getString("theme", "font_path", font::defaultFontPath()); 257 | float fontSize = config.getFloat("theme", "font_size", 14.0f); 258 | 259 | if (!fontPath.empty()) { 260 | ImFont* font = io.Fonts->AddFontFromFileTTF(fontPath.c_str(), fontSize); 261 | if (font) 262 | io.FontDefault = font; 263 | } 264 | 265 | // hidpi handled by DisplayFramebufferScale 266 | io.FontGlobalScale = 1.0f; 267 | } 268 | 269 | void UI::centerWindow() { 270 | if (!surface) { 271 | return; 272 | } 273 | 274 | // get viewport size in physical pixels 275 | auto [viewport_physical_w, viewport_physical_h] = wayland.display().getOutputSize(); 276 | 277 | // convert viewport to hyprland logical units 278 | int vw_hypr = (int)(viewport_physical_w / currentFractionalScale); 279 | int vh_hypr = (int)(viewport_physical_h / currentFractionalScale); 280 | 281 | // window size from surface->width() is in hyprland logical units 282 | // (because getSize() returns hyprland logical and that's what we pass to resize()) 283 | int width_hypr = surface->width(); 284 | int height_hypr = surface->height(); 285 | 286 | // calculate centered position in hyprland logical units 287 | int centered_x_hypr = (vw_hypr - width_hypr) / 2; 288 | int centered_y_hypr = (vh_hypr - height_hypr) / 2; 289 | 290 | // reposition to center 291 | surface->reposition(centered_x_hypr, centered_y_hypr, vw_hypr, vh_hypr, width_hypr, height_hypr); 292 | } 293 | -------------------------------------------------------------------------------- /src/net/network_manager.cpp: -------------------------------------------------------------------------------- 1 | #include "network_manager.hpp" 2 | #include "../debug/log.hpp" 3 | #include "../util.hpp" 4 | 5 | NetworkManagerClient::NetworkManagerClient() { 6 | connection = sdbus::createSystemBusConnection(); 7 | connection->enterEventLoopAsync(); 8 | proxy = sdbus::createProxy(*connection, 9 | sdbus::ServiceName("org.freedesktop.NetworkManager"), 10 | sdbus::ObjectPath("/org/freedesktop/NetworkManager")); 11 | } 12 | 13 | // returns all wifi capable devices 14 | std::vector NetworkManagerClient::getWifiDevices() { 15 | std::vector devices; 16 | 17 | proxy->callMethod("GetDevices").onInterface("org.freedesktop.NetworkManager").storeResultsTo(devices); 18 | 19 | // Filter only wifi devices 20 | std::vector wifiDevices; 21 | for (auto& devPath : devices) { 22 | auto devProxy = sdbus::createProxy(*connection, sdbus::ServiceName("org.freedesktop.NetworkManager"), devPath); 23 | 24 | sdbus::Variant devTypeVar; 25 | devProxy->callMethod("Get") 26 | .onInterface("org.freedesktop.DBus.Properties") 27 | .withArguments("org.freedesktop.NetworkManager.Device", "DeviceType") 28 | .storeResultsTo(devTypeVar); 29 | 30 | uint32_t devType = devTypeVar.get(); 31 | 32 | // NM_DEVICE_TYPE_WIFI = 2 33 | if (devType == 2) { 34 | wifiDevices.push_back(devPath); 35 | } 36 | } 37 | return wifiDevices; 38 | } 39 | 40 | // AP object paths on a given wifi device 41 | std::vector NetworkManagerClient::getAccessPoints(const sdbus::ObjectPath& devicePath) { 42 | std::vector aps; 43 | 44 | auto devProxy = sdbus::createProxy(*connection, sdbus::ServiceName("org.freedesktop.NetworkManager"), devicePath); 45 | 46 | try { 47 | devProxy->callMethod("GetAllAccessPoints") 48 | .onInterface("org.freedesktop.NetworkManager.Device.Wireless") 49 | .storeResultsTo(aps); 50 | } catch (const sdbus::Error& e) { 51 | debug::log(ERR, "Failed to get access points for device {}: {}", devicePath.c_str(), e.getMessage()); 52 | } 53 | return aps; 54 | } 55 | 56 | // list networks 57 | // just returns currently known access points, no scanning 58 | std::vector NetworkManagerClient::listWifiNetworks() { 59 | std::map networkMap; // SSID -> max strength 60 | 61 | auto wifiDevices = getWifiDevices(); 62 | if (wifiDevices.empty()) { 63 | debug::log(ERR, "No Wi-Fi devices found"); 64 | return {}; 65 | } 66 | 67 | for (auto& devicePath : wifiDevices) { 68 | std::vector apPaths; 69 | try { 70 | apPaths = getAccessPoints(devicePath); 71 | } catch (const sdbus::Error& e) { 72 | debug::log(ERR, "Failed to get access points for device {}: {}", devicePath.c_str(), e.getMessage()); 73 | continue; 74 | } 75 | 76 | for (auto& apPath : apPaths) { 77 | try { 78 | auto apProxy = 79 | sdbus::createProxy(*connection, sdbus::ServiceName("org.freedesktop.NetworkManager"), apPath); 80 | 81 | sdbus::Variant ssidVar; 82 | apProxy->callMethod("Get") 83 | .onInterface("org.freedesktop.DBus.Properties") 84 | .withArguments("org.freedesktop.NetworkManager.AccessPoint", "Ssid") 85 | .storeResultsTo(ssidVar); 86 | auto ssidBytes = ssidVar.get>(); 87 | std::string ssid(ssidBytes.begin(), ssidBytes.end()); 88 | 89 | sdbus::Variant strengthVar; 90 | apProxy->callMethod("Get") 91 | .onInterface("org.freedesktop.DBus.Properties") 92 | .withArguments("org.freedesktop.NetworkManager.AccessPoint", "Strength") 93 | .storeResultsTo(strengthVar); 94 | int strength = static_cast(strengthVar.get()); 95 | 96 | if (networkMap.find(ssid) == networkMap.end() || networkMap[ssid] < strength) { 97 | networkMap[ssid] = strength; 98 | } 99 | } catch (const sdbus::Error& e) { 100 | // Ignore errors 101 | } 102 | } 103 | } 104 | 105 | std::vector networks; 106 | for (const auto& [ssid, strength] : networkMap) { 107 | networks.push_back({ssid, strength}); 108 | } 109 | 110 | return networks; 111 | } 112 | 113 | // initiates scan and calls callback for each newly discovered AP 114 | void NetworkManagerClient::scanWifiNetworks(std::function callback, int timeoutSeconds) { 115 | // cancel the scan before the timeout with stopScanning() 116 | stopScanRequest = false; 117 | 118 | auto wifiDevices = getWifiDevices(); 119 | if (wifiDevices.empty()) { 120 | debug::log(ERR, "No Wi-Fi devices found"); 121 | return; 122 | } 123 | 124 | for (auto& devicePath : wifiDevices) { 125 | auto devProxy = 126 | sdbus::createProxy(*connection, sdbus::ServiceName("org.freedesktop.NetworkManager"), devicePath); 127 | 128 | // setup signal handler for newly discovered APs 129 | devProxy->uponSignal("AccessPointAdded") 130 | .onInterface("org.freedesktop.NetworkManager.Device.Wireless") 131 | .call([&, callback](sdbus::ObjectPath apPath) { 132 | try { 133 | auto apProxy = 134 | sdbus::createProxy(*connection, sdbus::ServiceName("org.freedesktop.NetworkManager"), apPath); 135 | 136 | sdbus::Variant ssidVar; 137 | apProxy->callMethod("Get") 138 | .onInterface("org.freedesktop.DBus.Properties") 139 | .withArguments("org.freedesktop.NetworkManager.AccessPoint", "Ssid") 140 | .storeResultsTo(ssidVar); 141 | auto ssidBytes = ssidVar.get>(); 142 | std::string ssid(ssidBytes.begin(), ssidBytes.end()); 143 | 144 | sdbus::Variant strengthVar; 145 | apProxy->callMethod("Get") 146 | .onInterface("org.freedesktop.DBus.Properties") 147 | .withArguments("org.freedesktop.NetworkManager.AccessPoint", "Strength") 148 | .storeResultsTo(strengthVar); 149 | int strength = static_cast(strengthVar.get()); 150 | 151 | if (!ssid.empty()) { 152 | callback({ssid, strength}); 153 | } 154 | } catch (const sdbus::Error& e) { 155 | // Ignore errors 156 | } 157 | }); 158 | 159 | // Request scan 160 | try { 161 | devProxy->callMethod("RequestScan") 162 | .onInterface("org.freedesktop.NetworkManager.Device.Wireless") 163 | .withArguments(std::map{}); 164 | } catch (const sdbus::Error& e) { 165 | debug::log(ERR, "RequestScan failed on device {}: {}", devicePath.c_str(), e.getMessage()); 166 | } 167 | 168 | // Process events until timeout 169 | auto startTime = std::chrono::steady_clock::now(); 170 | auto timeout = std::chrono::seconds(timeoutSeconds); 171 | 172 | while (std::chrono::steady_clock::now() - startTime < timeout && !stopScanRequest) { 173 | connection->processPendingEvent(); 174 | std::this_thread::sleep_for(std::chrono::milliseconds(50)); 175 | } 176 | } 177 | } 178 | 179 | // connect to network 180 | bool NetworkManagerClient::connectToNetwork(const std::string& ssid, 181 | const std::string& password, 182 | std::function statusCallback) { 183 | 184 | auto wifiDevices = getWifiDevices(); 185 | if (wifiDevices.empty()) 186 | return false; 187 | 188 | if (connectionProxy) { 189 | connectionProxy.reset(); 190 | } 191 | 192 | auto devicePath = wifiDevices[0]; // pick first wifi device for now 193 | auto apPaths = getAccessPoints(devicePath); 194 | sdbus::ObjectPath apPath("/"); // can optionally pick specific AP 195 | 196 | // Convert ssid to byte array 197 | std::vector ssidBytes(ssid.begin(), ssid.end()); 198 | 199 | std::map> conMap; 200 | conMap["connection"] = {{"id", sdbus::Variant(ssid)}, 201 | {"type", sdbus::Variant("802-11-wireless")}, 202 | {"uuid", sdbus::Variant(generateUuid())}}; 203 | 204 | conMap["802-11-wireless"] = {{"ssid", sdbus::Variant(ssidBytes)}, {"mode", sdbus::Variant("infrastructure")}}; 205 | 206 | if (!password.empty()) { 207 | conMap["802-11-wireless-security"] = {{"key-mgmt", sdbus::Variant("wpa-psk")}, 208 | {"psk", sdbus::Variant(password)}}; 209 | } 210 | 211 | try { 212 | sdbus::ObjectPath activeConnection, resultDevice; 213 | proxy->callMethod("AddAndActivateConnection") 214 | .onInterface("org.freedesktop.NetworkManager") 215 | .withArguments(conMap, devicePath, apPath) 216 | .storeResultsTo(activeConnection, resultDevice); 217 | 218 | // create proxy for the active connection 219 | connectionProxy = 220 | sdbus::createProxy(*connection, sdbus::ServiceName("org.freedesktop.NetworkManager"), devicePath); 221 | 222 | // register signal handler for state changes 223 | connectionProxy->uponSignal("StateChanged") 224 | .onInterface("org.freedesktop.NetworkManager.Device") 225 | .call([this, statusCallback](uint32_t newState, uint32_t oldState, uint32_t reason) { 226 | if (statusCallback) { 227 | switch (newState) { 228 | case 40: 229 | statusCallback(ConnectionState::DISCONNECTED, "Disconnected"); 230 | break; 231 | case 50: 232 | statusCallback(ConnectionState::ACTIVATING, "Connecting..."); 233 | break; 234 | case 60: 235 | statusCallback(ConnectionState::CONFIGURING, "Configuring..."); 236 | break; 237 | case 70: 238 | statusCallback(ConnectionState::AUTHENTICATING, "Awaiting authentication..."); 239 | break; 240 | case 100: 241 | statusCallback(ConnectionState::ACTIVATED, "Connected"); 242 | break; 243 | case 120: 244 | statusCallback(ConnectionState::FAILED, "Failed"); 245 | break; 246 | default: 247 | statusCallback(ConnectionState::UNKNOWN, "Unknown"); 248 | break; 249 | } 250 | } 251 | }); 252 | return true; 253 | } catch (const sdbus::Error& e) { 254 | debug::log(ERR, "Failed to connect to network {}: {}", ssid, e.getMessage()); 255 | return false; 256 | } 257 | } 258 | -------------------------------------------------------------------------------- /src/wayland/input.cpp: -------------------------------------------------------------------------------- 1 | #include "input.hpp" 2 | 3 | extern "C" { 4 | #include 5 | } 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | // keycode to ImGuiKey mapping 12 | static ImGuiKey ImGui_ImplWayland_KeycodeToImGuiKey(uint32_t keycode); 13 | 14 | // map xkb keycode to ImGuiKey 15 | static const struct { 16 | xkb_keycode_t xkbKeycode; 17 | ImGuiKey imguiKey; 18 | } KeyMap[] = { 19 | {XKB_KEY_Return, ImGuiKey_Enter}, 20 | {XKB_KEY_Tab, ImGuiKey_Tab}, 21 | {XKB_KEY_Left, ImGuiKey_LeftArrow}, 22 | {XKB_KEY_Right, ImGuiKey_RightArrow}, 23 | {XKB_KEY_Up, ImGuiKey_UpArrow}, 24 | {XKB_KEY_Down, ImGuiKey_DownArrow}, 25 | {XKB_KEY_Page_Up, ImGuiKey_PageUp}, 26 | {XKB_KEY_Page_Down, ImGuiKey_PageDown}, 27 | {XKB_KEY_Home, ImGuiKey_Home}, 28 | {XKB_KEY_End, ImGuiKey_End}, 29 | {XKB_KEY_Delete, ImGuiKey_Delete}, 30 | {XKB_KEY_BackSpace, ImGuiKey_Backspace}, 31 | {XKB_KEY_space, ImGuiKey_Space}, 32 | {XKB_KEY_Insert, ImGuiKey_Insert}, 33 | {XKB_KEY_Escape, ImGuiKey_Escape}, 34 | {XKB_KEY_Control_L, ImGuiKey_LeftCtrl}, 35 | {XKB_KEY_Control_R, ImGuiKey_RightCtrl}, 36 | {XKB_KEY_Shift_L, ImGuiKey_LeftShift}, 37 | {XKB_KEY_Shift_R, ImGuiKey_RightShift}, 38 | {XKB_KEY_Alt_L, ImGuiKey_LeftAlt}, 39 | {XKB_KEY_Alt_R, ImGuiKey_RightAlt}, 40 | {XKB_KEY_Super_L, ImGuiKey_LeftSuper}, 41 | {XKB_KEY_Super_R, ImGuiKey_RightSuper}, 42 | {XKB_KEY_Menu, ImGuiKey_Menu}, 43 | {XKB_KEY_0, ImGuiKey_0}, 44 | {XKB_KEY_1, ImGuiKey_1}, 45 | {XKB_KEY_2, ImGuiKey_2}, 46 | {XKB_KEY_3, ImGuiKey_3}, 47 | {XKB_KEY_4, ImGuiKey_4}, 48 | {XKB_KEY_5, ImGuiKey_5}, 49 | {XKB_KEY_6, ImGuiKey_6}, 50 | {XKB_KEY_7, ImGuiKey_7}, 51 | {XKB_KEY_8, ImGuiKey_8}, 52 | {XKB_KEY_9, ImGuiKey_9}, 53 | {XKB_KEY_a, ImGuiKey_A}, 54 | {XKB_KEY_b, ImGuiKey_B}, 55 | {XKB_KEY_c, ImGuiKey_C}, 56 | {XKB_KEY_d, ImGuiKey_D}, 57 | {XKB_KEY_e, ImGuiKey_E}, 58 | {XKB_KEY_f, ImGuiKey_F}, 59 | {XKB_KEY_g, ImGuiKey_G}, 60 | {XKB_KEY_h, ImGuiKey_H}, 61 | {XKB_KEY_i, ImGuiKey_I}, 62 | {XKB_KEY_j, ImGuiKey_J}, 63 | {XKB_KEY_k, ImGuiKey_K}, 64 | {XKB_KEY_l, ImGuiKey_L}, 65 | {XKB_KEY_m, ImGuiKey_M}, 66 | {XKB_KEY_n, ImGuiKey_N}, 67 | {XKB_KEY_o, ImGuiKey_O}, 68 | {XKB_KEY_p, ImGuiKey_P}, 69 | {XKB_KEY_q, ImGuiKey_Q}, 70 | {XKB_KEY_r, ImGuiKey_R}, 71 | {XKB_KEY_s, ImGuiKey_S}, 72 | {XKB_KEY_t, ImGuiKey_T}, 73 | {XKB_KEY_u, ImGuiKey_U}, 74 | {XKB_KEY_v, ImGuiKey_V}, 75 | {XKB_KEY_w, ImGuiKey_W}, 76 | {XKB_KEY_x, ImGuiKey_X}, 77 | {XKB_KEY_y, ImGuiKey_Y}, 78 | {XKB_KEY_z, ImGuiKey_Z}, 79 | {XKB_KEY_F1, ImGuiKey_F1}, 80 | {XKB_KEY_F2, ImGuiKey_F2}, 81 | {XKB_KEY_F3, ImGuiKey_F3}, 82 | {XKB_KEY_F4, ImGuiKey_F4}, 83 | {XKB_KEY_F5, ImGuiKey_F5}, 84 | {XKB_KEY_F6, ImGuiKey_F6}, 85 | {XKB_KEY_F7, ImGuiKey_F7}, 86 | {XKB_KEY_F8, ImGuiKey_F8}, 87 | {XKB_KEY_F9, ImGuiKey_F9}, 88 | {XKB_KEY_F10, ImGuiKey_F10}, 89 | {XKB_KEY_F11, ImGuiKey_F11}, 90 | {XKB_KEY_F12, ImGuiKey_F12}, 91 | {XKB_KEY_semicolon, ImGuiKey_Semicolon}, 92 | {XKB_KEY_equal, ImGuiKey_Equal}, 93 | {XKB_KEY_comma, ImGuiKey_Comma}, 94 | {XKB_KEY_minus, ImGuiKey_Minus}, 95 | {XKB_KEY_period, ImGuiKey_Period}, 96 | {XKB_KEY_slash, ImGuiKey_Slash}, 97 | {XKB_KEY_grave, ImGuiKey_GraveAccent}, 98 | {XKB_KEY_bracketleft, ImGuiKey_LeftBracket}, 99 | {XKB_KEY_backslash, ImGuiKey_Backslash}, 100 | {XKB_KEY_bracketright, ImGuiKey_RightBracket}, 101 | {XKB_KEY_apostrophe, ImGuiKey_Apostrophe}, 102 | }; 103 | 104 | namespace wl { 105 | InputHandler::InputHandler(wl_seat* seat) : seat(seat), io(nullptr) { 106 | wl_seat_add_listener(seat, &seatListener, this); 107 | context = xkb_context_new(XKB_CONTEXT_NO_FLAGS); 108 | } 109 | 110 | InputHandler::InputHandler(wl_seat* seat, ImGuiIO* io) : seat(seat), io(io) { 111 | wl_seat_add_listener(seat, &seatListener, this); 112 | context = xkb_context_new(XKB_CONTEXT_NO_FLAGS); 113 | } 114 | 115 | InputHandler::~InputHandler() { 116 | if (keyboard) { 117 | wl_keyboard_destroy(keyboard); 118 | keyboard = nullptr; 119 | } 120 | if (pointer) { 121 | wl_pointer_destroy(pointer); 122 | pointer = nullptr; 123 | } 124 | if (state) { 125 | xkb_state_unref(state); 126 | state = nullptr; 127 | } 128 | if (keymap) { 129 | xkb_keymap_unref(keymap); 130 | keymap = nullptr; 131 | } 132 | if (context) { 133 | xkb_context_unref(context); 134 | context = nullptr; 135 | } 136 | } 137 | 138 | void InputHandler::setWindowBounds(int w, int h) { 139 | this->width = w; 140 | this->height = h; 141 | } 142 | 143 | // calback for the wayland seat capabilities event 144 | void InputHandler::seatCapabilities(void* data, wl_seat* seat, uint32_t capabilities) { 145 | InputHandler* self = static_cast(data); 146 | 147 | // check if the seat has pointer capabilities 148 | if (capabilities & WL_SEAT_CAPABILITY_POINTER) { 149 | self->pointer = wl_seat_get_pointer(seat); 150 | static const wl_pointer_listener pointerListener = {.enter = pointerEnter, 151 | .leave = pointerLeave, 152 | .motion = pointerMotion, 153 | .button = pointerButton, 154 | .axis = pointerAxis, 155 | .frame = pointerFrame, 156 | .axis_source = pointerAxisSource, 157 | .axis_stop = pointerAxisStop, 158 | .axis_discrete = pointerAxisDiscrete}; 159 | wl_pointer_add_listener(self->pointer, &pointerListener, self); 160 | } 161 | 162 | // check if the seat has keyboard capabilities 163 | if (capabilities & WL_SEAT_CAPABILITY_KEYBOARD) { 164 | self->keyboard = wl_seat_get_keyboard(seat); 165 | static const wl_keyboard_listener keyboardListener = {.keymap = keyboardKeymap, 166 | .enter = keyboardEnter, 167 | .leave = keyboardLeave, 168 | .key = keyboardKey, 169 | .modifiers = keyboardModifiers, 170 | .repeat_info = keyboardRepeatInfo}; 171 | wl_keyboard_add_listener(self->keyboard, &keyboardListener, self); 172 | } 173 | } 174 | 175 | void InputHandler::seatName(void*, wl_seat*, const char*) {} 176 | 177 | void InputHandler::pointerEnter(void* data, wl_pointer*, uint32_t, wl_surface*, wl_fixed_t sx, wl_fixed_t sy) { 178 | InputHandler* self = static_cast(data); 179 | self->io->MousePos = ImVec2((float)wl_fixed_to_int(sx), (float)wl_fixed_to_int(sy)); 180 | } 181 | 182 | void InputHandler::pointerLeave(void*, wl_pointer*, uint32_t, wl_surface*) {} 183 | 184 | void InputHandler::pointerMotion(void* data, wl_pointer*, uint32_t, wl_fixed_t sx, wl_fixed_t sy) { 185 | InputHandler* self = static_cast(data); 186 | self->io->MousePos = ImVec2((float)wl_fixed_to_int(sx), (float)wl_fixed_to_int(sy)); 187 | } 188 | 189 | void InputHandler::pointerButton(void* data, wl_pointer*, uint32_t, uint32_t, uint32_t button, uint32_t state) { 190 | InputHandler* self = static_cast(data); 191 | 192 | // handle mouse state for ImGui 193 | bool pressed = (state == WL_POINTER_BUTTON_STATE_PRESSED); 194 | if (button == BTN_LEFT) 195 | self->io->MouseDown[0] = pressed; 196 | else if (button == BTN_RIGHT) 197 | self->io->MouseDown[1] = pressed; 198 | 199 | // if clicked outside the window with either, request exit 200 | if (pressed && (button == BTN_LEFT || button == BTN_RIGHT)) { 201 | float x = self->io->MousePos.x; 202 | float y = self->io->MousePos.y; 203 | if (x < 0 || x >= self->width || y < 0 || y >= self->height) { 204 | self->shouldExit_ = true; 205 | } 206 | } 207 | } 208 | 209 | void InputHandler::pointerAxis(void* data, wl_pointer*, uint32_t, uint32_t axis, wl_fixed_t value) { 210 | InputHandler* self = static_cast(data); 211 | if (axis == WL_POINTER_AXIS_VERTICAL_SCROLL) 212 | self->io->MouseWheel += wl_fixed_to_double(value); 213 | if (axis == WL_POINTER_AXIS_HORIZONTAL_SCROLL) 214 | self->io->MouseWheelH += wl_fixed_to_double(value); 215 | } 216 | 217 | void InputHandler::pointerFrame(void*, wl_pointer*) {} 218 | void InputHandler::pointerAxisSource(void*, wl_pointer*, uint32_t) {} 219 | void InputHandler::pointerAxisStop(void*, wl_pointer*, uint32_t, uint32_t) {} 220 | 221 | void InputHandler::pointerAxisDiscrete(void* data, wl_pointer*, uint32_t axis, int32_t discrete) { 222 | InputHandler* self = static_cast(data); 223 | if (axis == WL_POINTER_AXIS_VERTICAL_SCROLL) 224 | self->io->MouseWheel += (float)discrete; 225 | if (axis == WL_POINTER_AXIS_HORIZONTAL_SCROLL) 226 | self->io->MouseWheelH += (float)discrete; 227 | } 228 | 229 | void InputHandler::keyboardKeymap(void* data, wl_keyboard*, uint32_t format, int32_t fd, uint32_t size) { 230 | InputHandler* self = static_cast(data); 231 | 232 | if (!self->context) { 233 | close(fd); 234 | return; 235 | } 236 | 237 | if (format != WL_KEYBOARD_KEYMAP_FORMAT_XKB_V1) { 238 | close(fd); 239 | return; 240 | } 241 | 242 | char* map_str = static_cast(mmap(nullptr, size, PROT_READ, MAP_SHARED, fd, 0)); 243 | if (map_str == MAP_FAILED) { 244 | close(fd); 245 | return; 246 | } 247 | 248 | // free old keymap if it exists 249 | if (self->keymap) { 250 | xkb_keymap_unref(self->keymap); 251 | self->keymap = nullptr; 252 | } 253 | 254 | if (self->state) { 255 | xkb_state_unref(self->state); 256 | self->state = nullptr; 257 | } 258 | 259 | // create new keymap and state 260 | self->keymap = 261 | xkb_keymap_new_from_string(self->context, map_str, XKB_KEYMAP_FORMAT_TEXT_V1, XKB_KEYMAP_COMPILE_NO_FLAGS); 262 | 263 | munmap(map_str, size); 264 | close(fd); 265 | 266 | if (!self->keymap) { 267 | return; 268 | } 269 | 270 | self->state = xkb_state_new(self->keymap); 271 | if (!self->state) { 272 | xkb_keymap_unref(self->keymap); 273 | self->keymap = nullptr; 274 | return; 275 | } 276 | 277 | // get modifier masks 278 | xkb_keycode_t minKeycode = xkb_keymap_min_keycode(self->keymap); 279 | xkb_keycode_t maxKeycode = xkb_keymap_max_keycode(self->keymap); 280 | 281 | for (xkb_keycode_t keycode = minKeycode; keycode <= maxKeycode; ++keycode) { 282 | xkb_layout_index_t num_layouts = xkb_keymap_num_layouts_for_key(self->keymap, keycode); 283 | for (xkb_layout_index_t layout = 0; layout < num_layouts; ++layout) { 284 | const xkb_keysym_t* syms; 285 | int nsyms = xkb_keymap_key_get_syms_by_level(self->keymap, keycode, layout, 0, &syms); 286 | 287 | for (int i = 0; i < nsyms; ++i) { 288 | xkb_keysym_t sym = syms[i]; 289 | if (sym == XKB_KEY_Control_L || sym == XKB_KEY_Control_R) { 290 | self->controlMask = 1 << xkb_keymap_mod_get_index(self->keymap, XKB_MOD_NAME_CTRL); 291 | } else if (sym == XKB_KEY_Shift_L || sym == XKB_KEY_Shift_R) { 292 | self->shiftMask = 1 << xkb_keymap_mod_get_index(self->keymap, XKB_MOD_NAME_SHIFT); 293 | } else if (sym == XKB_KEY_Alt_L || sym == XKB_KEY_Alt_R) { 294 | self->altMask = 1 << xkb_keymap_mod_get_index(self->keymap, XKB_MOD_NAME_ALT); 295 | } else if (sym == XKB_KEY_Super_L || sym == XKB_KEY_Super_R) { 296 | self->superMask = 1 << xkb_keymap_mod_get_index(self->keymap, XKB_MOD_NAME_LOGO); 297 | } 298 | } 299 | } 300 | } 301 | } 302 | 303 | void InputHandler::keyboardEnter(void* data, wl_keyboard*, uint32_t, wl_surface*, wl_array* keys) { 304 | InputHandler* self = static_cast(data); 305 | if (!self->io) 306 | return; 307 | self->io->AddFocusEvent(true); 308 | } 309 | 310 | void InputHandler::keyboardLeave(void* data, wl_keyboard*, uint32_t, wl_surface*) { 311 | InputHandler* self = static_cast(data); 312 | if (!self->io) 313 | return; 314 | self->io->AddFocusEvent(false); 315 | } 316 | 317 | void InputHandler::keyboardKey(void* data, wl_keyboard*, uint32_t, uint32_t, uint32_t key, uint32_t state) { 318 | InputHandler* self = static_cast(data); 319 | if (!self->io) 320 | return; 321 | 322 | // key events are passed as evdev codes, which are offset by 8 from xkb codes 323 | xkb_keycode_t keycode = key + 8; 324 | bool pressed = (state == WL_KEYBOARD_KEY_STATE_PRESSED); 325 | 326 | // update the key state in xkb 327 | if (self->state) { 328 | xkb_state_update_key(self->state, keycode, pressed ? XKB_KEY_DOWN : XKB_KEY_UP); 329 | } 330 | 331 | // handle the key press/release 332 | self->handleKey(key, pressed); 333 | } 334 | 335 | void InputHandler::keyboardModifiers(void* data, 336 | wl_keyboard*, 337 | uint32_t, 338 | uint32_t modsDepressed, 339 | uint32_t modsLatched, 340 | uint32_t modsLocked, 341 | uint32_t group) { 342 | InputHandler* self = static_cast(data); 343 | if (!self->state || !self->io) 344 | return; 345 | 346 | // update the xkb state with the new modifiers 347 | xkb_state_update_mask(self->state, modsDepressed, modsLatched, modsLocked, 0, 0, group); 348 | 349 | // update modifier key states 350 | bool ctrl = (modsDepressed & self->controlMask) || (modsLatched & self->controlMask) || 351 | (modsLocked & self->controlMask); 352 | bool shift = 353 | (modsDepressed & self->shiftMask) || (modsLatched & self->shiftMask) || (modsLocked & self->shiftMask); 354 | bool alt = (modsDepressed & self->altMask) || (modsLatched & self->altMask) || (modsLocked & self->altMask); 355 | bool super = 356 | (modsDepressed & self->superMask) || (modsLatched & self->superMask) || (modsLocked & self->superMask); 357 | 358 | // update modifier keys 359 | self->io->AddKeyEvent(ImGuiMod_Ctrl, ctrl); 360 | self->io->AddKeyEvent(ImGuiMod_Shift, shift); 361 | self->io->AddKeyEvent(ImGuiMod_Alt, alt); 362 | self->io->AddKeyEvent(ImGuiMod_Super, super); 363 | } 364 | 365 | void InputHandler::keyboardRepeatInfo(void* data, wl_keyboard*, int32_t rate, int32_t delay) { 366 | InputHandler* self = static_cast(data); 367 | self->repeatRate = rate; 368 | self->repeatDelay = delay; 369 | } 370 | 371 | void InputHandler::handleKey(uint32_t key, bool pressed) { 372 | if (!io) 373 | return; 374 | 375 | if (state && keymap) { 376 | xkb_keycode_t keycode = key + 8; // convert to xkb keycode 377 | xkb_keysym_t sym = xkb_state_key_get_one_sym(state, keycode); 378 | 379 | // map to ImGuiKey 380 | ImGuiKey imguiKey = ImGuiKey_None; 381 | for (const auto& mapping : KeyMap) { 382 | if (mapping.xkbKeycode == sym) { 383 | imguiKey = mapping.imguiKey; 384 | break; 385 | } 386 | } 387 | 388 | // update key state 389 | if (imguiKey != ImGuiKey_None) { 390 | io->AddKeyEvent(imguiKey, pressed); 391 | } 392 | 393 | // get the utf-8 character for text input 394 | if (pressed) { 395 | char buffer[16]; 396 | int size = xkb_state_key_get_utf8(state, keycode, buffer, sizeof(buffer)); 397 | if (size > 0) { 398 | buffer[size] = '\0'; 399 | io->AddInputCharactersUTF8(buffer); 400 | } 401 | } 402 | } 403 | } 404 | 405 | // map xkb keysyms to ImGuiKey 406 | static ImGuiKey ImGui_ImplWayland_KeycodeToImGuiKey(uint32_t keycode) { 407 | for (const auto& mapping : KeyMap) { 408 | if (mapping.xkbKeycode == keycode) { 409 | return mapping.imguiKey; 410 | } 411 | } 412 | return ImGuiKey_None; 413 | } 414 | 415 | } // namespace wl 416 | --------------------------------------------------------------------------------