diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 190c36a..2f623f6 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -10,17 +10,25 @@ jobs: container: image: archlinux:base-devel steps: - - uses: actions/checkout@v4 - - name: Install formatter run: | pacman -Syu --noconfirm --needed \ clang \ + cloc \ + git \ make + - uses: actions/checkout@v4 + - name: Check formatting run: make format-check + - name: Check LoC update + run: | + cp README.md /tmp/README.before + make update-loc + cmp -s README.md /tmp/README.before + build: runs-on: ubuntu-latest container: diff --git a/README.md b/README.md index 778343f..a8bc5c6 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ wlroots-based Wayland compositor with virtual outputs and physical cursor continuity. Originally forked from dwl. -`LOC: 7332 total, 2815 vwl.c` +`LOC: 7441 total, 2878 vwl.c` ## Features diff --git a/contrib/README.md b/contrib/README.md new file mode 100644 index 0000000..7e9ac90 --- /dev/null +++ b/contrib/README.md @@ -0,0 +1,26 @@ +# Contrib + +Helper scripts and integration snippets that are useful with `vwl` but are not required by the compositor itself. + +## Waybar + +- `waybar/vwl-reveal`: auto hide/reveal helper driven by `vwlctl subscribe`. + +Install: + +```sh +install -Dm755 contrib/waybar/vwl-reveal ~/.config/waybar/scripts/vwl-reveal +``` + +Run it from your session startup (for example `~/.config/vwl/run`) after starting Waybar: + +```sh +waybar & +sh ~/.config/waybar/scripts/vwl-reveal >/dev/null 2>&1 & +``` + +Waybar config requirements: + +- `"mode": "hide"` +- `"start_hidden": true` + diff --git a/contrib/waybar/vwl-reveal b/contrib/waybar/vwl-reveal new file mode 100755 index 0000000..0cfb0bc --- /dev/null +++ b/contrib/waybar/vwl-reveal @@ -0,0 +1,85 @@ +#!/bin/sh + +# Expects Waybar to start hidden; toggles visibility on reveal-edge state changes. +visible=0 +pidfile="${XDG_RUNTIME_DIR:-/tmp}/vwl-waybar-reveal.pid" +waybar_config="${WAYBAR_CONFIG:-$HOME/.config/waybar/config.jsonc}" +reveal_edge="${VWL_REVEAL_EDGE:-}" + +if [ -f "$pidfile" ]; then + oldpid=$(cat "$pidfile" 2>/dev/null) + if [ -n "$oldpid" ] && kill -0 "$oldpid" 2>/dev/null; then + if ps -p "$oldpid" -o args= 2>/dev/null | grep -q "vwl-reveal"; then + exit 0 + fi + fi +fi +echo "$$" > "$pidfile" +cleanup() { + rm -f "$pidfile" +} +trap cleanup EXIT INT TERM + +if command -v vwlctl >/dev/null 2>&1; then + VWLCTL=vwlctl +elif [ -x /var/home/wegel/work/perso/vwl/vwlctl ]; then + VWLCTL=/var/home/wegel/work/perso/vwl/vwlctl +else + exit 0 +fi + +if ! command -v jq >/dev/null 2>&1; then + exit 0 +fi + +if [ -z "$reveal_edge" ] && [ -r "$waybar_config" ]; then + reveal_edge=$(sed -n 's/^[[:space:]]*"position"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$waybar_config" | head -n1) +fi +case "$reveal_edge" in +top|bottom|left|right) + ;; +*) + reveal_edge=top + ;; +esac + +"$VWLCTL" subscribe \ + | jq --unbuffered -r ' + .state.pointer as $p + | if ($p.reveal_edge? != null) then + ("edge:" + $p.reveal_edge) + elif ($p.reveal_hover? != null) then + ("hover:" + ($p.reveal_hover | tostring)) + else + empty + end + ' \ + | while read -r state; do + case "$state" in + edge:*) + edge=${state#edge:} + if [ "$edge" = "$reveal_edge" ]; then + target=1 + else + target=0 + fi + ;; + hover:true) + target=1 + ;; + hover:false) + target=0 + ;; + *) + continue + ;; + esac + + if [ "$target" -eq "$visible" ]; then + continue + fi + + pkill -USR1 -x waybar >/dev/null 2>&1 || true + visible="$target" + done + diff --git a/docs/ipc.md b/docs/ipc.md index 54ba244..cb33a43 100644 --- a/docs/ipc.md +++ b/docs/ipc.md @@ -90,10 +90,17 @@ Errors use: The snapshot/event state is structured around: +- `pointer`: pointer location metadata for subscribers (for example reveal-hover state) - `outputs`: physical outputs, geometry, active/focused state, active window info - `virtual_outputs`: vout ids, names, workspace mapping, layout, regions - `workspaces`: flat workspace list with visibility/focus/assignment metadata +`pointer` currently includes: + +- `output`: output name under the pointer (or `null`) +- `reveal_hover`: compositor-provided hover state intended for bar/panel reveal automation +- `reveal_edge`: `top`, `bottom`, `left`, `right`, or `null` + Virtual outputs currently expose a single region, but the schema uses `regions[]` so it can represent spanning virtual outputs later without a schema break. ## CLI diff --git a/docs/waybar-howto.md b/docs/waybar-howto.md index 8ae7820..991275a 100644 --- a/docs/waybar-howto.md +++ b/docs/waybar-howto.md @@ -10,10 +10,16 @@ The IPC already exposes the data a bar needs: - focused output - active window title/appid +- pointer reveal-hover state +- pointer reveal edge (`top`/`bottom`/`left`/`right`) - physical outputs - virtual outputs - workspace to virtual-output mapping +## Auto Hide/Reveal + +Use the helper at `contrib/waybar/vwl-reveal` for edge-hover hide/reveal integration with Waybar. + ## Requirements - `vwlctl` in `$PATH` diff --git a/ipc.c b/ipc.c index cd2c037..2373db1 100644 --- a/ipc.c +++ b/ipc.c @@ -14,6 +14,7 @@ #include #include #include +#include #include #include @@ -192,6 +193,23 @@ json_write_box(FILE *fp, const struct wlr_box *box) fprintf(fp, "{\"x\":%d,\"y\":%d,\"width\":%d,\"height\":%d}", box->x, box->y, box->width, box->height); } +static const char * +pointer_reveal_edge_name(int edge) +{ + switch (edge) { + case POINTER_REVEAL_EDGE_TOP: + return "top"; + case POINTER_REVEAL_EDGE_BOTTOM: + return "bottom"; + case POINTER_REVEAL_EDGE_LEFT: + return "left"; + case POINTER_REVEAL_EDGE_RIGHT: + return "right"; + default: + return NULL; + } +} + static void workspace_stats(Workspace *ws, unsigned int *count, int *urgent) { @@ -215,6 +233,7 @@ build_snapshot(void) size_t size = 0; FILE *fp = open_memstream(&buf, &size); Monitor *m; + Monitor *pointer_mon; VirtualOutput *vout; int i; @@ -241,6 +260,24 @@ build_snapshot(void) else fputs("null", fp); + pointer_mon = cursor ? xytomon(cursor->x, cursor->y) : NULL; + fputs(",\"pointer\":{", fp); + fputs("\"output\":", fp); + if (pointer_mon && pointer_mon->wlr_output) + json_write_escaped(fp, pointer_mon->wlr_output->name); + else + fputs("null", fp); + fprintf(fp, ",\"reveal_hover\":%s", ipc_pointer_reveal_hover ? "true" : "false"); + fputs(",\"reveal_edge\":", fp); + { + const char *edge_name = pointer_reveal_edge_name(ipc_pointer_reveal_edge); + if (edge_name) + json_write_escaped(fp, edge_name); + else + fputs("null", fp); + } + fputc('}', fp); + fputs(",\"outputs\":[", fp); { bool first = true; diff --git a/plumbing.c b/plumbing.c index b42bcbc..7524260 100644 --- a/plumbing.c +++ b/plumbing.c @@ -121,6 +121,8 @@ Workspace workspaces[WORKSPACE_COUNT]; Workspace *selws; VirtualOutput *selvout; HoverState hover; +int ipc_pointer_reveal_hover; +int ipc_pointer_reveal_edge; KeyboardGroup *kb_group; unsigned int cursor_mode; Client *grabc; diff --git a/vwl.c b/vwl.c index 350e0af..c3cd12d 100644 --- a/vwl.c +++ b/vwl.c @@ -219,6 +219,8 @@ static void wsload(VirtualOutput *vout, Workspace *ws); static Client *focustopvout(VirtualOutput *vout); static Client *focustoptiledvout(VirtualOutput *vout); static void cursorwarptovout(VirtualOutput *vout); +static int pointer_reveal_edge_for_cursor(Monitor *m, int current_edge); +static void update_pointer_reveal_state(void); /* removed unused voname function */ /* variables */ @@ -243,6 +245,8 @@ static struct wlr_session_lock_manager_v1 *session_lock_mgr; static struct wlr_box sgeom; static bool vt_recovery_mode = false; /* Track if we're recovering from VT switch */ +static const int pointer_reveal_trigger_px = 2; +static const int pointer_reveal_hold_px = 40; /* Global event handlers are now in plumbing.c */ extern struct wl_listener cursor_axis; @@ -1545,6 +1549,71 @@ maximizenotify(struct wl_listener *listener, void *data) wlr_xdg_surface_schedule_configure(c->surface.xdg); } +static int +pointer_reveal_edge_for_cursor(Monitor *m, int current_edge) +{ + double rel_x, rel_y; + double distances[4]; + int edges[4]; + double best = DBL_MAX; + int best_edge = POINTER_REVEAL_EDGE_NONE; + int i; + + if (!m || !cursor || m->monitor_area.width <= 0 || m->monitor_area.height <= 0) + return POINTER_REVEAL_EDGE_NONE; + + rel_x = cursor->x - m->monitor_area.x; + rel_y = cursor->y - m->monitor_area.y; + if (rel_x < 0.0 || rel_y < 0.0 || rel_x > m->monitor_area.width || rel_y > m->monitor_area.height) + return POINTER_REVEAL_EDGE_NONE; + + distances[0] = rel_y; + distances[1] = m->monitor_area.height - rel_y; + distances[2] = rel_x; + distances[3] = m->monitor_area.width - rel_x; + + edges[0] = POINTER_REVEAL_EDGE_TOP; + edges[1] = POINTER_REVEAL_EDGE_BOTTOM; + edges[2] = POINTER_REVEAL_EDGE_LEFT; + edges[3] = POINTER_REVEAL_EDGE_RIGHT; + + for (i = 0; i < 4; i++) { + int edge = edges[i]; + double threshold = (edge == current_edge) ? pointer_reveal_hold_px : pointer_reveal_trigger_px; + double d = distances[i]; + + if (d < 0.0 || d > threshold) + continue; + if (d < best || (d == best && edge == current_edge)) { + best = d; + best_edge = edge; + } + } + + return best_edge; +} + +static void +update_pointer_reveal_state(void) +{ + Monitor *m; + int next_edge = POINTER_REVEAL_EDGE_NONE; + int next_hover = 0; + + if (cursor) { + m = xytomon(cursor->x, cursor->y); + next_edge = pointer_reveal_edge_for_cursor(m, ipc_pointer_reveal_edge); + } + next_hover = next_edge != POINTER_REVEAL_EDGE_NONE; + + if (next_hover == ipc_pointer_reveal_hover && next_edge == ipc_pointer_reveal_edge) + return; + + ipc_pointer_reveal_hover = next_hover; + ipc_pointer_reveal_edge = next_edge; + updateipc(); +} + void motionnotify(uint32_t time, struct wlr_input_device *device, double dx, double dy, double dx_unaccel, double dy_unaccel) { @@ -1586,6 +1655,7 @@ motionnotify(uint32_t time, struct wlr_input_device *device, double dx, double d if (enable_physical_cursor_gap_jumps && prev_mon && cursor_mode == CurNormal && !seat->drag && (!active_constraint || active_constraint->type != WLR_POINTER_CONSTRAINT_V1_LOCKED) && cursorgap(prev_mon, prev_mm_x, prev_mm_y)) { + update_pointer_reveal_state(); return; } @@ -1602,8 +1672,10 @@ motionnotify(uint32_t time, struct wlr_input_device *device, double dx, double d dy = sy_confined - sy; } - if (active_constraint->type == WLR_POINTER_CONSTRAINT_V1_LOCKED) + if (active_constraint->type == WLR_POINTER_CONSTRAINT_V1_LOCKED) { + update_pointer_reveal_state(); return; + } } } @@ -1645,6 +1717,7 @@ motionnotify(uint32_t time, struct wlr_input_device *device, double dx, double d .height = grabc->geom.height}, 1); cursorsync(); + update_pointer_reveal_state(); return; } else if (cursor_mode == CurResize) { resize(grabc, @@ -1654,6 +1727,7 @@ motionnotify(uint32_t time, struct wlr_input_device *device, double dx, double d .height = (int)round(cursor->y) - grabc->geom.y}, 1); cursorsync(); + update_pointer_reveal_state(); return; } @@ -1665,6 +1739,7 @@ motionnotify(uint32_t time, struct wlr_input_device *device, double dx, double d pointerfocus(c, surface, sx, sy, time); cursorsync(); + update_pointer_reveal_state(); } void diff --git a/vwl.h b/vwl.h index 126e361..62f3de7 100644 --- a/vwl.h +++ b/vwl.h @@ -48,6 +48,13 @@ enum { CurNormal, CurPressed, CurMove, CurResize }; /* cursor */ enum { XDGShell, LayerShell, X11 }; /* client types */ enum { LyrBg, LyrBottom, LyrTile, LyrTop, LyrFS, LyrOverlay, LyrBlock, NUM_LAYERS }; /* scene layers */ enum { FS_NONE, FS_VIRTUAL, FS_MONITOR }; /* fullscreen modes */ +enum { + POINTER_REVEAL_EDGE_NONE, + POINTER_REVEAL_EDGE_TOP, + POINTER_REVEAL_EDGE_BOTTOM, + POINTER_REVEAL_EDGE_LEFT, + POINTER_REVEAL_EDGE_RIGHT, +}; /* pointer reveal edge */ enum TabHdrPos { TABHDR_TOP, TABHDR_BOTTOM }; typedef struct TabTitleTransformRule { @@ -341,6 +348,8 @@ extern Workspace workspaces[WORKSPACE_COUNT]; extern Workspace *selws; extern VirtualOutput *selvout; extern HoverState hover; +extern int ipc_pointer_reveal_hover; +extern int ipc_pointer_reveal_edge; extern KeyboardGroup *kb_group; extern unsigned int cursor_mode; extern Client *grabc;