fvwm-window-search 2.0.0 → 2.1.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 (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