fvwm-window-search 1.2.0

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