fvwm-window-search 2.1.0 → 2.2.0

Sign up to get free protection for your applications and to get access to all the features.
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
- }