fvwm-window-search 2.1.0 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/Makefile +8 -4
- data/README.md +14 -13
- data/activate.c +163 -0
- data/{focus.sh → activate.sh} +1 -1
- data/dmenu.patch +63 -13
- data/fontinfo.c +34 -0
- data/fvwm-window-search +46 -8
- data/lib.c +36 -0
- data/winlist.c +36 -45
- metadata +10 -7
- data/focus.c +0 -80
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 3e0b567a71daca6fd7780719a9228cd34a63f9cdfbf084a4b4c854f13b60c103
|
4
|
+
data.tar.gz: 8b274620b6a4ba391ad4127c91f63bfff85057c971c0678c4850b1a48cdb287b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b354e8b6cfe44214474c39f54ba10a931835c272ced4b315abf7872cd4c33017a6a146b02c4699b4fe67c633a1fb25d45ddc340f1f1f81e7d8af5d9fd3a92a9e
|
7
|
+
data.tar.gz: a41ae5a0816b84a33149b60ca3a2a30556ec7ea443acd07b57a4a258c5276a5797141ba0c2a7cf93fd53d44e3f01ec3f68b16edb4be2a32c1daefc35dc6a68c3
|
data/Makefile
CHANGED
@@ -2,7 +2,7 @@ out := _out
|
|
2
2
|
dmenu := $(out)/dmenu
|
3
3
|
dmenu.commit := 1a13d0465d1a6f4f74bc5b07b04c9bd542f20ba6
|
4
4
|
|
5
|
-
all: $(out)
|
5
|
+
all: $(addprefix $(out)/, .dmenu.build activate winlist fontinfo)
|
6
6
|
|
7
7
|
$(out)/.dmenu.build: $(out)/.dmenu.$(dmenu.commit) dmenu.patch
|
8
8
|
patch -d $(dmenu) -p1 < dmenu.patch
|
@@ -14,11 +14,15 @@ $(out)/.dmenu.$(dmenu.commit):
|
|
14
14
|
git -C $(dmenu) checkout $(dmenu.commit) -q
|
15
15
|
touch $@
|
16
16
|
|
17
|
-
libs := x11
|
18
|
-
LDFLAGS
|
19
|
-
CFLAGS
|
17
|
+
libs := x11
|
18
|
+
LDFLAGS = $(shell pkg-config --libs $(libs))
|
19
|
+
CFLAGS = -g -Wall -Werror $(shell pkg-config --cflags $(libs))
|
20
20
|
$(out)/%: %.c lib.c
|
21
21
|
$(LINK.c) $< $(LOADLIBES) $(LDLIBS) -o $@
|
22
22
|
|
23
|
+
$(out)/activate: libs += jansson
|
24
|
+
$(out)/winlist: libs += jansson
|
25
|
+
$(out)/fontinfo: libs += xft freetype2
|
26
|
+
|
23
27
|
# an empty target to satisfy rubygems
|
24
28
|
install:
|
data/README.md
CHANGED
@@ -7,13 +7,15 @@ Incremental window search & immediate switch to the selected window
|
|
7
7
|
|
8
8
|

|
9
9
|
|
10
|
-
* Should work w/ most stackings X11 window managers.
|
11
|
-
*
|
10
|
+
* Should work w/ most EWMH-compliant stackings X11 window managers.
|
11
|
+
* Filter by window name/resource/class.
|
12
|
+
* Optionally list windows from the current desktop only.
|
13
|
+
* Preserve minimised/shaded window states.
|
12
14
|
|
13
15
|
## Reqs
|
14
16
|
|
15
17
|
* Ruby 2.1+
|
16
|
-
* `dnf install jansson-devel`
|
18
|
+
* `dnf install jansson-devel freetype-devel`
|
17
19
|
|
18
20
|
## Compilation
|
19
21
|
|
@@ -25,10 +27,14 @@ doesn't interfere w/ a system-installed dmenu.
|
|
25
27
|
~~~
|
26
28
|
$ ./fvwm-window-search -h
|
27
29
|
Usage: fvwm-window-search [options]
|
28
|
-
-c path
|
29
|
-
-
|
30
|
+
-c path an alternative path to conf.yaml
|
31
|
+
-d list windows from the current desktop only
|
32
|
+
-r switch to a window only when <Return> is pressed
|
30
33
|
~~~
|
31
34
|
|
35
|
+
To scroll in dmenu (using Up/Down/Home/End/PgUp/PgDown) without
|
36
|
+
windows activation, hold <kbd>Shift</kbd>.
|
37
|
+
|
32
38
|
To customise dmenu or filtering, create a yaml file
|
33
39
|
`$XDG_CONFIG_HOME/fvwm-window-search/conf.yaml`, e.g.:
|
34
40
|
|
@@ -37,7 +43,7 @@ To customise dmenu or filtering, create a yaml file
|
|
37
43
|
dmenu:
|
38
44
|
fn: Monospace-12
|
39
45
|
b: false
|
40
|
-
|
46
|
+
selection_hook_activation_return_key_only: true
|
41
47
|
filter-out:
|
42
48
|
name: ['System Monitor']
|
43
49
|
resource: []
|
@@ -45,8 +51,8 @@ filter-out:
|
|
45
51
|
~~~
|
46
52
|
|
47
53
|
Subkeys in `dmenu` are the usual CLOs for
|
48
|
-
[dmenu(1)][]. `
|
49
|
-
CLO.
|
54
|
+
[dmenu(1)][]. `selection_hook_activation_return_key_only` is an
|
55
|
+
equivalent of `-r` CLO.
|
50
56
|
|
51
57
|
[dmenu(1)]: https://manpages.debian.org/unstable/suckless-tools/dmenu.1.en.html
|
52
58
|
|
@@ -64,11 +70,6 @@ This is not the case with rubygems! The latter generates a stub script
|
|
64
70
|
that invokes `./fvwm-window-search` file. This indirection may add
|
65
71
|
~140ms of additional delay.
|
66
72
|
|
67
|
-
## Bugs
|
68
|
-
|
69
|
-
* Tested only w/ Fvwm3.
|
70
|
-
* No distinction between normal & iconified windows.
|
71
|
-
|
72
73
|
## License
|
73
74
|
|
74
75
|
MIT.
|
data/activate.c
ADDED
@@ -0,0 +1,163 @@
|
|
1
|
+
#include <stdlib.h>
|
2
|
+
#include <err.h>
|
3
|
+
#include <stdio.h>
|
4
|
+
#include <stdbool.h>
|
5
|
+
#include <unistd.h>
|
6
|
+
#include <fcntl.h>
|
7
|
+
#include <string.h>
|
8
|
+
#include <limits.h>
|
9
|
+
#include <libgen.h>
|
10
|
+
#include <sys/stat.h>
|
11
|
+
#include <errno.h>
|
12
|
+
|
13
|
+
#include <X11/Xlib.h>
|
14
|
+
#include <X11/Xatom.h>
|
15
|
+
#include <jansson.h>
|
16
|
+
|
17
|
+
#include "lib.c"
|
18
|
+
|
19
|
+
ulong str2id(const char *s) {
|
20
|
+
ulong id;
|
21
|
+
if (sscanf(s, "0x%lx", &id) != 1 &&
|
22
|
+
sscanf(s, "0X%lx", &id) != 1 &&
|
23
|
+
sscanf(s, "%lu", &id) != 1) return 0;
|
24
|
+
return id;
|
25
|
+
}
|
26
|
+
|
27
|
+
bool client_msg(Display *dpy, Window id, const char *msg,
|
28
|
+
unsigned long data0, unsigned long data1,
|
29
|
+
unsigned long data2, unsigned long data3,
|
30
|
+
unsigned long data4) {
|
31
|
+
XEvent event;
|
32
|
+
long mask = SubstructureRedirectMask | SubstructureNotifyMask;
|
33
|
+
|
34
|
+
event.xclient.type = ClientMessage;
|
35
|
+
event.xclient.serial = 0;
|
36
|
+
event.xclient.send_event = True;
|
37
|
+
event.xclient.message_type = XInternAtom(dpy, msg, False);
|
38
|
+
event.xclient.window = id;
|
39
|
+
event.xclient.format = 32;
|
40
|
+
event.xclient.data.l[0] = data0;
|
41
|
+
event.xclient.data.l[1] = data1;
|
42
|
+
event.xclient.data.l[2] = data2;
|
43
|
+
event.xclient.data.l[3] = data3;
|
44
|
+
event.xclient.data.l[4] = data4;
|
45
|
+
|
46
|
+
if (XSendEvent(dpy, DefaultRootWindow(dpy), False, mask, &event))
|
47
|
+
return true;
|
48
|
+
warnx("cannot send %s event", msg);
|
49
|
+
return false;
|
50
|
+
}
|
51
|
+
|
52
|
+
bool window_activate(Display *dpy, Window id) {
|
53
|
+
long desk = desktop(dpy, id);
|
54
|
+
if (-1 != desk) {
|
55
|
+
client_msg(dpy, DefaultRootWindow(dpy), "_NET_CURRENT_DESKTOP",
|
56
|
+
desk, 0, 0, 0, 0);
|
57
|
+
}
|
58
|
+
|
59
|
+
bool active = client_msg(dpy, id, "_NET_ACTIVE_WINDOW", 0, 0, 0, 0, 0);
|
60
|
+
|
61
|
+
const int _net_wm_state_rm = 0;
|
62
|
+
bool unshaded = client_msg(dpy, id, "_NET_WM_STATE", _net_wm_state_rm,
|
63
|
+
myAtoms._NET_WM_STATE_SHADED, 0, 0, 0);
|
64
|
+
|
65
|
+
XMapRaised(dpy, id);
|
66
|
+
return active && unshaded;
|
67
|
+
}
|
68
|
+
|
69
|
+
bool window_center_mouse(Display *dpy, ulong id) {
|
70
|
+
XWindowAttributes attrs;
|
71
|
+
if (!XGetWindowAttributes(dpy, id, &attrs)) return false;
|
72
|
+
if (!XWarpPointer(dpy, 0, id, 0, 0, 0, 0, attrs.width/2, attrs.height/2))
|
73
|
+
return false;
|
74
|
+
XFlush(dpy);
|
75
|
+
return true;
|
76
|
+
}
|
77
|
+
|
78
|
+
// the result shout be freed
|
79
|
+
char* config() {
|
80
|
+
char xdg_runtime_home[PATH_MAX-64];
|
81
|
+
if (getenv("XDG_RUNTIME_HOME")) {
|
82
|
+
snprintf(xdg_runtime_home, PATH_MAX-64, "%s", getenv("XDG_RUNTIME_HOME"));
|
83
|
+
} else {
|
84
|
+
snprintf(xdg_runtime_home, PATH_MAX-64, "/run/user/%d", getuid());
|
85
|
+
}
|
86
|
+
char *file = (char*)malloc(PATH_MAX);
|
87
|
+
snprintf(file, PATH_MAX, "%s/%s/%s",
|
88
|
+
xdg_runtime_home, "fvwm-window-search", "last_window.json");
|
89
|
+
|
90
|
+
char *dir = dirname(strdup(file));
|
91
|
+
mkdir(xdg_runtime_home, 0755);
|
92
|
+
int r = mkdir(dir, 0755); if (-1 == r && EEXIST != errno) {
|
93
|
+
warn("failed to create %s", dir);
|
94
|
+
return NULL;
|
95
|
+
}
|
96
|
+
free(dir);
|
97
|
+
return file;
|
98
|
+
}
|
99
|
+
|
100
|
+
void state_save(Display *dpy, Window id) {
|
101
|
+
char *file = config();
|
102
|
+
int fd = open(file, O_WRONLY | O_CREAT | O_TRUNC, 0644); if (-1 == fd) {
|
103
|
+
warn("failed to truncate %s", file);
|
104
|
+
return;
|
105
|
+
}
|
106
|
+
free(file);
|
107
|
+
|
108
|
+
WindowState ws = state(dpy, id);
|
109
|
+
json_t *o = json_object();
|
110
|
+
json_object_set_new(o, "id", json_integer(ws.id));
|
111
|
+
json_object_set_new(o, "_NET_WM_STATE_SHADED", json_boolean(ws._NET_WM_STATE_SHADED));
|
112
|
+
json_object_set_new(o, "_NET_WM_STATE_HIDDEN", json_boolean(ws._NET_WM_STATE_HIDDEN));
|
113
|
+
|
114
|
+
char *dump = json_dumps(o, JSON_COMPACT);
|
115
|
+
write(fd, dump, strlen(dump));
|
116
|
+
free(dump);
|
117
|
+
json_decref(o);
|
118
|
+
|
119
|
+
close(fd);
|
120
|
+
}
|
121
|
+
|
122
|
+
Window state_load(Display *dpy, Window id_current) {
|
123
|
+
char *file = config();
|
124
|
+
json_t *root = json_load_file(file, 0, NULL);
|
125
|
+
free(file);
|
126
|
+
if (!root) return 0;
|
127
|
+
|
128
|
+
Window id = json_integer_value(json_object_get(root, "id"));
|
129
|
+
if (id == id_current) return id;
|
130
|
+
|
131
|
+
const int _net_wm_state_add = 1;
|
132
|
+
bool is_shaded = json_boolean_value(json_object_get(root, "_NET_WM_STATE_SHADED"));
|
133
|
+
if (is_shaded) client_msg(dpy, id, "_NET_WM_STATE", _net_wm_state_add,
|
134
|
+
myAtoms._NET_WM_STATE_SHADED, 0, 0, 0);
|
135
|
+
bool is_hidden = json_boolean_value(json_object_get(root, "_NET_WM_STATE_HIDDEN"));
|
136
|
+
if (is_hidden) client_msg(dpy, id, "_NET_WM_STATE", _net_wm_state_add,
|
137
|
+
myAtoms._NET_WM_STATE_HIDDEN, 0, 0, 0);
|
138
|
+
|
139
|
+
json_decref(root);
|
140
|
+
return id;
|
141
|
+
}
|
142
|
+
|
143
|
+
|
144
|
+
|
145
|
+
int main(int argc, char **argv) {
|
146
|
+
Display *dpy = XOpenDisplay(getenv("DISPLAY"));
|
147
|
+
if (!dpy) errx(1, "failed to open display %s", getenv("DISPLAY"));
|
148
|
+
if (argc != 2) errx(1, "usage: activate window-id");
|
149
|
+
|
150
|
+
mk_atoms(dpy);
|
151
|
+
|
152
|
+
ulong id = str2id(argv[1]);
|
153
|
+
if (!id) errx(1, "invalid window id: `%s`", argv[1]);
|
154
|
+
|
155
|
+
Window prev_id = state_load(dpy, id);
|
156
|
+
if (prev_id != id) state_save(dpy, id);
|
157
|
+
|
158
|
+
XSynchronize(dpy, True); // snake oil?
|
159
|
+
bool r = window_activate(dpy, id);
|
160
|
+
if (!r) return 1;
|
161
|
+
r = window_center_mouse(dpy, id);
|
162
|
+
return !r;
|
163
|
+
}
|
data/{focus.sh → activate.sh}
RENAMED
data/dmenu.patch
CHANGED
@@ -14,19 +14,20 @@ index a03a95c..ee5cffb 100644
|
|
14
14
|
$(OBJ): arg.h config.h config.mk drw.h
|
15
15
|
|
16
16
|
diff --git a/config.def.h b/config.def.h
|
17
|
-
index 1edb647..
|
17
|
+
index 1edb647..65c831f 100644
|
18
18
|
--- a/config.def.h
|
19
19
|
+++ b/config.def.h
|
20
|
-
@@ -21,3 +21,
|
20
|
+
@@ -21,3 +21,8 @@ static unsigned int lines = 0;
|
21
21
|
* for example: " /?\"&[]"
|
22
22
|
*/
|
23
23
|
static const char worddelimiters[] = " ";
|
24
24
|
+
|
25
|
-
+/* -
|
25
|
+
+/* -selection_hook option; run a command on every selection */
|
26
26
|
+static const char *selection_hook = NULL;
|
27
|
-
+static int
|
27
|
+
+static int selection_hook_activation = 1;
|
28
|
+
+static int selection_hook_activation_return_key_only = 0;
|
28
29
|
diff --git a/dmenu.c b/dmenu.c
|
29
|
-
index 65f25ce..
|
30
|
+
index 65f25ce..47a6b37 100644
|
30
31
|
--- a/dmenu.c
|
31
32
|
+++ b/dmenu.c
|
32
33
|
@@ -304,6 +304,62 @@ movewordedge(int dir)
|
@@ -92,7 +93,45 @@ index 65f25ce..274668a 100644
|
|
92
93
|
static void
|
93
94
|
keypress(XKeyEvent *ev)
|
94
95
|
{
|
95
|
-
@@ -
|
96
|
+
@@ -410,6 +466,7 @@ insert:
|
97
|
+
insert(NULL, nextrune(-1) - cursor);
|
98
|
+
break;
|
99
|
+
case XK_End:
|
100
|
+
+ if (ev->state & ShiftMask) selection_hook_activation = 0;
|
101
|
+
if (text[cursor] != '\0') {
|
102
|
+
cursor = strlen(text);
|
103
|
+
break;
|
104
|
+
@@ -429,6 +486,7 @@ insert:
|
105
|
+
cleanup();
|
106
|
+
exit(1);
|
107
|
+
case XK_Home:
|
108
|
+
+ if (ev->state & ShiftMask) selection_hook_activation = 0;
|
109
|
+
if (sel == matches) {
|
110
|
+
cursor = 0;
|
111
|
+
break;
|
112
|
+
@@ -445,18 +503,21 @@ insert:
|
113
|
+
return;
|
114
|
+
/* fallthrough */
|
115
|
+
case XK_Up:
|
116
|
+
+ if (ev->state & ShiftMask) selection_hook_activation = 0;
|
117
|
+
if (sel && sel->left && (sel = sel->left)->right == curr) {
|
118
|
+
curr = prev;
|
119
|
+
calcoffsets();
|
120
|
+
}
|
121
|
+
break;
|
122
|
+
case XK_Next:
|
123
|
+
+ if (ev->state & ShiftMask) selection_hook_activation = 0;
|
124
|
+
if (!next)
|
125
|
+
return;
|
126
|
+
sel = curr = next;
|
127
|
+
calcoffsets();
|
128
|
+
break;
|
129
|
+
case XK_Prior:
|
130
|
+
+ if (ev->state & ShiftMask) selection_hook_activation = 0;
|
131
|
+
if (!prev)
|
132
|
+
return;
|
133
|
+
sel = curr = prev;
|
134
|
+
@@ -464,6 +525,7 @@ insert:
|
96
135
|
break;
|
97
136
|
case XK_Return:
|
98
137
|
case XK_KP_Enter:
|
@@ -100,29 +139,40 @@ index 65f25ce..274668a 100644
|
|
100
139
|
puts((sel && !(ev->state & ShiftMask)) ? sel->text : text);
|
101
140
|
if (!(ev->state & ControlMask)) {
|
102
141
|
cleanup();
|
103
|
-
@@ -
|
142
|
+
@@ -481,6 +543,7 @@ insert:
|
143
|
+
return;
|
144
|
+
/* fallthrough */
|
145
|
+
case XK_Down:
|
146
|
+
+ if (ev->state & ShiftMask) selection_hook_activation = 0;
|
147
|
+
if (sel && sel->right && (sel = sel->right) == next) {
|
148
|
+
curr = next;
|
149
|
+
calcoffsets();
|
150
|
+
@@ -572,6 +635,11 @@ run(void)
|
104
151
|
break;
|
105
152
|
case KeyPress:
|
106
153
|
keypress(&ev.xkey);
|
107
|
-
+ if (!
|
154
|
+
+ if (!selection_hook_activation_return_key_only &&
|
155
|
+
+ selection_hook_activation)
|
108
156
|
+ selhook(selection_hook, sel);
|
157
|
+
+
|
158
|
+
+ selection_hook_activation = 1;
|
109
159
|
break;
|
110
160
|
case SelectionNotify:
|
111
161
|
if (ev.xselection.property == utf8)
|
112
|
-
@@ -712,6 +
|
162
|
+
@@ -712,6 +780,8 @@ main(int argc, char *argv[])
|
113
163
|
else if (!strcmp(argv[i], "-i")) { /* case-insensitive item matching */
|
114
164
|
fstrncmp = strncasecmp;
|
115
165
|
fstrstr = cistrstr;
|
116
|
-
+ } else if (!strcmp(argv[i], "-
|
117
|
-
+
|
166
|
+
+ } else if (!strcmp(argv[i], "-selection_hook_activation_return_key_only")) {
|
167
|
+
+ selection_hook_activation_return_key_only = 1;
|
118
168
|
} else if (i + 1 == argc)
|
119
169
|
usage();
|
120
170
|
/* these options take one argument */
|
121
|
-
@@ -733,6 +
|
171
|
+
@@ -733,6 +803,8 @@ main(int argc, char *argv[])
|
122
172
|
colors[SchemeSel][ColFg] = argv[++i];
|
123
173
|
else if (!strcmp(argv[i], "-w")) /* embedding window id */
|
124
174
|
embed = argv[++i];
|
125
|
-
+ else if (!strcmp(argv[i], "-
|
175
|
+
+ else if (!strcmp(argv[i], "-selection_hook")) /* a command to run */
|
126
176
|
+ selection_hook = argv[++i];
|
127
177
|
else
|
128
178
|
usage();
|
data/fontinfo.c
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
// Prints a triptych of 'screenWidth charWidth userTextWidth' to stdout.
|
2
|
+
|
3
|
+
#include <stdbool.h>
|
4
|
+
#include <err.h>
|
5
|
+
#include <X11/Xft/Xft.h>
|
6
|
+
#include <X11/Xatom.h>
|
7
|
+
|
8
|
+
#include "lib.c"
|
9
|
+
|
10
|
+
long desktop_width(Display *dpy) {
|
11
|
+
u_char *prop_val = NULL;
|
12
|
+
ulong prop_size;
|
13
|
+
if (!prop(dpy, DefaultRootWindow(dpy), XA_CARDINAL, "_NET_DESKTOP_GEOMETRY", &prop_val, &prop_size))
|
14
|
+
return -1;
|
15
|
+
|
16
|
+
long r = ((long*)prop_val)[0];
|
17
|
+
free(prop_val);
|
18
|
+
return r;
|
19
|
+
}
|
20
|
+
|
21
|
+
int main(int argc, char **argv) {
|
22
|
+
Display *dpy = XOpenDisplay(getenv("DISPLAY"));
|
23
|
+
if (!dpy) errx(1, "failed to open display %s", getenv("DISPLAY"));
|
24
|
+
if (argc != 3) errx(1, "usage: fontinfo font text-string");
|
25
|
+
|
26
|
+
XftFont *font = XftFontOpenName(dpy, DefaultScreen(dpy), argv[1]);
|
27
|
+
if (!font) errx(1, "no font match");
|
28
|
+
|
29
|
+
XGlyphInfo info_text, info_char;
|
30
|
+
XftTextExtentsUtf8(dpy, font, (FcChar8*)"@", 1, &info_char);
|
31
|
+
XftTextExtentsUtf8(dpy, font, (FcChar8*)argv[2], strlen(argv[2]), &info_text);
|
32
|
+
|
33
|
+
printf("%ld %d %d\n", desktop_width(dpy), info_char.width, info_text.width);
|
34
|
+
}
|
data/fvwm-window-search
CHANGED
@@ -1,18 +1,21 @@
|
|
1
1
|
#!/usr/bin/env -S ruby --disable-gems
|
2
|
+
# coding: utf-8
|
3
|
+
# frozen_string_literal: true
|
2
4
|
|
3
5
|
require 'yaml'
|
4
6
|
require 'json'
|
5
7
|
require 'optparse'
|
8
|
+
require 'shellwords'
|
6
9
|
|
7
10
|
def options
|
8
11
|
default = {
|
9
|
-
|
12
|
+
'dmenu' => { # each key corresponds to a dmenu CL option
|
10
13
|
"fn" => "Monospace-10",
|
11
14
|
"l" => 8,
|
12
15
|
"b" => true,
|
13
16
|
"i" => true,
|
14
|
-
|
15
|
-
|
17
|
+
'selection_hook' => File.join(__dir__, "activate.sh %s"),
|
18
|
+
'selection_hook_activation_return_key_only' => false,
|
16
19
|
},
|
17
20
|
"filter-out" => {
|
18
21
|
"name" => [],
|
@@ -30,8 +33,9 @@ def options_command_line
|
|
30
33
|
opt = { "dmenu" => {} }
|
31
34
|
OptionParser.new do |o|
|
32
35
|
o.on("-c path", "an alternative path to conf.yaml") { |v| opt["conf"] = v }
|
36
|
+
o.on('-d', 'list windows from the current desktop only') { opt['this_desk_only'] = true }
|
33
37
|
o.on("-r", "switch to a window only when <Return> is pressed") do
|
34
|
-
opt[
|
38
|
+
opt['dmenu']['selection_hook_activation_return_key_only'] = true
|
35
39
|
end
|
36
40
|
end.parse!
|
37
41
|
opt
|
@@ -51,8 +55,10 @@ def deep_merge first, second
|
|
51
55
|
first.merge(second, &merger)
|
52
56
|
end
|
53
57
|
|
58
|
+
def helper exe; File.join(__dir__, "_out/#{exe}"); end
|
59
|
+
|
54
60
|
def dmenu_cmd params
|
55
|
-
[
|
61
|
+
[helper('dmenu/dmenu')] + params.map do |k,v|
|
56
62
|
k = "-"+k
|
57
63
|
if !!v == v
|
58
64
|
v ? k : nil
|
@@ -81,12 +87,38 @@ def desired patterns, window
|
|
81
87
|
match.call("name", window['name'])
|
82
88
|
end
|
83
89
|
|
90
|
+
def dmenu_max_text_len opt
|
91
|
+
cmd = "#{helper('fontinfo')} #{opt['dmenu']['fn'].shellescape} '@'"
|
92
|
+
desk_width, char_width = `#{cmd}`.split.map(&:to_i)
|
93
|
+
(desk_width - char_width*2) / char_width
|
94
|
+
end
|
95
|
+
|
96
|
+
def menu_line max_len, desk_indicator, w
|
97
|
+
desk = w['desk'] == -1 ? '*' : w['desk'].to_s
|
98
|
+
desktop = desk_indicator + desk
|
99
|
+
id = '0x'+w['id'].to_s(16)
|
100
|
+
|
101
|
+
c = ->(s, len) { s.size > len ? s[0...len-1] + '…' : s }
|
102
|
+
|
103
|
+
name_width = max_len - 4 - 10 - 10 - 9 - 4*3
|
104
|
+
|
105
|
+
"%-4s | %10s | %-#{name_width}s | %10s | %9s" % [
|
106
|
+
desktop,
|
107
|
+
c.call(w['class'], 10),
|
108
|
+
c.call(w['name'], name_width),
|
109
|
+
c.call(w['host'], 10),
|
110
|
+
c.call(id, 9)
|
111
|
+
]
|
112
|
+
end
|
113
|
+
|
84
114
|
def main
|
85
115
|
opt = options
|
86
116
|
pp opt if $DEBUG
|
117
|
+
|
118
|
+
max_len = dmenu_max_text_len opt
|
87
119
|
dmenu = IO.popen(dmenu_cmd(opt['dmenu']), 'r+')
|
88
120
|
|
89
|
-
IO.popen(
|
121
|
+
IO.popen(helper('winlist')).each_line do |line|
|
90
122
|
begin
|
91
123
|
w = JSON.parse line
|
92
124
|
rescue
|
@@ -94,10 +126,16 @@ def main
|
|
94
126
|
next
|
95
127
|
end
|
96
128
|
|
129
|
+
if opt['this_desk_only']
|
130
|
+
next unless w['desk_cur']
|
131
|
+
desk_indicator = ''
|
132
|
+
else
|
133
|
+
desk_indicator = w['desk_cur'] ? '→ ' : ' '
|
134
|
+
end
|
135
|
+
|
97
136
|
next unless desired opt['filter-out'], w
|
98
137
|
|
99
|
-
|
100
|
-
dmenu.puts [desk, w['class'], w['name'], w['host'], '0x'+w['id'].to_s(16)].join ' | '
|
138
|
+
dmenu.puts menu_line(max_len, desk_indicator, w)
|
101
139
|
end
|
102
140
|
|
103
141
|
dmenu.close
|
data/lib.c
CHANGED
@@ -22,3 +22,39 @@ long desktop(Display *dpy, Window wid) {
|
|
22
22
|
free(prop_val);
|
23
23
|
return r;
|
24
24
|
}
|
25
|
+
|
26
|
+
typedef struct {
|
27
|
+
bool _NET_WM_STATE_SHADED;
|
28
|
+
bool _NET_WM_STATE_HIDDEN;
|
29
|
+
Window id;
|
30
|
+
} WindowState;
|
31
|
+
|
32
|
+
typedef struct {
|
33
|
+
Atom _NET_WM_STATE_SHADED;
|
34
|
+
Atom _NET_WM_STATE_HIDDEN;
|
35
|
+
Atom UTF8_STRING;
|
36
|
+
} MyAtoms;
|
37
|
+
|
38
|
+
MyAtoms myAtoms;
|
39
|
+
|
40
|
+
void mk_atoms(Display *dpy) {
|
41
|
+
myAtoms._NET_WM_STATE_SHADED = XInternAtom(dpy, "_NET_WM_STATE_SHADED", False);
|
42
|
+
myAtoms._NET_WM_STATE_HIDDEN = XInternAtom(dpy, "_NET_WM_STATE_HIDDEN", False);
|
43
|
+
myAtoms.UTF8_STRING = XInternAtom(dpy, "UTF8_STRING", False);
|
44
|
+
}
|
45
|
+
|
46
|
+
WindowState state(Display *dpy, Window id) {
|
47
|
+
WindowState r = { .id = id };
|
48
|
+
u_char *prop_val = NULL;
|
49
|
+
ulong prop_size;
|
50
|
+
if (!prop(dpy, id, XA_ATOM, "_NET_WM_STATE", &prop_val, &prop_size)) return r;
|
51
|
+
|
52
|
+
Atom *atoms = (Atom*)prop_val;
|
53
|
+
for (int idx = 0; idx < prop_size; idx++) {
|
54
|
+
if (atoms[idx] == myAtoms._NET_WM_STATE_SHADED) r._NET_WM_STATE_SHADED = true;
|
55
|
+
if (atoms[idx] == myAtoms._NET_WM_STATE_HIDDEN) r._NET_WM_STATE_HIDDEN = true;
|
56
|
+
}
|
57
|
+
XFree(prop_val);
|
58
|
+
|
59
|
+
return r;
|
60
|
+
}
|
data/winlist.c
CHANGED
@@ -14,6 +14,7 @@
|
|
14
14
|
|
15
15
|
#include <X11/Xlib.h>
|
16
16
|
#include <X11/Xatom.h>
|
17
|
+
#include <X11/Xutil.h>
|
17
18
|
#include <jansson.h>
|
18
19
|
|
19
20
|
#include "lib.c"
|
@@ -25,12 +26,11 @@ typedef struct {
|
|
25
26
|
|
26
27
|
// result (WinList.ids) should be freed
|
27
28
|
WinList winlist(Display *dpy) {
|
28
|
-
WinList list;
|
29
|
+
WinList list = { .ids = NULL };
|
29
30
|
u_char *result;
|
30
31
|
|
31
|
-
if (!prop(dpy, DefaultRootWindow(dpy), XA_WINDOW, "
|
32
|
+
if (!prop(dpy, DefaultRootWindow(dpy), XA_WINDOW, "_NET_CLIENT_LIST_STACKING",
|
32
33
|
&result, &list.size)) {
|
33
|
-
list.size = -1;
|
34
34
|
return list;
|
35
35
|
}
|
36
36
|
|
@@ -43,40 +43,15 @@ char* wm_client_machine(Display *dpy, Window wid) {
|
|
43
43
|
u_char *prop_val = NULL;
|
44
44
|
ulong prop_size;
|
45
45
|
prop(dpy, wid, XA_STRING, "WM_CLIENT_MACHINE", &prop_val, &prop_size);
|
46
|
-
return (char*)prop_val;
|
46
|
+
return prop_val ? (char*)prop_val : strdup("nil");
|
47
47
|
}
|
48
48
|
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
typedef struct {
|
56
|
-
char *resource;
|
57
|
-
char *class_name;
|
58
|
-
} ResClass;
|
59
|
-
|
60
|
-
// result (ResClass.*) should be freed
|
61
|
-
ResClass wm_class(Display *dpy, Window wid) {
|
62
|
-
ResClass r = {.resource = NULL};
|
63
|
-
|
64
|
-
u_char *prop_val = NULL;
|
65
|
-
ulong prop_size;
|
66
|
-
if (!prop(dpy, wid, XA_STRING, "WM_CLASS", &prop_val, &prop_size))
|
67
|
-
return r;
|
68
|
-
|
69
|
-
ulong idx = str_index((char*)prop_val, '\0');
|
70
|
-
if (idx < prop_size) {
|
71
|
-
r.resource = (char*)malloc(idx+2);
|
72
|
-
snprintf(r.resource, idx+1, "%s", prop_val);
|
73
|
-
|
74
|
-
ulong len = prop_size-idx;
|
75
|
-
r.class_name = (char*)malloc(len+1);
|
76
|
-
snprintf(r.class_name, len, "%s", prop_val+idx+1);
|
77
|
-
}
|
78
|
-
|
79
|
-
free(prop_val);
|
49
|
+
// result (XClassHint.*) should be freed
|
50
|
+
XClassHint wm_class(Display *dpy, Window wid) {
|
51
|
+
XClassHint r = { .res_name = NULL };
|
52
|
+
XGetClassHint(dpy, wid, &r);
|
53
|
+
if (!r.res_name) r.res_name = strdup("nil");
|
54
|
+
if (!r.res_class) r.res_class = strdup("nil");
|
80
55
|
return r;
|
81
56
|
}
|
82
57
|
|
@@ -85,12 +60,24 @@ char* wm_name(Display *dpy, Window wid) {
|
|
85
60
|
u_char *prop_val = NULL;
|
86
61
|
ulong prop_size;
|
87
62
|
|
88
|
-
|
89
|
-
bool r = prop(dpy, wid, utf8_str, "_NET_WM_NAME", &prop_val, &prop_size);
|
63
|
+
bool r = prop(dpy, wid, myAtoms.UTF8_STRING, "_NET_WM_NAME", &prop_val, &prop_size);
|
90
64
|
if (r && prop_val) return (char*)prop_val;
|
91
65
|
|
92
66
|
prop(dpy, wid, XA_STRING, "WM_NAME", &prop_val, &prop_size);
|
93
|
-
return (char*)prop_val;
|
67
|
+
return prop_val ? (char*)prop_val : strdup("nil");
|
68
|
+
}
|
69
|
+
|
70
|
+
long desktop_current(Display *dpy) {
|
71
|
+
u_char *prop_val = NULL;
|
72
|
+
ulong prop_size;
|
73
|
+
long r = -1;
|
74
|
+
if (!prop(dpy, DefaultRootWindow(dpy), XA_CARDINAL, "_NET_CURRENT_DESKTOP",
|
75
|
+
&prop_val, &prop_size))
|
76
|
+
return r;
|
77
|
+
|
78
|
+
if (prop_val) r = ((long*)prop_val)[0];
|
79
|
+
free(prop_val);
|
80
|
+
return r;
|
94
81
|
}
|
95
82
|
|
96
83
|
|
@@ -98,21 +85,25 @@ char* wm_name(Display *dpy, Window wid) {
|
|
98
85
|
int main() {
|
99
86
|
Display *dpy = XOpenDisplay(getenv("DISPLAY"));
|
100
87
|
if (!dpy) errx(1, "failed to open display %s", getenv("DISPLAY"));
|
88
|
+
mk_atoms(dpy);
|
101
89
|
|
102
90
|
WinList list = winlist(dpy);
|
103
|
-
for (
|
91
|
+
for (long idx = list.size-1; idx >= 0; idx--) {
|
104
92
|
ulong wid = list.ids[idx];
|
105
93
|
|
106
94
|
char *host = wm_client_machine(dpy, wid);
|
107
95
|
char *name = wm_name(dpy, wid);
|
108
|
-
|
96
|
+
XClassHint rc = wm_class(dpy, wid);
|
97
|
+
long desk = desktop(dpy, wid);
|
98
|
+
bool is_desk_cur = desk < 0 || desk == desktop_current(dpy);
|
109
99
|
|
110
100
|
json_t *line = json_object();
|
111
|
-
json_object_set_new(line, "desk", json_integer(
|
101
|
+
json_object_set_new(line, "desk", json_integer(desk));
|
102
|
+
json_object_set_new(line, "desk_cur", json_boolean(is_desk_cur));
|
112
103
|
json_object_set_new(line, "host", json_string(host));
|
113
104
|
json_object_set_new(line, "name", json_string(name));
|
114
|
-
json_object_set_new(line, "resource", json_string(rc.
|
115
|
-
json_object_set_new(line, "class", json_string(rc.
|
105
|
+
json_object_set_new(line, "resource", json_string(rc.res_name));
|
106
|
+
json_object_set_new(line, "class", json_string(rc.res_class));
|
116
107
|
json_object_set_new(line, "id", json_integer(wid));
|
117
108
|
|
118
109
|
char *dump = json_dumps(line, JSON_COMPACT);
|
@@ -122,8 +113,8 @@ int main() {
|
|
122
113
|
|
123
114
|
free(host);
|
124
115
|
free(name);
|
125
|
-
free(rc.
|
126
|
-
free(rc.
|
116
|
+
free(rc.res_name);
|
117
|
+
free(rc.res_class);
|
127
118
|
}
|
128
119
|
XFree(list.ids);
|
129
120
|
}
|
metadata
CHANGED
@@ -1,20 +1,22 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: fvwm-window-search
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 2.
|
4
|
+
version: 2.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Alexander Gromnitsky
|
8
8
|
autorequire:
|
9
9
|
bindir: "."
|
10
10
|
cert_chain: []
|
11
|
-
date: 2021-04-
|
11
|
+
date: 2021-04-09 00:00:00.000000000 Z
|
12
12
|
dependencies: []
|
13
13
|
description: |
|
14
14
|
A window switcher: search for windows interactively using a patched
|
15
|
-
dmenu utility (
|
16
|
-
but it's been
|
17
|
-
|
15
|
+
dmenu utility (the gem fetches & patches it during its installation).
|
16
|
+
This was originally made for Fvwm, but it's been rewritten to work with
|
17
|
+
any EWMH-compliant stacking window manager.
|
18
|
+
|
19
|
+
Requires a preinstalled jansson-devel C library.
|
18
20
|
|
19
21
|
It differs from rofi & co in that it activates (brings up) windows
|
20
22
|
_during_ the search.
|
@@ -28,10 +30,11 @@ files:
|
|
28
30
|
- "./fvwm-window-search"
|
29
31
|
- Makefile
|
30
32
|
- README.md
|
33
|
+
- activate.c
|
34
|
+
- activate.sh
|
31
35
|
- dmenu.patch
|
32
36
|
- extconf.rb
|
33
|
-
-
|
34
|
-
- focus.sh
|
37
|
+
- fontinfo.c
|
35
38
|
- lib.c
|
36
39
|
- winlist.c
|
37
40
|
homepage: https://github.com/gromnitsky/fvwm-window-search
|
data/focus.c
DELETED
@@ -1,80 +0,0 @@
|
|
1
|
-
#include <stdlib.h>
|
2
|
-
#include <err.h>
|
3
|
-
#include <stdio.h>
|
4
|
-
#include <stdbool.h>
|
5
|
-
|
6
|
-
#include <X11/Xlib.h>
|
7
|
-
#include <X11/Xatom.h>
|
8
|
-
|
9
|
-
#include "lib.c"
|
10
|
-
|
11
|
-
ulong str2id(const char *s) {
|
12
|
-
ulong id;
|
13
|
-
if (sscanf(s, "0x%lx", &id) != 1 &&
|
14
|
-
sscanf(s, "0X%lx", &id) != 1 &&
|
15
|
-
sscanf(s, "%lu", &id) != 1) return 0;
|
16
|
-
return id;
|
17
|
-
}
|
18
|
-
|
19
|
-
bool client_msg(Display *dpy, Window id, char *msg,
|
20
|
-
unsigned long data0, unsigned long data1,
|
21
|
-
unsigned long data2, unsigned long data3,
|
22
|
-
unsigned long data4) {
|
23
|
-
XEvent event;
|
24
|
-
long mask = SubstructureRedirectMask | SubstructureNotifyMask;
|
25
|
-
|
26
|
-
event.xclient.type = ClientMessage;
|
27
|
-
event.xclient.serial = 0;
|
28
|
-
event.xclient.send_event = True;
|
29
|
-
event.xclient.message_type = XInternAtom(dpy, msg, False);
|
30
|
-
event.xclient.window = id;
|
31
|
-
event.xclient.format = 32;
|
32
|
-
event.xclient.data.l[0] = data0;
|
33
|
-
event.xclient.data.l[1] = data1;
|
34
|
-
event.xclient.data.l[2] = data2;
|
35
|
-
event.xclient.data.l[3] = data3;
|
36
|
-
event.xclient.data.l[4] = data4;
|
37
|
-
|
38
|
-
if (XSendEvent(dpy, DefaultRootWindow(dpy), False, mask, &event))
|
39
|
-
return true;
|
40
|
-
warnx("cannot send %s event", msg);
|
41
|
-
return false;
|
42
|
-
}
|
43
|
-
|
44
|
-
bool window_activate(Display *dpy, Window id) {
|
45
|
-
long desk = desktop(dpy, id);
|
46
|
-
if (-1 != desk) {
|
47
|
-
client_msg(dpy, DefaultRootWindow(dpy), "_NET_CURRENT_DESKTOP",
|
48
|
-
desk, 0, 0, 0, 0);
|
49
|
-
}
|
50
|
-
|
51
|
-
bool r = client_msg(dpy, id, "_NET_ACTIVE_WINDOW", 0, 0, 0, 0, 0);
|
52
|
-
XMapRaised(dpy, id);
|
53
|
-
return r;
|
54
|
-
}
|
55
|
-
|
56
|
-
bool window_center_mouse(Display *dpy, ulong id) {
|
57
|
-
XWindowAttributes attrs;
|
58
|
-
if (!XGetWindowAttributes(dpy, id, &attrs)) return false;
|
59
|
-
if (!XWarpPointer(dpy, 0, id, 0, 0, 0, 0, attrs.width/2, attrs.height/2))
|
60
|
-
return false;
|
61
|
-
XFlush(dpy);
|
62
|
-
return true;
|
63
|
-
}
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
int main(int argc, char **argv) {
|
68
|
-
Display *dpy = XOpenDisplay(getenv("DISPLAY"));
|
69
|
-
if (!dpy) errx(1, "failed to open display %s", getenv("DISPLAY"));
|
70
|
-
if (argc != 2) errx(1, "usage: focus window-id");
|
71
|
-
|
72
|
-
ulong id = str2id(argv[1]);
|
73
|
-
if (!id) errx(1, "invalid window id: `%s`", argv[1]);
|
74
|
-
|
75
|
-
XSynchronize(dpy, True); // snake oil?
|
76
|
-
bool r = window_activate(dpy, id);
|
77
|
-
if (!r) return 1;
|
78
|
-
r = window_center_mouse(dpy, id);
|
79
|
-
return !r;
|
80
|
-
}
|