fvwm-window-search 1.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.
Files changed (9) hide show
  1. checksums.yaml +7 -0
  2. data/Makefile +16 -0
  3. data/README.md +64 -0
  4. data/dmenu.patch +129 -0
  5. data/extconf.rb +1 -0
  6. data/focus.sh +22 -0
  7. data/fvwm-window-search +81 -0
  8. data/lib.rb +105 -0
  9. metadata +59 -0
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: dd55a9dff4bdaee0fcb2e3aab600d85f6c0d40b8a706ea1dd0d7d17e2c0a83c8
4
+ data.tar.gz: b47ae4bac4a1a0e68f629306de58e76f6d0111f48e1a9bb686fb63362179161b
5
+ SHA512:
6
+ metadata.gz: 13ce630d60e59c33f1bd26c6b29f4dcca5134dabc8d4f0a6a54f812e9d5d50ebb4ca644031e126785cefdaad6cd95ce008a3ca7167deb04b103a0e9dfc43ed53
7
+ data.tar.gz: 00311f2288294e4556f3e38b297c33581a7f0ab0b8ced615daf07d31c88682d0555b164a8eb5be6d68729495a8460469ed244511d93d8f2cab0793abac7a8fbc
@@ -0,0 +1,16 @@
1
+ out := _out
2
+ dmenu := $(out)/dmenu
3
+ dmenu.commit := 9b38fda6feda68f95754d5b8932b1a69471df960
4
+
5
+ $(out)/.dmenu.build: $(out)/.dmenu.$(dmenu.commit) dmenu.patch
6
+ patch -d $(dmenu) -p1 < dmenu.patch
7
+ make -C $(dmenu)
8
+ touch $@
9
+
10
+ $(out)/.dmenu.$(dmenu.commit):
11
+ git clone https://git.suckless.org/dmenu $(dmenu)
12
+ git -C $(dmenu) checkout $(dmenu.commit) -q
13
+ touch $@
14
+
15
+ # an empty target to satisfy rubygems
16
+ install:
@@ -0,0 +1,64 @@
1
+ # fvwm-window-search
2
+
3
+ Incremental window search & immediate switch to the selected window
4
+ *during the search*. Uses a patched version of dmenu as a GUI.
5
+
6
+ $ gem install fvwm-window-search
7
+
8
+ ![demo](https://thumbs.gfycat.com/GenerousRingedFlicker-small.gif)
9
+
10
+ * Should work w/ most stackings X11 window managers.
11
+ * Filtering by window name/resource/classe.
12
+
13
+ ## Reqs
14
+
15
+ * Ruby 2.4+
16
+ * `xwininfo` & `xdotool` (`xorg-x11-utils` & `xdotool` Fedora pkgs)
17
+
18
+ ## Compilation
19
+
20
+ Type `make`. This clones the dmenu repo, patches & builds it. It does
21
+ not contravene w/ a system-installed dmenu.
22
+
23
+ ## Usage
24
+
25
+ ~~~
26
+ $ ./fvwm-window-search -h
27
+ Usage: fvwm-window-search [options]
28
+ -c path an alternative path to conf.yaml
29
+ -r focus a window only if Return is pressed
30
+ ~~~
31
+
32
+ To customise dmenu or filtering, create a yaml file
33
+ `$XDG_CONFIG_HOME/fvwm-window-search/conf.yaml`, e.g.:
34
+
35
+ ~~~
36
+ ---
37
+ dmenu:
38
+ fn: Monospace-12
39
+ b: false
40
+ selhook-return-key-focus-only: true
41
+ filter:
42
+ name: ['System Monitor']
43
+ resource: []
44
+ class: []
45
+ ~~~
46
+
47
+ Subkeys in `dmenu` are the usual CLOs for
48
+ [dmenu(1)][]. `selhook-return-key-focus-only` is an equivalent of `-r`
49
+ CLO.
50
+
51
+ [dmenu(1)]: https://manpages.debian.org/unstable/suckless-tools/dmenu.1.en.html
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
55
+ `fvwm-window-search` file.
56
+
57
+ ## Bugs
58
+
59
+ * Tested only w/ Fvwm3.
60
+ * No distinction between normal & iconified windows.
61
+
62
+ ## License
63
+
64
+ MIT.
@@ -0,0 +1,129 @@
1
+ diff --git a/Makefile b/Makefile
2
+ index a03a95c..ee5cffb 100644
3
+ --- a/Makefile
4
+ +++ b/Makefile
5
+ @@ -17,8 +17,8 @@ options:
6
+ .c.o:
7
+ $(CC) -c $(CFLAGS) $<
8
+
9
+ -config.h:
10
+ - cp config.def.h $@
11
+ +config.h: config.def.h
12
+ + cp $< $@
13
+
14
+ $(OBJ): arg.h config.h config.mk drw.h
15
+
16
+ diff --git a/config.def.h b/config.def.h
17
+ index 1edb647..1920110 100644
18
+ --- a/config.def.h
19
+ +++ b/config.def.h
20
+ @@ -21,3 +21,7 @@ static unsigned int lines = 0;
21
+ * for example: " /?\"&[]"
22
+ */
23
+ static const char worddelimiters[] = " ";
24
+ +
25
+ +/* -selhook option; run a command on every selection */
26
+ +static const char *selection_hook = NULL;
27
+ +static int selection_hook_return_key_focus_only = 0;
28
+ diff --git a/dmenu.c b/dmenu.c
29
+ index 65f25ce..274668a 100644
30
+ --- a/dmenu.c
31
+ +++ b/dmenu.c
32
+ @@ -304,6 +304,62 @@ movewordedge(int dir)
33
+ }
34
+ }
35
+
36
+ +/*
37
+ + * Replacing all instances of ' with '\'' then enclosing the whole
38
+ + * string in single quotes (') is the safe way.
39
+ + * */
40
+ +static char *
41
+ +shellquote(const char *src)
42
+ +{
43
+ + char *quoted, *q;
44
+ + const char *p;
45
+ +
46
+ + if (!src) return NULL;
47
+ + p = src;
48
+ +
49
+ + if (!(quoted = malloc(strlen(src)*4+3))) die("malloc failed");
50
+ + q = quoted;
51
+ +
52
+ + *q++ = '\'';
53
+ + while (*p) {
54
+ + if (*p == '\'') {
55
+ + *q++ = '\'';
56
+ + *q++ = '\\';
57
+ + *q++ = '\'';
58
+ + *q++ = '\'';
59
+ + } else {
60
+ + *q++ = *p;
61
+ + }
62
+ +
63
+ + p++;
64
+ + }
65
+ + *q++ = '\'';
66
+ + *q = '\0';
67
+ +
68
+ + return quoted;
69
+ +}
70
+ +
71
+ +void
72
+ +selhook(const char *user_command, const struct item *item)
73
+ +{
74
+ + char *cmd, *quoted_item_text;
75
+ + size_t cmd_size;
76
+ +
77
+ + if ( !(user_command && item)) return;
78
+ + quoted_item_text = shellquote(item->text);
79
+ + /* printf("QUOTED: >%s<\n", quoted_item_text); */
80
+ +
81
+ + cmd_size = strlen(user_command) + strlen(quoted_item_text) + 1;
82
+ + if ( !(cmd = malloc(cmd_size))) die("malloc failed");
83
+ + snprintf(cmd, cmd_size, user_command, quoted_item_text);
84
+ +
85
+ + /* printf("SELECTED HOOK: %s\n", cmd); */
86
+ + system(cmd);
87
+ +
88
+ + free(quoted_item_text);
89
+ + free(cmd);
90
+ +}
91
+ +
92
+ static void
93
+ keypress(XKeyEvent *ev)
94
+ {
95
+ @@ -464,6 +520,7 @@ insert:
96
+ break;
97
+ case XK_Return:
98
+ case XK_KP_Enter:
99
+ + selhook(selection_hook, sel);
100
+ puts((sel && !(ev->state & ShiftMask)) ? sel->text : text);
101
+ if (!(ev->state & ControlMask)) {
102
+ cleanup();
103
+ @@ -572,6 +629,8 @@ run(void)
104
+ break;
105
+ case KeyPress:
106
+ keypress(&ev.xkey);
107
+ + if (!selection_hook_return_key_focus_only)
108
+ + selhook(selection_hook, sel);
109
+ break;
110
+ case SelectionNotify:
111
+ if (ev.xselection.property == utf8)
112
+ @@ -712,6 +771,8 @@ main(int argc, char *argv[])
113
+ else if (!strcmp(argv[i], "-i")) { /* case-insensitive item matching */
114
+ fstrncmp = strncasecmp;
115
+ fstrstr = cistrstr;
116
+ + } else if (!strcmp(argv[i], "-selhook-return-key-focus-only")) {
117
+ + selection_hook_return_key_focus_only = 1;
118
+ } else if (i + 1 == argc)
119
+ usage();
120
+ /* these options take one argument */
121
+ @@ -733,6 +794,8 @@ main(int argc, char *argv[])
122
+ colors[SchemeSel][ColFg] = argv[++i];
123
+ else if (!strcmp(argv[i], "-w")) /* embedding window id */
124
+ embed = argv[++i];
125
+ + else if (!strcmp(argv[i], "-selhook")) /* a command to run */
126
+ + selection_hook = argv[++i];
127
+ else
128
+ usage();
129
+
@@ -0,0 +1 @@
1
+ # a dummy file that forces rubygems to run our Makefile during 'gem install'
@@ -0,0 +1,22 @@
1
+ #!/bin/sh
2
+
3
+ id=`echo "$1" | awk -F'|' '{print $NF} END { exit $NF == "" ? 1 : 0}'` || {
4
+ echo "usage: `basename "$0"` 'name | class | id'"
5
+ exit 1
6
+ }
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
+ # +-------------+
18
+
19
+ eval "`xdotool getwindowgeometry --shell "$id"`"
20
+ x=$((WIDTH/2))
21
+
22
+ xdotool windowactivate "$id" mousemove --window "$id" "$x" 0
@@ -0,0 +1,81 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require_relative './lib'
4
+ include FvwmWindowSearch
5
+ require 'yaml'
6
+ require 'optparse'
7
+
8
+ def options
9
+ default = {
10
+ "dmenu" => { # each key is a corresponding CL option
11
+ "selhook" => File.join(__dir__, "focus.sh %s"),
12
+ "selhook-return-key-focus-only" => false,
13
+ "fn" => "Monospace-10",
14
+ "l" => 8,
15
+ "b" => true,
16
+ "i" => true,
17
+ },
18
+ "filter" => {
19
+ "name" => [],
20
+ "resource" => [],
21
+ "class" => ['^Fvwm', '!^FvwmIdent$']
22
+ }
23
+ }
24
+
25
+ cl = options_load_cl
26
+ env = deep_merge(default, options_load_file(cl) || {})
27
+ deep_merge env, cl
28
+ end
29
+
30
+ def options_load_cl
31
+ opt = { "dmenu" => {} }
32
+ OptionParser.new do |o|
33
+ o.on("-c path", "an alternative path to conf.yaml") { |v| opt["conf"] = v }
34
+ o.on("-r", "focus a window only if Return is pressed") do
35
+ opt["dmenu"]["selhook-return-key-focus-only"] = true
36
+ end
37
+ end.parse!
38
+ opt
39
+ end
40
+
41
+ def options_load_file opt # return nil if config wasn't found
42
+ conf = opt["conf"] || -> do
43
+ xdg_config_home = ENV['XDG_CONFIG_HOME'] || File.expand_path('~/.config')
44
+ File.join xdg_config_home, 'fvwm-window-search', 'conf.yaml'
45
+ end.call
46
+ r = File.read conf rescue nil
47
+ YAML.load r, conf rescue errx 1, "invalid config: #{$!}" if r
48
+ end
49
+
50
+ def menu params, text
51
+ cmd = [File.join(__dir__, "_out/dmenu/dmenu")]
52
+ params = params.map do |k,v|
53
+ k = "-"+k
54
+ if !!v == v
55
+ v ? k : nil
56
+ else
57
+ [k,v]
58
+ end
59
+ end.reject(&:nil?).flatten.map(&:to_s)
60
+ IO.popen(cmd + params, 'w') { |ios| ios.puts text }
61
+ end
62
+
63
+ def main
64
+ ['xwininfo', 'xdotool'].each do |util|
65
+ errx 1, "no #{util} in PATH" unless which util
66
+ end
67
+
68
+ opt = options
69
+ begin
70
+ winlist = windows_filter opt["filter"], windows
71
+ rescue RegexpError
72
+ errx 1, "filter: #{$!}"
73
+ end
74
+ winlist = winlist.map do |w|
75
+ "#{w.name} | #{w.class} | #{w.id}"
76
+ end.join "\n"
77
+
78
+ menu opt["dmenu"], winlist
79
+ end
80
+
81
+ main
data/lib.rb ADDED
@@ -0,0 +1,105 @@
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].select {|v| v[0] != '!'}
75
+ exclude = patterns[type].select {|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.select { |w| desired.call "class", w.class }
87
+ .select { |w| desired.call "resource", w.resource }
88
+ .select { |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
metadata ADDED
@@ -0,0 +1,59 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: fvwm-window-search
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.2.0
5
+ platform: ruby
6
+ authors:
7
+ - Alexander Gromnitsky
8
+ autorequire:
9
+ bindir: "."
10
+ cert_chain: []
11
+ date: 2020-07-21 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: |
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 xdotool & xwininfo installed.
18
+
19
+ It differs from rofi & co in that it activates (brings up) windows
20
+ _during_ the search.
21
+ email: alexander.gromnitsky@gmail.com
22
+ executables:
23
+ - fvwm-window-search
24
+ extensions:
25
+ - extconf.rb
26
+ extra_rdoc_files: []
27
+ files:
28
+ - "./fvwm-window-search"
29
+ - Makefile
30
+ - README.md
31
+ - dmenu.patch
32
+ - extconf.rb
33
+ - focus.sh
34
+ - lib.rb
35
+ homepage: https://github.com/gromnitsky/fvwm-window-search
36
+ licenses:
37
+ - MIT
38
+ metadata: {}
39
+ post_install_message:
40
+ rdoc_options: []
41
+ require_paths:
42
+ - lib
43
+ required_ruby_version: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: 2.4.0
48
+ required_rubygems_version: !ruby/object:Gem::Requirement
49
+ requirements:
50
+ - - ">="
51
+ - !ruby/object:Gem::Version
52
+ version: '0'
53
+ requirements: []
54
+ rubygems_version: 3.1.2
55
+ signing_key:
56
+ specification_version: 4
57
+ summary: 'A window switcher: an interactive incremental windows search & selection
58
+ for X Window'
59
+ test_files: []