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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6334e45909512868782e41da16d60c26114f43230f0579b1e2e60918fadca628
4
- data.tar.gz: 16e792a204f7ed859752781dc537a9e106200ac2c722dd8a581510d90d51d164
3
+ metadata.gz: 3e0b567a71daca6fd7780719a9228cd34a63f9cdfbf084a4b4c854f13b60c103
4
+ data.tar.gz: 8b274620b6a4ba391ad4127c91f63bfff85057c971c0678c4850b1a48cdb287b
5
5
  SHA512:
6
- metadata.gz: ca486c7cac4a994953c0855877b687a44283c106b6e21cc2cba56ea4a95248ddec2b4a9fa3b0b4df1d82f1d8743a33d949566371c68e54c5c96d4646db4af531
7
- data.tar.gz: 3b6f993cdc08f38fb695fb99ca9f2e50c09150bd3cb65d7423b9d99aa97917e71115863b5b46cf1372d34cf8f5dd40b623fd98df9456f8c8b5cb17006e1167c1
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)/.dmenu.build $(out)/focus $(out)/winlist
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 jansson
18
- LDFLAGS := $(shell pkg-config --libs $(libs))
19
- CFLAGS := -g -Wall $(shell pkg-config --cflags $(libs))
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
  ![demo](https://thumbs.gfycat.com/GenerousRingedFlicker-small.gif)
9
9
 
10
- * Should work w/ most stackings X11 window managers.
11
- * Filtering by window name/resource/class.
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 an alternative path to conf.yaml
29
- -r switch to a window only when <Return> is pressed
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
- selhook-return-key-focus-only: true
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)][]. `selhook-return-key-focus-only` is an equivalent of `-r`
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
+ }
@@ -9,4 +9,4 @@ __filename=`readlink -f "$0"`
9
9
  __dirname=`dirname "${__filename}"`
10
10
 
11
11
  # shellcheck disable=2086
12
- ${__dirname}/_out/focus $id
12
+ ${__dirname}/_out/activate $id
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..1920110 100644
17
+ index 1edb647..65c831f 100644
18
18
  --- a/config.def.h
19
19
  +++ b/config.def.h
20
- @@ -21,3 +21,7 @@ static unsigned int lines = 0;
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
- +/* -selhook option; run a command on every selection */
25
+ +/* -selection_hook option; run a command on every selection */
26
26
  +static const char *selection_hook = NULL;
27
- +static int selection_hook_return_key_focus_only = 0;
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..274668a 100644
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
- @@ -464,6 +520,7 @@ insert:
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
- @@ -572,6 +629,8 @@ run(void)
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 (!selection_hook_return_key_focus_only)
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 +771,8 @@ main(int argc, char *argv[])
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], "-selhook-return-key-focus-only")) {
117
- + selection_hook_return_key_focus_only = 1;
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 +794,8 @@ main(int argc, char *argv[])
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], "-selhook")) /* a command to run */
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
- "dmenu" => { # each key corresponds to a dmenu CL option, except for selhook*
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
- "selhook" => File.join(__dir__, "focus.sh %s"),
15
- "selhook-return-key-focus-only" => false, # FIXME: rename
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["dmenu"]["selhook-return-key-focus-only"] = true
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
- [File.join(__dir__, "_out/dmenu/dmenu")] + params.map do |k,v|
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(File.join(__dir__, '_out/winlist')).each_line do |line|
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
- desk = w['desk'] == -1 ? '*' : w['desk']
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, "_NET_CLIENT_LIST",
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
- ulong str_index(const char *s, char ch) {
50
- char *p = strchr(s, ch);
51
- if (!p) return -1;
52
- return (ulong)(p - s);
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
- Atom utf8_str = XInternAtom(dpy, "UTF8_STRING", False);
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 (ulong idx = 0; idx < list.size; idx++) {
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
- ResClass rc = wm_class(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);
109
99
 
110
100
  json_t *line = json_object();
111
- json_object_set_new(line, "desk", json_integer(desktop(dpy, wid)));
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.resource));
115
- json_object_set_new(line, "class", json_string(rc.class_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));
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.resource);
126
- free(rc.class_name);
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.1.0
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-05 00:00:00.000000000 Z
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 (comes with the gem). This was originally made for Fvwm,
16
- but it's been fully rewritten to work out-of-the-box with any stacking
17
- window manager. Requires a preinstalled jansson-devel C library.
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
- - focus.c
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
- }