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