fvwm-window-search 2.0.0 → 2.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (7) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +17 -7
  3. data/fvwm-window-search +58 -29
  4. data/lib.c +1 -1
  5. data/winlist.c +14 -9
  6. metadata +5 -6
  7. data/lib.rb +0 -37
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2819e711c1b51e6a619fbbf5839481b9a8019fc2c6db40265bda3756c65cad51
4
- data.tar.gz: 8a58cab8dfc2e504e83d9b71a430a52bb95a63144fc5c499db02172078c57cb6
3
+ metadata.gz: 6334e45909512868782e41da16d60c26114f43230f0579b1e2e60918fadca628
4
+ data.tar.gz: 16e792a204f7ed859752781dc537a9e106200ac2c722dd8a581510d90d51d164
5
5
  SHA512:
6
- metadata.gz: 9d0ab3a08c7bf6993e74c6c8aa07cfbbc1d34dfbc0a31bc6e61f190d6db4fbe0201305967522bf7507470df21e8183e49032b78faa9ea8e77b5f71fe0603d0d5
7
- data.tar.gz: 58b49f87d0b92f176b6cf4c80ae0d11fc81f471ed096ac64e50bb74c64c5e52cc929ee437c240c298212ee58cf4af4633d965ebe6ebfc15e27f7f2943bf4c48d
6
+ metadata.gz: ca486c7cac4a994953c0855877b687a44283c106b6e21cc2cba56ea4a95248ddec2b4a9fa3b0b4df1d82f1d8743a33d949566371c68e54c5c96d4646db4af531
7
+ data.tar.gz: 3b6f993cdc08f38fb695fb99ca9f2e50c09150bd3cb65d7423b9d99aa97917e71115863b5b46cf1372d34cf8f5dd40b623fd98df9456f8c8b5cb17006e1167c1
data/README.md CHANGED
@@ -12,21 +12,21 @@ Incremental window search & immediate switch to the selected window
12
12
 
13
13
  ## Reqs
14
14
 
15
- * Ruby 2.4+
15
+ * Ruby 2.1+
16
16
  * `dnf install jansson-devel`
17
17
 
18
18
  ## Compilation
19
19
 
20
- Type `make`. This clones the dmenu repo, patches & builds it. It does
21
- not contravene w/ a system-installed dmenu.
20
+ Type `make`. This clones the dmenu repo, patches & builds it. It
21
+ doesn't interfere w/ a system-installed dmenu.
22
22
 
23
23
  ## Usage
24
24
 
25
25
  ~~~
26
26
  $ ./fvwm-window-search -h
27
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
28
+ -c path an alternative path to conf.yaml
29
+ -r switch to a window only when <Return> is pressed
30
30
  ~~~
31
31
 
32
32
  To customise dmenu or filtering, create a yaml file
@@ -50,10 +50,20 @@ CLO.
50
50
 
51
51
  [dmenu(1)]: https://manpages.debian.org/unstable/suckless-tools/dmenu.1.en.html
52
52
 
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
53
+ `filter-out` key tells what windows should be ignored. Each value in a
54
+ subkey is an array of regexes. See the defaults at the top of
55
55
  `fvwm-window-search` file.
56
56
 
57
+ ## Start-up time
58
+
59
+ As a task switcher, the program must not only run fast, but also
60
+ *start* fast. I managed to get it under 70ms on my laptop, when you
61
+ run `./fvwm-window-search` directly from the repo.
62
+
63
+ This is not the case with rubygems! The latter generates a stub script
64
+ that invokes `./fvwm-window-search` file. This indirection may add
65
+ ~140ms of additional delay.
66
+
57
67
  ## Bugs
58
68
 
59
69
  * Tested only w/ Fvwm3.
data/fvwm-window-search CHANGED
@@ -1,19 +1,18 @@
1
- #!/usr/bin/env ruby
1
+ #!/usr/bin/env -S ruby --disable-gems
2
2
 
3
- require_relative './lib'
4
- include FvwmWindowSearch
5
3
  require 'yaml'
4
+ require 'json'
6
5
  require 'optparse'
7
6
 
8
7
  def options
9
8
  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,
9
+ "dmenu" => { # each key corresponds to a dmenu CL option, except for selhook*
13
10
  "fn" => "Monospace-10",
14
11
  "l" => 8,
15
12
  "b" => true,
16
13
  "i" => true,
14
+ "selhook" => File.join(__dir__, "focus.sh %s"),
15
+ "selhook-return-key-focus-only" => false, # FIXME: rename
17
16
  },
18
17
  "filter-out" => {
19
18
  "name" => [],
@@ -22,34 +21,38 @@ def options
22
21
  }
23
22
  }
24
23
 
25
- cl = options_load_cl
26
- env = deep_merge(default, options_load_file(cl) || {})
27
- deep_merge env, cl
24
+ args = options_command_line
25
+ file = options_config_file(args) || {}
26
+ deep_merge default, deep_merge(file, args)
28
27
  end
29
28
 
30
- def options_load_cl
29
+ def options_command_line
31
30
  opt = { "dmenu" => {} }
32
31
  OptionParser.new do |o|
33
32
  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
33
+ o.on("-r", "switch to a window only when <Return> is pressed") do
35
34
  opt["dmenu"]["selhook-return-key-focus-only"] = true
36
35
  end
37
36
  end.parse!
38
37
  opt
39
38
  end
40
39
 
41
- def options_load_file opt # return nil if config wasn't found
42
- conf = opt["conf"] || -> do
40
+ def options_config_file opt
41
+ file = opt["conf"] || -> do
43
42
  xdg_config_home = ENV['XDG_CONFIG_HOME'] || File.expand_path('~/.config')
44
43
  File.join xdg_config_home, 'fvwm-window-search', 'conf.yaml'
45
44
  end.call
46
- r = File.read conf rescue nil
47
- YAML.load r, conf rescue errx 1, "invalid config: #{$!}" if r
45
+ r = File.read file rescue nil
46
+ YAML.safe_load(r, filename: file) rescue abort "invalid config: #{$!}" if r
48
47
  end
49
48
 
50
- def menu params, text
51
- cmd = [File.join(__dir__, "_out/dmenu/dmenu")]
52
- params = params.map do |k,v|
49
+ def deep_merge first, second
50
+ merger = proc {|_,v1,v2| Hash === v1 && Hash === v2 ? v1.merge(v2, &merger) : v2 }
51
+ first.merge(second, &merger)
52
+ end
53
+
54
+ def dmenu_cmd params
55
+ [File.join(__dir__, "_out/dmenu/dmenu")] + params.map do |k,v|
53
56
  k = "-"+k
54
57
  if !!v == v
55
58
  v ? k : nil
@@ -57,22 +60,48 @@ def menu params, text
57
60
  [k,v]
58
61
  end
59
62
  end.reject(&:nil?).flatten.map(&:to_s)
60
- IO.popen(cmd + params, 'w') { |ios| ios.puts text }
63
+ end
64
+
65
+ def desired patterns, window
66
+ match = -> (type, value) {
67
+ include = patterns[type].select {|v| v[0] != '!'}
68
+ exclude = patterns[type].select {|v| v[0] == '!'}.map {|v| v[1..-1]}
69
+
70
+ exclude.each do |pattern|
71
+ return true if value.match pattern
72
+ end
73
+ include.each do |pattern|
74
+ return false if value.match pattern
75
+ end
76
+ true
77
+ }
78
+
79
+ match.call("class", window['class']) &&
80
+ match.call("resource", window['resource']) &&
81
+ match.call("name", window['name'])
61
82
  end
62
83
 
63
84
  def main
64
85
  opt = options
65
- begin
66
- winlist = windows_filter_out opt["filter-out"], windows
67
- rescue RegexpError
68
- errx 1, "filter-out: #{$!}"
69
- end
70
- winlist = winlist.map do |w|
86
+ pp opt if $DEBUG
87
+ dmenu = IO.popen(dmenu_cmd(opt['dmenu']), 'r+')
88
+
89
+ IO.popen(File.join(__dir__, '_out/winlist')).each_line do |line|
90
+ begin
91
+ w = JSON.parse line
92
+ rescue
93
+ dmenu.puts $!.to_s.gsub(/\n+/m, ' ') # let a user see an error
94
+ next
95
+ end
96
+
97
+ next unless desired opt['filter-out'], w
98
+
71
99
  desk = w['desk'] == -1 ? '*' : w['desk']
72
- [desk, w['class'], w['name'], w['host'], '0x'+w['id'].to_s(16)].join ' | '
73
- end.join "\n"
100
+ dmenu.puts [desk, w['class'], w['name'], w['host'], '0x'+w['id'].to_s(16)].join ' | '
101
+ end
74
102
 
75
- menu opt["dmenu"], winlist
103
+ dmenu.close
76
104
  end
77
105
 
78
- main
106
+ # not __FILE__ == $0, for $0 points to a generated stub after `gem install ...`
107
+ main unless defined? Minitest
data/lib.c CHANGED
@@ -17,7 +17,7 @@ long desktop(Display *dpy, Window wid) {
17
17
  if (!prop(dpy, wid, XA_CARDINAL, "_NET_WM_DESKTOP", &prop_val, &prop_size))
18
18
  return -2;
19
19
 
20
- long r = -1;
20
+ long r = -1; // means a window is in a 'sticky' mode
21
21
  if (prop_val) r = ((long*)prop_val)[0];
22
22
  free(prop_val);
23
23
  return r;
data/winlist.c CHANGED
@@ -1,3 +1,10 @@
1
+ /*
2
+ produces line-delimited JSON of currently managed windows by an X
3
+ window manager:
4
+
5
+ {"desk":0,"host":"hm76","name":"xterm","resource":"xterm","class":"XTerm","id":67108878}
6
+ */
7
+
1
8
  #include <stdlib.h>
2
9
  #include <err.h>
3
10
  #include <stdio.h>
@@ -16,7 +23,7 @@ typedef struct {
16
23
  ulong size;
17
24
  } WinList;
18
25
 
19
- // result should be freed
26
+ // result (WinList.ids) should be freed
20
27
  WinList winlist(Display *dpy) {
21
28
  WinList list;
22
29
  u_char *result;
@@ -50,7 +57,7 @@ typedef struct {
50
57
  char *class_name;
51
58
  } ResClass;
52
59
 
53
- // result should be freed
60
+ // result (ResClass.*) should be freed
54
61
  ResClass wm_class(Display *dpy, Window wid) {
55
62
  ResClass r = {.resource = NULL};
56
63
 
@@ -92,7 +99,6 @@ int main() {
92
99
  Display *dpy = XOpenDisplay(getenv("DISPLAY"));
93
100
  if (!dpy) errx(1, "failed to open display %s", getenv("DISPLAY"));
94
101
 
95
- json_t *r = json_array();
96
102
  WinList list = winlist(dpy);
97
103
  for (ulong idx = 0; idx < list.size; idx++) {
98
104
  ulong wid = list.ids[idx];
@@ -108,7 +114,11 @@ int main() {
108
114
  json_object_set_new(line, "resource", json_string(rc.resource));
109
115
  json_object_set_new(line, "class", json_string(rc.class_name));
110
116
  json_object_set_new(line, "id", json_integer(wid));
111
- json_array_append_new(r, line);
117
+
118
+ char *dump = json_dumps(line, JSON_COMPACT);
119
+ printf("%s\n", dump);
120
+ free(dump);
121
+ json_decref(line);
112
122
 
113
123
  free(host);
114
124
  free(name);
@@ -116,9 +126,4 @@ int main() {
116
126
  free(rc.class_name);
117
127
  }
118
128
  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
129
  }
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: 2.0.0
4
+ version: 2.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alexander Gromnitsky
8
8
  autorequire:
9
9
  bindir: "."
10
10
  cert_chain: []
11
- date: 2021-04-04 00:00:00.000000000 Z
11
+ date: 2021-04-05 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 jansson-devel C library installed.
17
+ window manager. Requires a preinstalled jansson-devel C library.
18
18
 
19
19
  It differs from rofi & co in that it activates (brings up) windows
20
20
  _during_ the search.
@@ -33,7 +33,6 @@ files:
33
33
  - focus.c
34
34
  - focus.sh
35
35
  - lib.c
36
- - lib.rb
37
36
  - winlist.c
38
37
  homepage: https://github.com/gromnitsky/fvwm-window-search
39
38
  licenses:
@@ -47,14 +46,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
47
46
  requirements:
48
47
  - - ">="
49
48
  - !ruby/object:Gem::Version
50
- version: 2.4.0
49
+ version: 2.1.0
51
50
  required_rubygems_version: !ruby/object:Gem::Requirement
52
51
  requirements:
53
52
  - - ">="
54
53
  - !ruby/object:Gem::Version
55
54
  version: '0'
56
55
  requirements: []
57
- rubygems_version: 3.1.2
56
+ rubygems_version: 3.2.3
58
57
  signing_key:
59
58
  specification_version: 4
60
59
  summary: 'A window switcher: an interactive incremental windows search & selection
data/lib.rb DELETED
@@ -1,37 +0,0 @@
1
- require "json"
2
-
3
- module FvwmWindowSearch
4
- def windows
5
- cmd = File.join(__dir__, '_out/winlist')
6
- JSON.parse `#{cmd}`
7
- end
8
-
9
- def windows_filter_out patterns, winlist
10
- desired = -> (type, value) {
11
- include = patterns[type].select {|v| v[0] != '!'}
12
- exclude = patterns[type].select {|v| v[0] == '!'}.map {|v| v[1..-1]}
13
-
14
- exclude.each do |pattern|
15
- return true if value.match pattern
16
- end
17
- include.each do |pattern|
18
- return false if value.match pattern
19
- end
20
- true
21
- }
22
-
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'] }
26
- end
27
-
28
- def deep_merge first, second
29
- merger = proc { |_, v1, v2| Hash === v1 && Hash === v2 ? v1.merge(v2, &merger) : v2 }
30
- first.merge(second, &merger)
31
- end
32
-
33
- def errx exit_code, msg
34
- $stderr.puts "#{File.basename $0} error: #{msg}"
35
- exit exit_code
36
- end
37
- end