fvwm-window-search 1.0.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 +13 -1
- data/README.md +39 -15
- data/activate.c +163 -0
- data/activate.sh +12 -0
- data/dmenu.patch +70 -19
- data/fontinfo.c +34 -0
- data/fvwm-window-search +112 -31
- data/lib.c +60 -0
- data/winlist.c +120 -0
- metadata +23 -13
- data/focus.sh +0 -7
- data/lib.rb +0 -105
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
@@ -1,6 +1,8 @@
|
|
1
1
|
out := _out
|
2
2
|
dmenu := $(out)/dmenu
|
3
|
-
dmenu.commit :=
|
3
|
+
dmenu.commit := 1a13d0465d1a6f4f74bc5b07b04c9bd542f20ba6
|
4
|
+
|
5
|
+
all: $(addprefix $(out)/, .dmenu.build activate winlist fontinfo)
|
4
6
|
|
5
7
|
$(out)/.dmenu.build: $(out)/.dmenu.$(dmenu.commit) dmenu.patch
|
6
8
|
patch -d $(dmenu) -p1 < dmenu.patch
|
@@ -12,5 +14,15 @@ $(out)/.dmenu.$(dmenu.commit):
|
|
12
14
|
git -C $(dmenu) checkout $(dmenu.commit) -q
|
13
15
|
touch $@
|
14
16
|
|
17
|
+
libs := x11
|
18
|
+
LDFLAGS = $(shell pkg-config --libs $(libs))
|
19
|
+
CFLAGS = -g -Wall -Werror $(shell pkg-config --cflags $(libs))
|
20
|
+
$(out)/%: %.c lib.c
|
21
|
+
$(LINK.c) $< $(LOADLIBES) $(LDLIBS) -o $@
|
22
|
+
|
23
|
+
$(out)/activate: libs += jansson
|
24
|
+
$(out)/winlist: libs += jansson
|
25
|
+
$(out)/fontinfo: libs += xft freetype2
|
26
|
+
|
15
27
|
# an empty target to satisfy rubygems
|
16
28
|
install:
|
data/README.md
CHANGED
@@ -5,24 +5,35 @@ Incremental window search & immediate switch to the selected window
|
|
5
5
|
|
6
6
|
$ gem install fvwm-window-search
|
7
7
|
|
8
|
-

|
9
9
|
|
10
|
-
* Should work w/
|
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
|
-
* Ruby
|
16
|
-
* `
|
17
|
+
* Ruby 2.1+
|
18
|
+
* `dnf install jansson-devel freetype-devel`
|
17
19
|
|
18
20
|
## Compilation
|
19
21
|
|
20
|
-
Type `make`. This clones the dmenu repo, patches & builds it. It
|
21
|
-
|
22
|
+
Type `make`. This clones the dmenu repo, patches & builds it. It
|
23
|
+
doesn't interfere w/ a system-installed dmenu.
|
22
24
|
|
23
25
|
## Usage
|
24
26
|
|
25
|
-
|
27
|
+
~~~
|
28
|
+
$ ./fvwm-window-search -h
|
29
|
+
Usage: fvwm-window-search [options]
|
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
|
33
|
+
~~~
|
34
|
+
|
35
|
+
To scroll in dmenu (using Up/Down/Home/End/PgUp/PgDown) without
|
36
|
+
windows activation, hold <kbd>Shift</kbd>.
|
26
37
|
|
27
38
|
To customise dmenu or filtering, create a yaml file
|
28
39
|
`$XDG_CONFIG_HOME/fvwm-window-search/conf.yaml`, e.g.:
|
@@ -32,19 +43,32 @@ To customise dmenu or filtering, create a yaml file
|
|
32
43
|
dmenu:
|
33
44
|
fn: Monospace-12
|
34
45
|
b: false
|
35
|
-
|
46
|
+
selection_hook_activation_return_key_only: true
|
47
|
+
filter-out:
|
36
48
|
name: ['System Monitor']
|
49
|
+
resource: []
|
50
|
+
class: []
|
37
51
|
~~~
|
38
52
|
|
39
|
-
|
40
|
-
|
53
|
+
Subkeys in `dmenu` are the usual CLOs for
|
54
|
+
[dmenu(1)][]. `selection_hook_activation_return_key_only` is an
|
55
|
+
equivalent of `-r` CLO.
|
56
|
+
|
57
|
+
[dmenu(1)]: https://manpages.debian.org/unstable/suckless-tools/dmenu.1.en.html
|
58
|
+
|
59
|
+
`filter-out` key tells what windows should be ignored. Each value in a
|
60
|
+
subkey is an array of regexes. See the defaults at the top of
|
61
|
+
`fvwm-window-search` file.
|
41
62
|
|
42
|
-
|
63
|
+
## Start-up time
|
43
64
|
|
44
|
-
|
65
|
+
As a task switcher, the program must not only run fast, but also
|
66
|
+
*start* fast. I managed to get it under 70ms on my laptop, when you
|
67
|
+
run `./fvwm-window-search` directly from the repo.
|
45
68
|
|
46
|
-
|
47
|
-
|
69
|
+
This is not the case with rubygems! The latter generates a stub script
|
70
|
+
that invokes `./fvwm-window-search` file. This indirection may add
|
71
|
+
~140ms of additional delay.
|
48
72
|
|
49
73
|
## License
|
50
74
|
|
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/activate.sh
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
#!/bin/sh
|
2
|
+
|
3
|
+
id=`echo "$1" | awk -F'|' '{print $NF} END { exit $NF == "" ? 1 : 0}'` || {
|
4
|
+
echo "usage: `basename "$0"` 'foo | bar | id'"
|
5
|
+
exit 1
|
6
|
+
}
|
7
|
+
|
8
|
+
__filename=`readlink -f "$0"`
|
9
|
+
__dirname=`dirname "${__filename}"`
|
10
|
+
|
11
|
+
# shellcheck disable=2086
|
12
|
+
${__dirname}/_out/activate $id
|
data/dmenu.patch
CHANGED
@@ -14,18 +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 selection_hook_activation = 1;
|
28
|
+
+static int selection_hook_activation_return_key_only = 0;
|
27
29
|
diff --git a/dmenu.c b/dmenu.c
|
28
|
-
index 65f25ce..
|
30
|
+
index 65f25ce..47a6b37 100644
|
29
31
|
--- a/dmenu.c
|
30
32
|
+++ b/dmenu.c
|
31
33
|
@@ -304,6 +304,62 @@ movewordedge(int dir)
|
@@ -91,7 +93,45 @@ index 65f25ce..69254ee 100644
|
|
91
93
|
static void
|
92
94
|
keypress(XKeyEvent *ev)
|
93
95
|
{
|
94
|
-
@@ -
|
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:
|
95
135
|
break;
|
96
136
|
case XK_Return:
|
97
137
|
case XK_KP_Enter:
|
@@ -99,29 +139,40 @@ index 65f25ce..69254ee 100644
|
|
99
139
|
puts((sel && !(ev->state & ShiftMask)) ? sel->text : text);
|
100
140
|
if (!(ev->state & ControlMask)) {
|
101
141
|
cleanup();
|
102
|
-
@@ -
|
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)
|
103
151
|
break;
|
104
152
|
case KeyPress:
|
105
153
|
keypress(&ev.xkey);
|
106
|
-
+
|
154
|
+
+ if (!selection_hook_activation_return_key_only &&
|
155
|
+
+ selection_hook_activation)
|
156
|
+
+ selhook(selection_hook, sel);
|
157
|
+
+
|
158
|
+
+ selection_hook_activation = 1;
|
107
159
|
break;
|
108
160
|
case SelectionNotify:
|
109
161
|
if (ev.xselection.property == utf8)
|
110
|
-
@@ -
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
+
|
116
|
-
+
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
@@ -733,6 +792,8 @@ main(int argc, char *argv[])
|
162
|
+
@@ -712,6 +780,8 @@ main(int argc, char *argv[])
|
163
|
+
else if (!strcmp(argv[i], "-i")) { /* case-insensitive item matching */
|
164
|
+
fstrncmp = strncasecmp;
|
165
|
+
fstrstr = cistrstr;
|
166
|
+
+ } else if (!strcmp(argv[i], "-selection_hook_activation_return_key_only")) {
|
167
|
+
+ selection_hook_activation_return_key_only = 1;
|
168
|
+
} else if (i + 1 == argc)
|
169
|
+
usage();
|
170
|
+
/* these options take one argument */
|
171
|
+
@@ -733,6 +803,8 @@ main(int argc, char *argv[])
|
121
172
|
colors[SchemeSel][ColFg] = argv[++i];
|
122
173
|
else if (!strcmp(argv[i], "-w")) /* embedding window id */
|
123
174
|
embed = argv[++i];
|
124
|
-
+ else if(!strcmp(argv[i], "-
|
175
|
+
+ else if (!strcmp(argv[i], "-selection_hook")) /* a command to run */
|
125
176
|
+ selection_hook = argv[++i];
|
126
177
|
else
|
127
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,38 +1,64 @@
|
|
1
|
-
#!/usr/bin/env ruby
|
1
|
+
#!/usr/bin/env -S ruby --disable-gems
|
2
|
+
# coding: utf-8
|
3
|
+
# frozen_string_literal: true
|
2
4
|
|
3
|
-
require_relative './lib'
|
4
|
-
include FvwmWindowSearch
|
5
5
|
require 'yaml'
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
conf = File.join xdg_config_home, 'fvwm-window-search', 'conf.yaml'
|
10
|
-
r = File.read conf rescue nil
|
11
|
-
YAML.load r, conf rescue errx 1, "invalid config: #{$!}" if r
|
12
|
-
end
|
6
|
+
require 'json'
|
7
|
+
require 'optparse'
|
8
|
+
require 'shellwords'
|
13
9
|
|
14
10
|
def options
|
15
11
|
default = {
|
16
|
-
|
17
|
-
"selhook" => File.join(__dir__, "focus.sh %s"),
|
12
|
+
'dmenu' => { # each key corresponds to a dmenu CL option
|
18
13
|
"fn" => "Monospace-10",
|
19
14
|
"l" => 8,
|
20
15
|
"b" => true,
|
21
16
|
"i" => true,
|
17
|
+
'selection_hook' => File.join(__dir__, "activate.sh %s"),
|
18
|
+
'selection_hook_activation_return_key_only' => false,
|
22
19
|
},
|
23
|
-
"filter" => {
|
20
|
+
"filter-out" => {
|
24
21
|
"name" => [],
|
25
22
|
"resource" => [],
|
26
23
|
"class" => ['^Fvwm', '!^FvwmIdent$']
|
27
24
|
}
|
28
25
|
}
|
29
26
|
|
30
|
-
|
27
|
+
args = options_command_line
|
28
|
+
file = options_config_file(args) || {}
|
29
|
+
deep_merge default, deep_merge(file, args)
|
30
|
+
end
|
31
|
+
|
32
|
+
def options_command_line
|
33
|
+
opt = { "dmenu" => {} }
|
34
|
+
OptionParser.new do |o|
|
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 }
|
37
|
+
o.on("-r", "switch to a window only when <Return> is pressed") do
|
38
|
+
opt['dmenu']['selection_hook_activation_return_key_only'] = true
|
39
|
+
end
|
40
|
+
end.parse!
|
41
|
+
opt
|
42
|
+
end
|
43
|
+
|
44
|
+
def options_config_file opt
|
45
|
+
file = opt["conf"] || -> do
|
46
|
+
xdg_config_home = ENV['XDG_CONFIG_HOME'] || File.expand_path('~/.config')
|
47
|
+
File.join xdg_config_home, 'fvwm-window-search', 'conf.yaml'
|
48
|
+
end.call
|
49
|
+
r = File.read file rescue nil
|
50
|
+
YAML.safe_load(r, filename: file) rescue abort "invalid config: #{$!}" if r
|
51
|
+
end
|
52
|
+
|
53
|
+
def deep_merge first, second
|
54
|
+
merger = proc {|_,v1,v2| Hash === v1 && Hash === v2 ? v1.merge(v2, &merger) : v2 }
|
55
|
+
first.merge(second, &merger)
|
31
56
|
end
|
32
57
|
|
33
|
-
def
|
34
|
-
|
35
|
-
|
58
|
+
def helper exe; File.join(__dir__, "_out/#{exe}"); end
|
59
|
+
|
60
|
+
def dmenu_cmd params
|
61
|
+
[helper('dmenu/dmenu')] + params.map do |k,v|
|
36
62
|
k = "-"+k
|
37
63
|
if !!v == v
|
38
64
|
v ? k : nil
|
@@ -40,25 +66,80 @@ def menu params, text
|
|
40
66
|
[k,v]
|
41
67
|
end
|
42
68
|
end.reject(&:nil?).flatten.map(&:to_s)
|
43
|
-
IO.popen(cmd + params, 'w') { |ios| ios.puts text }
|
44
69
|
end
|
45
70
|
|
46
|
-
def
|
47
|
-
|
48
|
-
|
49
|
-
|
71
|
+
def desired patterns, window
|
72
|
+
match = -> (type, value) {
|
73
|
+
include = patterns[type].select {|v| v[0] != '!'}
|
74
|
+
exclude = patterns[type].select {|v| v[0] == '!'}.map {|v| v[1..-1]}
|
75
|
+
|
76
|
+
exclude.each do |pattern|
|
77
|
+
return true if value.match pattern
|
78
|
+
end
|
79
|
+
include.each do |pattern|
|
80
|
+
return false if value.match pattern
|
81
|
+
end
|
82
|
+
true
|
83
|
+
}
|
84
|
+
|
85
|
+
match.call("class", window['class']) &&
|
86
|
+
match.call("resource", window['resource']) &&
|
87
|
+
match.call("name", window['name'])
|
88
|
+
end
|
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 }
|
50
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
|
+
|
114
|
+
def main
|
51
115
|
opt = options
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
116
|
+
pp opt if $DEBUG
|
117
|
+
|
118
|
+
max_len = dmenu_max_text_len opt
|
119
|
+
dmenu = IO.popen(dmenu_cmd(opt['dmenu']), 'r+')
|
120
|
+
|
121
|
+
IO.popen(helper('winlist')).each_line do |line|
|
122
|
+
begin
|
123
|
+
w = JSON.parse line
|
124
|
+
rescue
|
125
|
+
dmenu.puts $!.to_s.gsub(/\n+/m, ' ') # let a user see an error
|
126
|
+
next
|
127
|
+
end
|
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
|
+
|
136
|
+
next unless desired opt['filter-out'], w
|
137
|
+
|
138
|
+
dmenu.puts menu_line(max_len, desk_indicator, w)
|
56
139
|
end
|
57
|
-
winlist = winlist.map do |w|
|
58
|
-
"#{w.name} | #{w.class} | #{w.id}"
|
59
|
-
end.join "\n"
|
60
140
|
|
61
|
-
|
141
|
+
dmenu.close
|
62
142
|
end
|
63
143
|
|
64
|
-
|
144
|
+
# not __FILE__ == $0, for $0 points to a generated stub after `gem install ...`
|
145
|
+
main unless defined? Minitest
|
data/lib.c
ADDED
@@ -0,0 +1,60 @@
|
|
1
|
+
bool prop(Display *dpy, Window wid, Atom expected_type, const char *name,
|
2
|
+
u_char **result, ulong *size) {
|
3
|
+
Atom type;
|
4
|
+
int format;
|
5
|
+
ulong bytes_after;
|
6
|
+
|
7
|
+
Atom atom = XInternAtom(dpy, name, False);
|
8
|
+
int r = XGetWindowProperty(dpy, wid, atom, 0L, ~0L, False,
|
9
|
+
expected_type, &type, &format,
|
10
|
+
size, &bytes_after, result);
|
11
|
+
return r == Success && result;
|
12
|
+
}
|
13
|
+
|
14
|
+
long desktop(Display *dpy, Window wid) {
|
15
|
+
u_char *prop_val = NULL;
|
16
|
+
ulong prop_size;
|
17
|
+
if (!prop(dpy, wid, XA_CARDINAL, "_NET_WM_DESKTOP", &prop_val, &prop_size))
|
18
|
+
return -2;
|
19
|
+
|
20
|
+
long r = -1; // means a window is in a 'sticky' mode
|
21
|
+
if (prop_val) r = ((long*)prop_val)[0];
|
22
|
+
free(prop_val);
|
23
|
+
return r;
|
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
ADDED
@@ -0,0 +1,120 @@
|
|
1
|
+
/*
|
2
|
+
produces line-delimited JSON of currently managed windows by an X
|
3
|
+
window manager:
|
4
|
+
|
5
|
+
{"desk":0,"host":"hm76","name":"xterm","resource":"xterm","class":"XTerm","id":67108878}
|
6
|
+
*/
|
7
|
+
|
8
|
+
#include <stdlib.h>
|
9
|
+
#include <err.h>
|
10
|
+
#include <stdio.h>
|
11
|
+
#include <stdbool.h>
|
12
|
+
#include <string.h>
|
13
|
+
#include <math.h>
|
14
|
+
|
15
|
+
#include <X11/Xlib.h>
|
16
|
+
#include <X11/Xatom.h>
|
17
|
+
#include <X11/Xutil.h>
|
18
|
+
#include <jansson.h>
|
19
|
+
|
20
|
+
#include "lib.c"
|
21
|
+
|
22
|
+
typedef struct {
|
23
|
+
Window *ids;
|
24
|
+
ulong size;
|
25
|
+
} WinList;
|
26
|
+
|
27
|
+
// result (WinList.ids) should be freed
|
28
|
+
WinList winlist(Display *dpy) {
|
29
|
+
WinList list = { .ids = NULL };
|
30
|
+
u_char *result;
|
31
|
+
|
32
|
+
if (!prop(dpy, DefaultRootWindow(dpy), XA_WINDOW, "_NET_CLIENT_LIST_STACKING",
|
33
|
+
&result, &list.size)) {
|
34
|
+
return list;
|
35
|
+
}
|
36
|
+
|
37
|
+
list.ids = (Window*)result;
|
38
|
+
return list;
|
39
|
+
}
|
40
|
+
|
41
|
+
// result should be freed
|
42
|
+
char* wm_client_machine(Display *dpy, Window wid) {
|
43
|
+
u_char *prop_val = NULL;
|
44
|
+
ulong prop_size;
|
45
|
+
prop(dpy, wid, XA_STRING, "WM_CLIENT_MACHINE", &prop_val, &prop_size);
|
46
|
+
return prop_val ? (char*)prop_val : strdup("nil");
|
47
|
+
}
|
48
|
+
|
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");
|
55
|
+
return r;
|
56
|
+
}
|
57
|
+
|
58
|
+
// result should be freed
|
59
|
+
char* wm_name(Display *dpy, Window wid) {
|
60
|
+
u_char *prop_val = NULL;
|
61
|
+
ulong prop_size;
|
62
|
+
|
63
|
+
bool r = prop(dpy, wid, myAtoms.UTF8_STRING, "_NET_WM_NAME", &prop_val, &prop_size);
|
64
|
+
if (r && prop_val) return (char*)prop_val;
|
65
|
+
|
66
|
+
prop(dpy, wid, XA_STRING, "WM_NAME", &prop_val, &prop_size);
|
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;
|
81
|
+
}
|
82
|
+
|
83
|
+
|
84
|
+
|
85
|
+
int main() {
|
86
|
+
Display *dpy = XOpenDisplay(getenv("DISPLAY"));
|
87
|
+
if (!dpy) errx(1, "failed to open display %s", getenv("DISPLAY"));
|
88
|
+
mk_atoms(dpy);
|
89
|
+
|
90
|
+
WinList list = winlist(dpy);
|
91
|
+
for (long idx = list.size-1; idx >= 0; idx--) {
|
92
|
+
ulong wid = list.ids[idx];
|
93
|
+
|
94
|
+
char *host = wm_client_machine(dpy, wid);
|
95
|
+
char *name = wm_name(dpy, wid);
|
96
|
+
XClassHint rc = wm_class(dpy, wid);
|
97
|
+
long desk = desktop(dpy, wid);
|
98
|
+
bool is_desk_cur = desk < 0 || desk == desktop_current(dpy);
|
99
|
+
|
100
|
+
json_t *line = json_object();
|
101
|
+
json_object_set_new(line, "desk", json_integer(desk));
|
102
|
+
json_object_set_new(line, "desk_cur", json_boolean(is_desk_cur));
|
103
|
+
json_object_set_new(line, "host", json_string(host));
|
104
|
+
json_object_set_new(line, "name", json_string(name));
|
105
|
+
json_object_set_new(line, "resource", json_string(rc.res_name));
|
106
|
+
json_object_set_new(line, "class", json_string(rc.res_class));
|
107
|
+
json_object_set_new(line, "id", json_integer(wid));
|
108
|
+
|
109
|
+
char *dump = json_dumps(line, JSON_COMPACT);
|
110
|
+
printf("%s\n", dump);
|
111
|
+
free(dump);
|
112
|
+
json_decref(line);
|
113
|
+
|
114
|
+
free(host);
|
115
|
+
free(name);
|
116
|
+
free(rc.res_name);
|
117
|
+
free(rc.res_class);
|
118
|
+
}
|
119
|
+
XFree(list.ids);
|
120
|
+
}
|
metadata
CHANGED
@@ -1,19 +1,25 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: fvwm-window-search
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 2.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Alexander Gromnitsky
|
8
|
-
autorequire:
|
8
|
+
autorequire:
|
9
9
|
bindir: "."
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2021-04-09 00:00:00.000000000 Z
|
12
12
|
dependencies: []
|
13
13
|
description: |
|
14
|
-
|
15
|
-
|
16
|
-
|
14
|
+
A window switcher: search for windows interactively using a patched
|
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.
|
20
|
+
|
21
|
+
It differs from rofi & co in that it activates (brings up) windows
|
22
|
+
_during_ the search.
|
17
23
|
email: alexander.gromnitsky@gmail.com
|
18
24
|
executables:
|
19
25
|
- fvwm-window-search
|
@@ -24,15 +30,18 @@ files:
|
|
24
30
|
- "./fvwm-window-search"
|
25
31
|
- Makefile
|
26
32
|
- README.md
|
33
|
+
- activate.c
|
34
|
+
- activate.sh
|
27
35
|
- dmenu.patch
|
28
36
|
- extconf.rb
|
29
|
-
-
|
30
|
-
- lib.
|
37
|
+
- fontinfo.c
|
38
|
+
- lib.c
|
39
|
+
- winlist.c
|
31
40
|
homepage: https://github.com/gromnitsky/fvwm-window-search
|
32
41
|
licenses:
|
33
42
|
- MIT
|
34
43
|
metadata: {}
|
35
|
-
post_install_message:
|
44
|
+
post_install_message:
|
36
45
|
rdoc_options: []
|
37
46
|
require_paths:
|
38
47
|
- lib
|
@@ -40,15 +49,16 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
40
49
|
requirements:
|
41
50
|
- - ">="
|
42
51
|
- !ruby/object:Gem::Version
|
43
|
-
version:
|
52
|
+
version: 2.1.0
|
44
53
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
45
54
|
requirements:
|
46
55
|
- - ">="
|
47
56
|
- !ruby/object:Gem::Version
|
48
57
|
version: '0'
|
49
58
|
requirements: []
|
50
|
-
rubygems_version: 3.
|
51
|
-
signing_key:
|
59
|
+
rubygems_version: 3.2.3
|
60
|
+
signing_key:
|
52
61
|
specification_version: 4
|
53
|
-
summary:
|
62
|
+
summary: 'A window switcher: an interactive incremental windows search & selection
|
63
|
+
for X Window'
|
54
64
|
test_files: []
|
data/focus.sh
DELETED
data/lib.rb
DELETED
@@ -1,105 +0,0 @@
|
|
1
|
-
module FvwmWindowSearch; end
|
2
|
-
|
3
|
-
class FvwmWindowSearch::Window
|
4
|
-
def initialize xwininfo_line
|
5
|
-
@line = xwininfo_line.match(/^([x0-9a-f]+)\s+(["\(].+["\)]):\s+\((.*)\)\s+([x0-9+-]+)\s+([0-9+-]+)$/)
|
6
|
-
raise "invalid xwininfo line" unless @line
|
7
|
-
@dim = parse
|
8
|
-
end
|
9
|
-
|
10
|
-
def parse
|
11
|
-
dim = {}
|
12
|
-
if @line[4]
|
13
|
-
m4 = @line[4].match(/^([0-9]+)x([0-9]+)\+([0-9-]+)\+([0-9-]+)$/)
|
14
|
-
if m4
|
15
|
-
dim[:w] = m4[1].to_i
|
16
|
-
dim[:h] = m4[2].to_i
|
17
|
-
dim[:x_rel] = m4[3].to_i
|
18
|
-
dim[:y_rel] = m4[4].to_i
|
19
|
-
end
|
20
|
-
end
|
21
|
-
|
22
|
-
if @line[5]
|
23
|
-
m5 = @line[5].match(/^\+([0-9-]+)\+([0-9-]+)$/)
|
24
|
-
if m5
|
25
|
-
dim[:x] = m5[1].to_i
|
26
|
-
dim[:y] = m5[2].to_i
|
27
|
-
end
|
28
|
-
end
|
29
|
-
|
30
|
-
dim
|
31
|
-
end
|
32
|
-
|
33
|
-
def id; @line[1]; end
|
34
|
-
|
35
|
-
def name;
|
36
|
-
return unless @line[2]
|
37
|
-
@line[2] == '(has no name)' ? nil : @line[2][1..-2]
|
38
|
-
end
|
39
|
-
|
40
|
-
def resource; @line[3]&.split(' ')&.dig(0)&.slice(1..-2); end
|
41
|
-
def class; @line[3]&.split(' ')&.dig(1)&.slice(1..-2); end
|
42
|
-
def width; @dim[:w]; end
|
43
|
-
def height; @dim[:h]; end
|
44
|
-
def x; @dim[:x]; end # an absolute upper-left X
|
45
|
-
def y; @dim[:y]; end # an absolute upper-left Y
|
46
|
-
def x_rel; @dim[:x_rel]; end
|
47
|
-
def y_rel; @dim[:y_rel]; end
|
48
|
-
|
49
|
-
def useful?
|
50
|
-
return false unless @line
|
51
|
-
return false if width == 0 || height == 0
|
52
|
-
return false if (x == x_rel) && (y == y_rel)
|
53
|
-
return false if x_rel > 0 || y_rel > 0
|
54
|
-
return false unless self.class
|
55
|
-
true
|
56
|
-
end
|
57
|
-
|
58
|
-
def inspect
|
59
|
-
"#<Window> id=#{id}, name=#{name}, resource=#{resource}, class=#{self.class}"
|
60
|
-
end
|
61
|
-
end
|
62
|
-
|
63
|
-
module FvwmWindowSearch
|
64
|
-
def windows
|
65
|
-
`xwininfo -root -tree`.split("\n")
|
66
|
-
.select {|v| v.match(/^\s*0x.+/)}
|
67
|
-
.map(&:strip)
|
68
|
-
.map {|v| Window.new(v)}
|
69
|
-
.select(&:useful?)
|
70
|
-
end
|
71
|
-
|
72
|
-
def windows_filter patterns, winlist
|
73
|
-
desired = -> (type, value) {
|
74
|
-
include = patterns[type].filter{|v| v[0] != '!'}
|
75
|
-
exclude = patterns[type].filter{|v| v[0] == '!'}.map {|v| v[1..-1]}
|
76
|
-
|
77
|
-
exclude.each do |pattern|
|
78
|
-
return true if value.match pattern
|
79
|
-
end
|
80
|
-
include.each do |pattern|
|
81
|
-
return false if value.match pattern
|
82
|
-
end
|
83
|
-
true
|
84
|
-
}
|
85
|
-
|
86
|
-
winlist.filter { |w| desired.call "class", w.class }
|
87
|
-
.filter{ |w| desired.call "resource", w.resource }
|
88
|
-
.filter{ |w| desired.call "name", w.name }
|
89
|
-
end
|
90
|
-
|
91
|
-
def deep_merge first, second
|
92
|
-
merger = proc { |_, v1, v2| Hash === v1 && Hash === v2 ? v1.merge(v2, &merger) : v2 }
|
93
|
-
first.merge(second, &merger)
|
94
|
-
end
|
95
|
-
|
96
|
-
def errx exit_code, msg
|
97
|
-
$stderr.puts "#{File.basename $0} error: #{msg}"
|
98
|
-
exit exit_code
|
99
|
-
end
|
100
|
-
|
101
|
-
def which cmd
|
102
|
-
ENV['PATH'].split(File::PATH_SEPARATOR).map {|v| File.join v, cmd }
|
103
|
-
.find {|v| File.executable?(v) && !File.directory?(v) }
|
104
|
-
end
|
105
|
-
end
|