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.
- checksums.yaml +7 -0
- data/Makefile +16 -0
- data/README.md +64 -0
- data/dmenu.patch +129 -0
- data/extconf.rb +1 -0
- data/focus.sh +22 -0
- data/fvwm-window-search +81 -0
- data/lib.rb +105 -0
- metadata +59 -0
checksums.yaml
ADDED
@@ -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
|
data/Makefile
ADDED
@@ -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:
|
data/README.md
ADDED
@@ -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
|
+

|
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.
|
data/dmenu.patch
ADDED
@@ -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
|
+
|
data/extconf.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
# a dummy file that forces rubygems to run our Makefile during 'gem install'
|
data/focus.sh
ADDED
@@ -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
|
data/fvwm-window-search
ADDED
@@ -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: []
|