fvwm-window-search 1.2.0 → 2.0.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.
Files changed (10) hide show
  1. checksums.yaml +4 -4
  2. data/Makefile +9 -1
  3. data/README.md +5 -5
  4. data/focus.c +80 -0
  5. data/focus.sh +5 -15
  6. data/fvwm-window-search +5 -8
  7. data/lib.c +24 -0
  8. data/lib.rb +7 -75
  9. data/winlist.c +124 -0
  10. metadata +9 -6
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: dd55a9dff4bdaee0fcb2e3aab600d85f6c0d40b8a706ea1dd0d7d17e2c0a83c8
4
- data.tar.gz: b47ae4bac4a1a0e68f629306de58e76f6d0111f48e1a9bb686fb63362179161b
3
+ metadata.gz: 2819e711c1b51e6a619fbbf5839481b9a8019fc2c6db40265bda3756c65cad51
4
+ data.tar.gz: 8a58cab8dfc2e504e83d9b71a430a52bb95a63144fc5c499db02172078c57cb6
5
5
  SHA512:
6
- metadata.gz: 13ce630d60e59c33f1bd26c6b29f4dcca5134dabc8d4f0a6a54f812e9d5d50ebb4ca644031e126785cefdaad6cd95ce008a3ca7167deb04b103a0e9dfc43ed53
7
- data.tar.gz: 00311f2288294e4556f3e38b297c33581a7f0ab0b8ced615daf07d31c88682d0555b164a8eb5be6d68729495a8460469ed244511d93d8f2cab0793abac7a8fbc
6
+ metadata.gz: 9d0ab3a08c7bf6993e74c6c8aa07cfbbc1d34dfbc0a31bc6e61f190d6db4fbe0201305967522bf7507470df21e8183e49032b78faa9ea8e77b5f71fe0603d0d5
7
+ data.tar.gz: 58b49f87d0b92f176b6cf4c80ae0d11fc81f471ed096ac64e50bb74c64c5e52cc929ee437c240c298212ee58cf4af4633d965ebe6ebfc15e27f7f2943bf4c48d
data/Makefile CHANGED
@@ -1,6 +1,8 @@
1
1
  out := _out
2
2
  dmenu := $(out)/dmenu
3
- dmenu.commit := 9b38fda6feda68f95754d5b8932b1a69471df960
3
+ dmenu.commit := 1a13d0465d1a6f4f74bc5b07b04c9bd542f20ba6
4
+
5
+ all: $(out)/.dmenu.build $(out)/focus $(out)/winlist
4
6
 
5
7
  $(out)/.dmenu.build: $(out)/.dmenu.$(dmenu.commit) dmenu.patch
6
8
  patch -d $(dmenu) -p1 < dmenu.patch
@@ -12,5 +14,11 @@ $(out)/.dmenu.$(dmenu.commit):
12
14
  git -C $(dmenu) checkout $(dmenu.commit) -q
13
15
  touch $@
14
16
 
17
+ libs := x11 jansson
18
+ LDFLAGS := $(shell pkg-config --libs $(libs))
19
+ CFLAGS := -g -Wall $(shell pkg-config --cflags $(libs))
20
+ $(out)/%: %.c lib.c
21
+ $(LINK.c) $< $(LOADLIBES) $(LDLIBS) -o $@
22
+
15
23
  # an empty target to satisfy rubygems
16
24
  install:
data/README.md CHANGED
@@ -8,12 +8,12 @@ Incremental window search & immediate switch to the selected window
8
8
  ![demo](https://thumbs.gfycat.com/GenerousRingedFlicker-small.gif)
9
9
 
10
10
  * Should work w/ most stackings X11 window managers.
11
- * Filtering by window name/resource/classe.
11
+ * Filtering by window name/resource/class.
12
12
 
13
13
  ## Reqs
14
14
 
15
15
  * Ruby 2.4+
16
- * `xwininfo` & `xdotool` (`xorg-x11-utils` & `xdotool` Fedora pkgs)
16
+ * `dnf install jansson-devel`
17
17
 
18
18
  ## Compilation
19
19
 
@@ -38,7 +38,7 @@ dmenu:
38
38
  fn: Monospace-12
39
39
  b: false
40
40
  selhook-return-key-focus-only: true
41
- filter:
41
+ filter-out:
42
42
  name: ['System Monitor']
43
43
  resource: []
44
44
  class: []
@@ -50,8 +50,8 @@ CLO.
50
50
 
51
51
  [dmenu(1)]: https://manpages.debian.org/unstable/suckless-tools/dmenu.1.en.html
52
52
 
53
- `filter` key tells what windows should be filtered out. Each value in
54
- a subkey is an array of regexes. See the defaults in
53
+ `filter-out` key tells what windows should be filtered out. Each value
54
+ in a subkey is an array of regexes. See the defaults in
55
55
  `fvwm-window-search` file.
56
56
 
57
57
  ## Bugs
data/focus.c ADDED
@@ -0,0 +1,80 @@
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
+ }
data/focus.sh CHANGED
@@ -1,22 +1,12 @@
1
1
  #!/bin/sh
2
2
 
3
3
  id=`echo "$1" | awk -F'|' '{print $NF} END { exit $NF == "" ? 1 : 0}'` || {
4
- echo "usage: `basename "$0"` 'name | class | id'"
4
+ echo "usage: `basename "$0"` 'foo | bar | id'"
5
5
  exit 1
6
6
  }
7
7
 
8
- # tries to put a mouse pointer as shown on the picture below (▲ is the
9
- # pointer):
10
- #
11
- # +-------------+
12
- # | Title |
13
- # +-------------+
14
- # | ▲ |
15
- # | |
16
- # | |
17
- # +-------------+
8
+ __filename=`readlink -f "$0"`
9
+ __dirname=`dirname "${__filename}"`
18
10
 
19
- eval "`xdotool getwindowgeometry --shell "$id"`"
20
- x=$((WIDTH/2))
21
-
22
- xdotool windowactivate "$id" mousemove --window "$id" "$x" 0
11
+ # shellcheck disable=2086
12
+ ${__dirname}/_out/focus $id
data/fvwm-window-search CHANGED
@@ -15,7 +15,7 @@ def options
15
15
  "b" => true,
16
16
  "i" => true,
17
17
  },
18
- "filter" => {
18
+ "filter-out" => {
19
19
  "name" => [],
20
20
  "resource" => [],
21
21
  "class" => ['^Fvwm', '!^FvwmIdent$']
@@ -61,18 +61,15 @@ def menu params, text
61
61
  end
62
62
 
63
63
  def main
64
- ['xwininfo', 'xdotool'].each do |util|
65
- errx 1, "no #{util} in PATH" unless which util
66
- end
67
-
68
64
  opt = options
69
65
  begin
70
- winlist = windows_filter opt["filter"], windows
66
+ winlist = windows_filter_out opt["filter-out"], windows
71
67
  rescue RegexpError
72
- errx 1, "filter: #{$!}"
68
+ errx 1, "filter-out: #{$!}"
73
69
  end
74
70
  winlist = winlist.map do |w|
75
- "#{w.name} | #{w.class} | #{w.id}"
71
+ desk = w['desk'] == -1 ? '*' : w['desk']
72
+ [desk, w['class'], w['name'], w['host'], '0x'+w['id'].to_s(16)].join ' | '
76
73
  end.join "\n"
77
74
 
78
75
  menu opt["dmenu"], winlist
data/lib.c ADDED
@@ -0,0 +1,24 @@
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;
21
+ if (prop_val) r = ((long*)prop_val)[0];
22
+ free(prop_val);
23
+ return r;
24
+ }
data/lib.rb CHANGED
@@ -1,75 +1,12 @@
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
1
+ require "json"
62
2
 
63
3
  module FvwmWindowSearch
64
4
  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?)
5
+ cmd = File.join(__dir__, '_out/winlist')
6
+ JSON.parse `#{cmd}`
70
7
  end
71
8
 
72
- def windows_filter patterns, winlist
9
+ def windows_filter_out patterns, winlist
73
10
  desired = -> (type, value) {
74
11
  include = patterns[type].select {|v| v[0] != '!'}
75
12
  exclude = patterns[type].select {|v| v[0] == '!'}.map {|v| v[1..-1]}
@@ -83,9 +20,9 @@ module FvwmWindowSearch
83
20
  true
84
21
  }
85
22
 
86
- winlist.select { |w| desired.call "class", w.class }
87
- .select { |w| desired.call "resource", w.resource }
88
- .select { |w| desired.call "name", w.name }
23
+ winlist.select { |w| desired.call "class", w['class'] }
24
+ .select { |w| desired.call "resource", w['resource'] }
25
+ .select { |w| desired.call "name", w['name'] }
89
26
  end
90
27
 
91
28
  def deep_merge first, second
@@ -97,9 +34,4 @@ module FvwmWindowSearch
97
34
  $stderr.puts "#{File.basename $0} error: #{msg}"
98
35
  exit exit_code
99
36
  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
37
  end
data/winlist.c ADDED
@@ -0,0 +1,124 @@
1
+ #include <stdlib.h>
2
+ #include <err.h>
3
+ #include <stdio.h>
4
+ #include <stdbool.h>
5
+ #include <string.h>
6
+ #include <math.h>
7
+
8
+ #include <X11/Xlib.h>
9
+ #include <X11/Xatom.h>
10
+ #include <jansson.h>
11
+
12
+ #include "lib.c"
13
+
14
+ typedef struct {
15
+ Window *ids;
16
+ ulong size;
17
+ } WinList;
18
+
19
+ // result should be freed
20
+ WinList winlist(Display *dpy) {
21
+ WinList list;
22
+ u_char *result;
23
+
24
+ if (!prop(dpy, DefaultRootWindow(dpy), XA_WINDOW, "_NET_CLIENT_LIST",
25
+ &result, &list.size)) {
26
+ list.size = -1;
27
+ return list;
28
+ }
29
+
30
+ list.ids = (Window*)result;
31
+ return list;
32
+ }
33
+
34
+ // result should be freed
35
+ char* wm_client_machine(Display *dpy, Window wid) {
36
+ u_char *prop_val = NULL;
37
+ ulong prop_size;
38
+ prop(dpy, wid, XA_STRING, "WM_CLIENT_MACHINE", &prop_val, &prop_size);
39
+ return (char*)prop_val;
40
+ }
41
+
42
+ ulong str_index(const char *s, char ch) {
43
+ char *p = strchr(s, ch);
44
+ if (!p) return -1;
45
+ return (ulong)(p - s);
46
+ }
47
+
48
+ typedef struct {
49
+ char *resource;
50
+ char *class_name;
51
+ } ResClass;
52
+
53
+ // result should be freed
54
+ ResClass wm_class(Display *dpy, Window wid) {
55
+ ResClass r = {.resource = NULL};
56
+
57
+ u_char *prop_val = NULL;
58
+ ulong prop_size;
59
+ if (!prop(dpy, wid, XA_STRING, "WM_CLASS", &prop_val, &prop_size))
60
+ return r;
61
+
62
+ ulong idx = str_index((char*)prop_val, '\0');
63
+ if (idx < prop_size) {
64
+ r.resource = (char*)malloc(idx+2);
65
+ snprintf(r.resource, idx+1, "%s", prop_val);
66
+
67
+ ulong len = prop_size-idx;
68
+ r.class_name = (char*)malloc(len+1);
69
+ snprintf(r.class_name, len, "%s", prop_val+idx+1);
70
+ }
71
+
72
+ free(prop_val);
73
+ return r;
74
+ }
75
+
76
+ // result should be freed
77
+ char* wm_name(Display *dpy, Window wid) {
78
+ u_char *prop_val = NULL;
79
+ ulong prop_size;
80
+
81
+ Atom utf8_str = XInternAtom(dpy, "UTF8_STRING", False);
82
+ bool r = prop(dpy, wid, utf8_str, "_NET_WM_NAME", &prop_val, &prop_size);
83
+ if (r && prop_val) return (char*)prop_val;
84
+
85
+ prop(dpy, wid, XA_STRING, "WM_NAME", &prop_val, &prop_size);
86
+ return (char*)prop_val;
87
+ }
88
+
89
+
90
+
91
+ int main() {
92
+ Display *dpy = XOpenDisplay(getenv("DISPLAY"));
93
+ if (!dpy) errx(1, "failed to open display %s", getenv("DISPLAY"));
94
+
95
+ json_t *r = json_array();
96
+ WinList list = winlist(dpy);
97
+ for (ulong idx = 0; idx < list.size; idx++) {
98
+ ulong wid = list.ids[idx];
99
+
100
+ char *host = wm_client_machine(dpy, wid);
101
+ char *name = wm_name(dpy, wid);
102
+ ResClass rc = wm_class(dpy, wid);
103
+
104
+ json_t *line = json_object();
105
+ json_object_set_new(line, "desk", json_integer(desktop(dpy, wid)));
106
+ json_object_set_new(line, "host", json_string(host));
107
+ json_object_set_new(line, "name", json_string(name));
108
+ json_object_set_new(line, "resource", json_string(rc.resource));
109
+ json_object_set_new(line, "class", json_string(rc.class_name));
110
+ json_object_set_new(line, "id", json_integer(wid));
111
+ json_array_append_new(r, line);
112
+
113
+ free(host);
114
+ free(name);
115
+ free(rc.resource);
116
+ free(rc.class_name);
117
+ }
118
+ XFree(list.ids);
119
+
120
+ char *dump = json_dumps(r, JSON_COMPACT);
121
+ printf("%s\n", dump);
122
+ free(dump);
123
+ json_decref(r);
124
+ }
metadata CHANGED
@@ -1,20 +1,20 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: fvwm-window-search
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.0
4
+ version: 2.0.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: 2020-07-21 00:00:00.000000000 Z
11
+ date: 2021-04-04 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: |
14
14
  A window switcher: search for windows interactively using a patched
15
15
  dmenu utility (comes with the gem). This was originally made for Fvwm,
16
16
  but it's been fully rewritten to work out-of-the-box with any stacking
17
- window manager. Requires xdotool & xwininfo installed.
17
+ window manager. Requires jansson-devel C library installed.
18
18
 
19
19
  It differs from rofi & co in that it activates (brings up) windows
20
20
  _during_ the search.
@@ -30,13 +30,16 @@ files:
30
30
  - README.md
31
31
  - dmenu.patch
32
32
  - extconf.rb
33
+ - focus.c
33
34
  - focus.sh
35
+ - lib.c
34
36
  - lib.rb
37
+ - winlist.c
35
38
  homepage: https://github.com/gromnitsky/fvwm-window-search
36
39
  licenses:
37
40
  - MIT
38
41
  metadata: {}
39
- post_install_message:
42
+ post_install_message:
40
43
  rdoc_options: []
41
44
  require_paths:
42
45
  - lib
@@ -52,7 +55,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
52
55
  version: '0'
53
56
  requirements: []
54
57
  rubygems_version: 3.1.2
55
- signing_key:
58
+ signing_key:
56
59
  specification_version: 4
57
60
  summary: 'A window switcher: an interactive incremental windows search & selection
58
61
  for X Window'