Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,45 @@ submap = reset
bind=$mainMod,g,exec,hyprctl keyword cursor:inactive_timeout 0; hyprctl keyword cursor:hide_on_key_press false; hyprctl dispatch submap cursor
```

## Multi-monitor support

By default, `wl-kbptr` shows its overlay only on the currently focused output. The `--all-outputs` / `-A` flag spans the overlay across all connected outputs simultaneously, so you don't need to focus the right display before invoking it.

> **Note:** Multi-monitor mode is implemented for **tile mode** and **floating mode**. For floating mode with `mode_floating.source=detect`, targets are detected on each output independently and combined. For floating mode with stdin input, pass areas in global coordinates.

### Usage

```bash
wl-kbptr -A -o modes=tile,click
```

Or enable it permanently in your configuration file:

```ini
[general]
all_outputs=true
```

### How it works

- One overlay surface is created per output, each covering its respective monitor.
- The first surface gets exclusive keyboard focus; the compositor routes all key events there regardless of which monitor the cursor is on.
- Each monitor is assigned its **exclusive pixels** — its full logical bounds minus any area that overlaps with a previously processed monitor. Labels are indexed continuously across all resulting regions.
- After you type a label, the cursor moves to the correct output automatically.

This exclusive-region approach handles arbitrary overlap topologies correctly:

| Layout | Behaviour |
| ------ | --------- |
| Side-by-side / stacked (no overlap) | One region per monitor, as expected. |
| Corner overlap | The first monitor keeps its full area; the second monitor's exclusive area is 2 strips (the non-overlapping edges). The shared corner belongs to the first monitor. |
| Landscape + portrait overlap | The first-listed monitor keeps its full area. If landscape is first, the portrait monitor covers only the strip extending beyond the landscape area. If portrait is first, landscape gets left/right columns beside the portrait. |
| Full mirror (same logical position) | The second monitor has no exclusive area and receives no labels. Its overlay surface shows only the background tint. |

### Cell density

Cell size is computed from the **average logical monitor area**, keeping density consistent with single-output mode — each monitor gets roughly the same number of cells as it would on its own. With multiple monitors the total label count scales with the number of outputs, so labels may require more keystrokes (e.g. 3 characters with 3 monitors).

## Configuration

`wl-kbptr` can be configured with a configuration file. See [`config.example`](./config.example) for an example and run `wl-kbptr --help-config` for help.
Expand Down
3 changes: 3 additions & 0 deletions config.example
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@
home_row_keys=
modes=tile,bisect
cancellation_status_code=0
# Span the overlay across all connected outputs simultaneously (tile and floating modes).
# Equivalent to the -A / --all-outputs command-line flag.
all_outputs=false

[mode_tile]
label_color=#fffd
Expand Down
18 changes: 17 additions & 1 deletion src/config.c
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,21 @@ static int parse_double(void *dest, char *value) {
return 0;
}

static int parse_bool(void *dest, char *value) {
bool *out = dest;
if (strcmp(value, "true") == 0 || strcmp(value, "1") == 0) {
*out = true;
} else if (strcmp(value, "false") == 0 || strcmp(value, "0") == 0) {
*out = false;
} else {
LOG_ERR(
"Invalid boolean value '%s'. Should be 'true' or 'false'.", value
);
return 1;
}
return 0;
}

static int parse_uint8(void *dest, char *value) {
int decoded = atoi(value);
if (decoded < 0 || decoded >= 256) {
Expand Down Expand Up @@ -360,7 +375,8 @@ static struct section_def section_defs[] = {
general,
G_FIELD(home_row_keys, "", parse_home_row_keys, free_home_row_keys),
G_FIELD(modes, "tile,bisect", parse_str, free_str),
G_FIELD(cancellation_status_code, "0", parse_uint8, noop)
G_FIELD(cancellation_status_code, "0", parse_uint8, noop),
G_FIELD(all_outputs, "false", parse_bool, noop)
),
SECTION(
mode_tile, MT_FIELD(label_color, "#fffd", parse_color, noop),
Expand Down
2 changes: 2 additions & 0 deletions src/config.h
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@

#include "utils.h"

#include <stdbool.h>
#include <stdint.h>

struct general_config {
char **home_row_keys;
char *modes;
uint8_t cancellation_status_code;
bool all_outputs;
};

struct relative_font_size {
Expand Down
Loading