From faa6b41a73e3f7ad58683df9b29d2b170357ac90 Mon Sep 17 00:00:00 2001 From: sandwich Date: Fri, 10 Oct 2025 14:15:19 +0200 Subject: [PATCH] bugfix: workspace ids, 10 -> 0, >10 -> alpha --- hyprexpo/dev-link.sh | 197 ------------------------------------------ hyprexpo/main.cpp | 31 +++++++ hyprexpo/overview.cpp | 44 +++++++++- 3 files changed, 73 insertions(+), 199 deletions(-) delete mode 100755 hyprexpo/dev-link.sh diff --git a/hyprexpo/dev-link.sh b/hyprexpo/dev-link.sh deleted file mode 100755 index c56dd2b..0000000 --- a/hyprexpo/dev-link.sh +++ /dev/null @@ -1,197 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -# Link local hyprexpo.so into the hyprpm-installed plugin path for quick testing. -# -# Usage: -# ./dev-link.sh # auto-detect installed hyprexpo.so and symlink to local build -# ./dev-link.sh -b # build first, then link -# ./dev-link.sh -t /path/to/hyprexpo.so # specify target path explicitly -# ./dev-link.sh -r # restore original file from .bak and remove symlink -# -# Notes: -# - This script only affects your local user install if hyprpm installed to XDG_DATA_HOME. -# - If your hyprexpo is system-wide, you may need sudo to replace it. - -here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -local_so="$here/hyprexpo.so" -target_so="" -do_build=0 -do_restore=0 -do_list=0 -do_interactive=0 - -msg() { echo "[dev-link] $*"; } -err() { echo "[dev-link] ERROR: $*" >&2; exit 1; } - -usage() { - sed -n '2,20p' "$BASH_SOURCE" | sed 's/^# \{0,1\}//' -} - -detect_target() { - local data_home="${XDG_DATA_HOME:-$HOME/.local/share}" - local cache_home="${XDG_CACHE_HOME:-$HOME/.cache}" - local candidates=( - "$data_home/hyprpm" - "$cache_home/hyprpm" - "$data_home/hyprland" - "$HOME/.local/lib/hyprland" - "/usr/lib/hyprland" - "/usr/lib64/hyprland" - ) - - for base in "${candidates[@]}"; do - if [[ -d "$base" ]]; then - local found - # Prefer plugin tree - found=$(find "$base" -type f -name hyprexpo.so 2>/dev/null | head -n1 || true) - if [[ -n "${found:-}" ]]; then - echo "$found" - return 0 - fi - fi - done - return 1 -} - -detect_runtime_target() { - # Extract hyprexpo.so from the running Hyprland process mappings - local pid - pid="$(pidof Hyprland 2>/dev/null || pgrep -x Hyprland 2>/dev/null || true)" - [[ -n "$pid" ]] || return 1 - local path - # print the last column (pathname) and stop at first match - path="$(awk '/hyprexpo\.so/ {print $NF; exit}' "/proc/$pid/maps" 2>/dev/null || true)" - if [[ -n "$path" && -f "$path" ]]; then - echo "$path" - return 0 - fi - return 1 -} - -while (( "$#" )); do - case "$1" in - -b|--build) do_build=1; shift ;; - -t|--target) target_so="${2:-}"; shift 2 ;; - -r|--restore) do_restore=1; shift ;; - -l|--list) do_list=1; shift ;; - -i|--interactive) do_interactive=1; shift ;; - -h|--help) usage; exit 0 ;; - *) err "Unknown arg: $1" ;; - esac -done - -if (( do_restore )); then - if [[ -z "$target_so" ]]; then - target_so=$(detect_target || true) - fi - [[ -n "$target_so" ]] || err "Could not detect hyprexpo.so. Pass -t /path/to/hyprexpo.so" - - if [[ -L "$target_so" ]]; then - msg "Removing symlink: $target_so" - rm -f -- "$target_so" - fi - if [[ -f "$target_so.bak" ]]; then - msg "Restoring backup: $target_so.bak -> $target_so" - mv -f -- "$target_so.bak" "$target_so" - else - msg "No backup found at $target_so.bak — nothing to restore" - fi - exit 0 -fi - -if (( do_build )); then - msg "Building hyprexpo.so" - make -C "$here" all -fi - -[[ -f "$local_so" ]] || err "Local build not found: $local_so (run with -b to build)" - -if [[ -z "$target_so" || $do_list -eq 1 || $do_interactive -eq 1 ]]; then - # Prefer the path actually loaded by the running Hyprland instance - target_so_runtime="$(detect_runtime_target || true)" - # Fallback to filesystem-based detection - target_so_fs="$(detect_target || true)" - # try hyprpm output as an additional fallback - target_so_hpm="" - if command -v hyprpm >/dev/null 2>&1; then - out="$(hyprpm list 2>/dev/null || true)" - if [[ -n "$out" ]]; then - target_so_hpm="$(printf '%s\n' "$out" | grep -i hyprexpo | grep -oE '/[^ ]*hyprexpo\.so' | head -n1 || true)" - fi - fi - # last resort: shallow scan - target_so_scan="$(find "$HOME/.local/share" "$HOME/.cache" -maxdepth 7 -type f -name hyprexpo.so 2>/dev/null | head -n1 || true)" - - # collect candidates and filter out the local build if present - local_abs="$(readlink -f "$local_so")" - mapfile -t candidates < <(printf '%s\n' "$target_so_runtime" "$target_so_fs" "$target_so_hpm" "$target_so_scan" | awk 'NF' | awk '!seen[$0]++') - filtered=() - for c in "${candidates[@]}"; do - ca="$(readlink -f "$c" 2>/dev/null || true)" - [[ -n "$ca" && "$ca" != "$local_abs" ]] && filtered+=("$ca") - done - - if (( do_list )); then - if ((${#filtered[@]} == 0)); then - msg "No installed hyprexpo.so found (excluding local build)." - else - printf '%s\n' "${filtered[@]}" - fi - exit 0 - fi - - if (( do_interactive )); then - if ((${#filtered[@]} == 0)); then - err "No installed hyprexpo.so found (excluding local build)." - fi - echo "Select target hyprexpo.so to link:" >&2 - i=1 - for c in "${filtered[@]}"; do - echo " [$i] $c" >&2 - i=$((i+1)) - done - read -rp "> " choice - if ! [[ "$choice" =~ ^[0-9]+$ ]] || (( choice < 1 || choice > ${#filtered[@]} )); then - err "Invalid selection" - fi - target_so="${filtered[$((choice-1))]}" - else - # pick the first filtered candidate if no explicit target provided - if [[ -z "$target_so" && ${#filtered[@]} -gt 0 ]]; then - target_so="${filtered[0]}" - fi - fi -fi - -[[ -n "$target_so" ]] || err "Could not detect hyprexpo.so. Pass -t /path/to/hyprexpo.so" - -msg "Target: $target_so" - -local_abs="$(readlink -f "$local_so")" -target_abs="$(readlink -f "$target_so" 2>/dev/null || true)" -if [[ -n "$target_abs" && "$target_abs" == "$local_abs" ]]; then - msg "Target is the local build; nothing to link." - exit 0 -fi - -if [[ -L "$target_so" ]]; then - current_link="$(readlink -f "$target_so")" - if [[ "$current_link" == "$local_abs" ]]; then - msg "Already linked to local build. Done." - exit 0 - else - msg "Target is a symlink to $current_link — replacing" - rm -f -- "$target_so" - fi -fi - -if [[ -f "$target_so" ]]; then - msg "Backing up existing file to $target_so.bak" - cp -f -- "$target_so" "$target_so.bak" -fi - -msg "Linking $target_so -> $local_so" -ln -sf "$local_so" "$target_so" - -msg "Done. Restart Hyprland to load the local build." diff --git a/hyprexpo/main.cpp b/hyprexpo/main.cpp index 9d2f9a1..15d94f3 100644 --- a/hyprexpo/main.cpp +++ b/hyprexpo/main.cpp @@ -15,6 +15,7 @@ using namespace Hyprutils::String; #include +#include #include "globals.hpp" #include "overview.hpp" @@ -42,6 +43,7 @@ static bool renderingOverview = false; static SDispatchResult onKbFocusDispatcher(std::string arg); static SDispatchResult onKbConfirmDispatcher(std::string arg); static SDispatchResult onKbSelectNumberDispatcher(std::string arg); +static SDispatchResult onKbSelectTokenDispatcher(std::string arg); // static void hkRenderWorkspace(void* thisptr, PHLMONITOR pMonitor, PHLWORKSPACE pWorkspace, timespec* now, const CBox& geometry) { @@ -251,6 +253,7 @@ APICALL EXPORT PLUGIN_DESCRIPTION_INFO PLUGIN_INIT(HANDLE handle) { HyprlandAPI::addDispatcherV2(PHANDLE, "hyprexpo:kb_focus", ::onKbFocusDispatcher); HyprlandAPI::addDispatcherV2(PHANDLE, "hyprexpo:kb_confirm", ::onKbConfirmDispatcher); HyprlandAPI::addDispatcherV2(PHANDLE, "hyprexpo:kb_selectn", ::onKbSelectNumberDispatcher); + HyprlandAPI::addDispatcherV2(PHANDLE, "hyprexpo:kb_select", ::onKbSelectTokenDispatcher); HyprlandAPI::addConfigKeyword(PHANDLE, "hyprexpo-gesture", ::expoGestureKeyword, {}); @@ -273,6 +276,7 @@ APICALL EXPORT PLUGIN_DESCRIPTION_INFO PLUGIN_INIT(HANDLE handle) { HyprlandAPI::addConfigValue(PHANDLE, "plugin:hyprexpo:label_enable", Hyprlang::INT{1}); HyprlandAPI::addConfigValue(PHANDLE, "plugin:hyprexpo:label_color", Hyprlang::INT{0xFFFFFFFF}); HyprlandAPI::addConfigValue(PHANDLE, "plugin:hyprexpo:label_font_size", Hyprlang::INT{16}); + HyprlandAPI::addConfigValue(PHANDLE, "plugin:hyprexpo:label_text_mode", Hyprlang::STRING{"id"}); // defaults: center/middle within the label container HyprlandAPI::addConfigValue(PHANDLE, "plugin:hyprexpo:label_position", Hyprlang::STRING{"center"}); HyprlandAPI::addConfigValue(PHANDLE, "plugin:hyprexpo:label_offset_x", Hyprlang::INT{0}); @@ -368,3 +372,30 @@ static SDispatchResult onKbSelectNumberDispatcher(std::string arg) { g_pOverview->onKbSelectNumber(num); return {}; } + +static std::optional tokenToIndex(const std::string& s) { + if (s.size() != 1) + return std::nullopt; + const char c = s[0]; + if (c >= '1' && c <= '9') + return (c - '1'); + if (c == '0') + return 9; + if (c >= 'a' && c <= 'z') + return 10 + (c - 'a'); + if (c >= 'A' && c <= 'Z') + return 10 + (c - 'A'); + return std::nullopt; +} + +static SDispatchResult onKbSelectTokenDispatcher(std::string arg) { + if (!g_pOverview) + return {}; + while (!arg.empty() && std::isspace(arg.front())) arg.erase(arg.begin()); + while (!arg.empty() && std::isspace(arg.back())) arg.pop_back(); + const auto idx = tokenToIndex(arg); + if (!idx) + return {.success = false, .error = "invalid token (expected 1..9, 0, a..z)"}; + g_pOverview->onKbSelectToken(*idx); + return {}; +} diff --git a/hyprexpo/overview.cpp b/hyprexpo/overview.cpp index e1249c9..77bd606 100644 --- a/hyprexpo/overview.cpp +++ b/hyprexpo/overview.cpp @@ -489,6 +489,20 @@ int COverview::tileForWorkspaceID(int wsid) const { return -1; } +int COverview::tileForVisibleIndex(int vIdx) const { + if (vIdx < 0) + return -1; + int seen = 0; + for (size_t i = 0; i < images.size(); ++i) { + if (images[i].workspaceID == WORKSPACE_INVALID) + continue; + if (seen == vIdx) + return (int)i; + ++seen; + } + return -1; +} + void COverview::moveFocus(int dx, int dy) { ensureKbFocusInitialized(); if (kbFocusID == -1) @@ -599,6 +613,18 @@ void COverview::onKbSelectNumber(int num) { } } +void COverview::onKbSelectToken(int visibleIdx) { + if (closing) + return; + if (visibleIdx < 0) + return; + const int tid = tileForVisibleIndex(visibleIdx); + if (tid != -1) { + closeOnID = tid; + close(); + } +} + void COverview::redrawID(int id, bool forcelowres) { if (pMonitor->m_activeWorkspace != startedOn && !closing) { // likely user changed. @@ -813,6 +839,7 @@ void COverview::fullRender() { static auto* const* PLABELCOL = (Hyprlang::INT* const*)HyprlandAPI::getConfigValue(PHANDLE, "plugin:hyprexpo:label_color")->getDataStaticPtr(); static auto* const* PLABELSIZE = (Hyprlang::INT* const*)HyprlandAPI::getConfigValue(PHANDLE, "plugin:hyprexpo:label_font_size")->getDataStaticPtr(); static auto const* PLABELPOS = (Hyprlang::STRING const*)HyprlandAPI::getConfigValue(PHANDLE, "plugin:hyprexpo:label_position")->getDataStaticPtr(); + static auto const* PLABELMODE = (Hyprlang::STRING const*)HyprlandAPI::getConfigValue(PHANDLE, "plugin:hyprexpo:label_text_mode")->getDataStaticPtr(); static auto* const* PLABELOX = (Hyprlang::INT* const*)HyprlandAPI::getConfigValue(PHANDLE, "plugin:hyprexpo:label_offset_x")->getDataStaticPtr(); static auto* const* PLABELOY = (Hyprlang::INT* const*)HyprlandAPI::getConfigValue(PHANDLE, "plugin:hyprexpo:label_offset_y")->getDataStaticPtr(); static auto const* PLABELSHOW = (Hyprlang::STRING const*)HyprlandAPI::getConfigValue(PHANDLE, "plugin:hyprexpo:label_show")->getDataStaticPtr(); @@ -893,10 +920,11 @@ void COverview::fullRender() { return CBox{x, y, (double)size.x, (double)size.y}; }; + int tokenCounter = 0; for (size_t y = 0; y < (size_t)SIDE_LENGTH; ++y) { for (size_t x = 0; x < (size_t)SIDE_LENGTH; ++x) { const int id = x + y * SIDE_LENGTH; - if (images[id].workspaceID == WORKSPACE_INVALID || !shouldShow(id)) + if (images[id].workspaceID == WORKSPACE_INVALID) continue; // compute tile box again for label placement @@ -904,7 +932,15 @@ void COverview::fullRender() { tile.scale(pMonitor->m_scale).translate(pos->value()); tile.round(); - const std::string label = std::to_string(images[id].workspaceID); + std::string label; + if (std::string{*PLABELMODE} == "token") { + int k = tokenCounter; + if (k <= 8) label = std::to_string(k + 1); + else if (k == 9) label = "0"; + else label = std::string(1, char('a' + (k - 10))); + } else { + label = std::to_string(images[id].workspaceID); + } const int baseF = std::max(8, (int)**PLABELSIZE); const int st = resolveState(id); @@ -919,6 +955,9 @@ void COverview::fullRender() { } }; + // if label isn't shown per mode, still advance token index + if (!shouldShow(id)) { tokenCounter++; continue; } + CHyprColor col = CHyprColor{(uint64_t)**PLCOLDEF}; float scl = 1.0f; static auto* const* PLPIXELSNAP = (Hyprlang::INT* const*)HyprlandAPI::getConfigValue(PHANDLE, "plugin:hyprexpo:label_pixel_snap")->getDataStaticPtr(); @@ -983,6 +1022,7 @@ void COverview::fullRender() { else drawNoBG(images[id].labelTexDefault, images[id].labelSizeDefault); } + tokenCounter++; } } }