bugfix: workspace ids, 10 -> 0, >10 -> alpha

This commit is contained in:
sandwich 2025-10-10 14:15:19 +02:00
parent 72cd83f8be
commit faa6b41a73
3 changed files with 73 additions and 199 deletions

View file

@ -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."

View file

@ -15,6 +15,7 @@
using namespace Hyprutils::String;
#include <cctype>
#include <optional>
#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<int> 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 {};
}

View file

@ -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++;
}
}
}