sapristi 0.1.2 → 0.1.31
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/workflows/ruby.yml +28 -0
- data/.reek.yml +4 -1
- data/.ruby-version +1 -0
- data/README.md +93 -14
- data/assets/images/sapristi.jpg +0 -0
- data/bin/sapristi +1 -0
- data/lib/sapristi/adapters/linux/process_manager.rb +12 -3
- data/lib/sapristi/adapters/linux/window_manager.rb +5 -0
- data/lib/sapristi/arguments_parser.rb +2 -0
- data/lib/sapristi/attribute_normalizer.rb +4 -4
- data/lib/sapristi/definition.rb +7 -6
- data/lib/sapristi/definition_parser.rb +8 -8
- data/lib/sapristi/definition_processor.rb +5 -3
- data/lib/sapristi/new_process_window_detector.rb +49 -14
- data/lib/sapristi/sapristi.rb +11 -8
- data/lib/sapristi/version.rb +1 -1
- data/lib/sapristi/window_manager.rb +1 -1
- metadata +5 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: fbe69ef3d4dc1b95146ae12ed0806a35e5248d213c2e9c25421cf70e702642bd
|
4
|
+
data.tar.gz: 61ba8b136d1dd8b7f34b55a945bb4297d046b7277bbcdcc3d6da5b72b73af415
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a9b2f36d57d26ec4489a88c810c6e46f465188c8b545ed22b5a034a1152fe25a703fae8cd255741584189a01b4ae7126c1eb50be63a5f0c28b8a889ec494aeca
|
7
|
+
data.tar.gz: d0e84f6ebf91c6a726da427d63e664e9a0b133650ebe9d860ec7518c5a0888a91c546ccd4ef962c6d68970e1f776ea12e8fdccbd9aab01e79851e7088118fc0f
|
@@ -0,0 +1,28 @@
|
|
1
|
+
name: Ruby
|
2
|
+
|
3
|
+
on:
|
4
|
+
push:
|
5
|
+
branches: [ $default-branch ]
|
6
|
+
pull_request:
|
7
|
+
branches: [ $default-branch ]
|
8
|
+
|
9
|
+
jobs:
|
10
|
+
test:
|
11
|
+
|
12
|
+
runs-on: ubuntu-latest
|
13
|
+
|
14
|
+
steps:
|
15
|
+
- uses: actions/checkout@v2
|
16
|
+
- name: Set up Ruby
|
17
|
+
# To automatically get bug fixes and new Ruby versions for ruby/setup-ruby,
|
18
|
+
# change this to (see https://github.com/ruby/setup-ruby#versioning):
|
19
|
+
# uses: ruby/setup-ruby@v1
|
20
|
+
uses: ruby/setup-ruby@ec106b438a1ff6ff109590de34ddc62c540232e0
|
21
|
+
with:
|
22
|
+
bundler-cache: true
|
23
|
+
#with:
|
24
|
+
# ruby-version: 2.5
|
25
|
+
- name: Install dependencies
|
26
|
+
run: bundle install
|
27
|
+
- name: Run tests
|
28
|
+
run: bundle exec rake
|
data/.reek.yml
CHANGED
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
ruby-2.5.3
|
data/README.md
CHANGED
@@ -2,35 +2,111 @@
|
|
2
2
|
|
3
3
|
[![Maintainability](https://api.codeclimate.com/v1/badges/e168b7940a847148f617/maintainability)](https://codeclimate.com/github/sapristi-tool/sapristi/maintainability)
|
4
4
|
|
5
|
-
|
5
|
+
![Sapristi image](/assets/images/sapristi.jpg)
|
6
6
|
|
7
|
-
|
7
|
+
An efficient tool to control your multi-monitor, multi-workspace enviroment. Just define your favorite working arragement in ~/sapristi.csv and execute `sapristi` to load your applications and align them in your favorite fashion.
|
8
8
|
|
9
9
|
## Installation
|
10
10
|
|
11
|
-
|
11
|
+
Install build dependencies
|
12
12
|
|
13
|
-
|
14
|
-
|
13
|
+
`$ sudo apt install build-essential ruby-dev libx11-dev libglib2.0-dev libxmu-dev libgtk-3-dev`
|
14
|
+
|
15
|
+
Install gem
|
16
|
+
|
17
|
+
`$ gem install sapristi`
|
18
|
+
|
19
|
+
## Usage
|
20
|
+
|
21
|
+
`sapristi` load definitions from default configuration file (~/.sapristi.csv) If default configuration file is not found, it will create an empty one.
|
22
|
+
|
23
|
+
> `-f FILE` load your definitions from another file, ie: sapristi -f ~/machine_learning_definitions.csv
|
24
|
+
|
25
|
+
> `-v | --verbose` verbose mode.
|
26
|
+
|
27
|
+
> `--dry-run` dry mode, show your definitions but it doesn't execute them.
|
28
|
+
|
29
|
+
> `-g|--group name` load definitions tagged with group, ie: sapristi -g social
|
30
|
+
|
31
|
+
|
32
|
+
### Configuration example: ~/.sapristi.csv
|
33
|
+
|
34
|
+
| __Title__ | __Command__ | __Monitor__ | __X__ | __Y__ | __Width__ | __Height__ | __Workspace__ | __Group__ |
|
35
|
+
|-------|---------------------------------------------------------------------------------|---------|------------|------------|--------|--------|-----------|----------|
|
36
|
+
| | subl ~/projects/ruby/sapristi | | 0 | 0 | 60% | 100% | 0 | sapristi |
|
37
|
+
| | terminator --working-directory=~/projects/ruby/sapristi | | 60% | 0 | 40% | 50% | 0 | sapristi |
|
38
|
+
| | zeal | | 60% | 50% | 40% | 50% | 0 | sapristi |
|
39
|
+
| | subl ~/projects/python/stuff | | 0 | 0 | 60% | 100% | 1 | python |
|
40
|
+
| | terminator --working-directory=~/projects/python/stuff | | 60% | 0 | 40% | 50% | 1 | python |
|
41
|
+
| | firefox --new-window https://docs.python.org/3/index.html | | 60% | 50% | 40% | 50% | 1 | python |
|
42
|
+
| | firefox --new-window https://www.gmail.com | | 0 | 0 | 50% | 100% | 2 | social |
|
43
|
+
| | firefox --new-window https://www.slack.com | | 50% | 0 | 50% | 50% | 2 | social |
|
44
|
+
| | firefox --new-window https://www.twitter.com | | 50% | | 50% | 50% | 2 | social |
|
45
|
+
| | sol | DP-2 | 0 | 0 | 100% | 100% | 3 | games |
|
46
|
+
|
47
|
+
The configuration file is a CSV file (comma delimited, no separator). First line is the header line, next lines are definitions. Each definition prescribes:
|
48
|
+
- How choose a window from the current ones, or how to launch a program to produce the window: __Title__, __Command__
|
49
|
+
- Which monitor to place it: __Monitor__
|
50
|
+
- Which workspace: __Workspace__
|
51
|
+
- Desired window geometry: __X__, __Y__, __Width__, __Height__
|
52
|
+
|
53
|
+
The table above represents a CSV file like the one below:
|
54
|
+
```
|
55
|
+
Title,Command,Monitor,X,Y,Width,Height,Workspace
|
56
|
+
,subl ~/projects/ruby/sapristi,0,0,60%,100%,0,sapristi
|
57
|
+
,terminator --working-directory=~/projects/ruby/sapristi,60%,0,40%,50%,0,sapristi
|
58
|
+
,zeal,60%,50%,40%,50%,0,sapristi
|
59
|
+
,subl ~/projects/python/stuff,0,0,60%,100%,1,python
|
60
|
+
,terminator --working-directory=~/projects/python/stuff,60%,0,40%,50%,1,python
|
61
|
+
,firefox --new-window https://docs.python.org/3/index.html,60%,50%,40%,50%,1,python
|
62
|
+
,firefox --new-window https://www.gmail.com,0,0,50%,100%,2,social
|
63
|
+
,firefox --new-window https://www.slack.com,50%,0,50%,50%,2,social
|
64
|
+
,firefox --new-window https://www.twitter.com,50%,50%,50%,2,social
|
65
|
+
,sol,DP-2,0,0,100%,100%,3,games
|
15
66
|
```
|
16
67
|
|
17
|
-
And then execute:
|
18
68
|
|
19
|
-
|
69
|
+
#### Fields:
|
20
70
|
|
21
|
-
|
71
|
+
- __Title__(optional): Regex If defined, sapristi will try to find a window whose title matches the regular expression. Examples:
|
72
|
+
- \(sapristi\) - Sublime
|
73
|
+
- Twitter.+Firefox
|
74
|
+
- System Monitor
|
22
75
|
|
23
|
-
|
76
|
+
- __Command__(optional): A command. If __Title__ is not provided or there isn't a window that matches it, sapristi will execute __Command__.
|
77
|
+
Every line has to define a __Title__, a __Command__ or both. Examples:
|
78
|
+
- firefox --new-window https://www.twitter.com
|
79
|
+
- terminator --working-directory=~/projects/python/stuff
|
80
|
+
|
24
81
|
|
25
|
-
|
82
|
+
- __Monitor__(optional): Monitor name (check your monitor names with xrandr) If a definition specifies a monitor not present or if is empty, window will be placed in the main monitor of the actual environment.
|
83
|
+
- Use monitor when specified.
|
84
|
+
- Use main monitor if monitor name is not found.
|
85
|
+
- Use main monitor if __Monitor__ is not provided.
|
86
|
+
|
87
|
+
- __X__(mandatory): Absolute or relative. Horizontal top left coordinate to place the window:
|
88
|
+
- Absolute (pixels): ie 100, 200, 250.
|
89
|
+
- Relative (monitor workarea): 10%, 20%, 50%. Percentage has to be an integer between 0 and 100.
|
90
|
+
|
91
|
+
- __Y__(mandatory): Absolute or relative. Vertical top left coordinate to place the window:
|
92
|
+
- Absolute (pixels): ie 100, 200, 250.
|
93
|
+
- Relative (monitor workarea): 10%, 20%, 50%. Percentage has to be an integer between 0 and 100.
|
26
94
|
|
27
|
-
|
95
|
+
- __Width__(mandatory): Absolute (pixels) or relative (workarea) Window width. Examples: 100, 50%.
|
28
96
|
|
29
|
-
|
97
|
+
- __Height__(mandatory): Absolute (pixels) or relative (workarea) Window height. Examples: 100, 50%.
|
30
98
|
|
31
|
-
|
99
|
+
- __Workspace__(optional): Workspace/desktop to place the window, current workspace if it is not defined. Examples: 0, 1, 5.
|
32
100
|
|
33
|
-
|
101
|
+
## Requirements
|
102
|
+
|
103
|
+
Linux
|
104
|
+
|
105
|
+
See ruby-wmctrl (`libx11-dev libglib2.0-dev libxmu-dev`) and Ruby/GTK (`libgtk-3-dev`) gem requirements.
|
106
|
+
|
107
|
+
## Caveats
|
108
|
+
|
109
|
+
Some programs use a main/server process to optimize their use of system resources. When you launch them, it is not possible to correlate the pid of the seed process with the pid of the window, Sapristi uses an heuristic approach to detect window instantiated under this type of strategy.
|
34
110
|
|
35
111
|
## Contributing
|
36
112
|
|
@@ -39,3 +115,6 @@ Bug reports and pull requests are welcome on GitHub at https://github.com/sapris
|
|
39
115
|
## License
|
40
116
|
|
41
117
|
Please see [LICENSE](https://github.com/sapristi-tool/sapristi/blob/master/LICENSE.txt) for personal usage and [COMM-LICENSE](https://github.com/sapristi-tool/sapristi/blob/master/COMM-LICENSE.txt) for commercial usage.
|
118
|
+
|
119
|
+
## Credits
|
120
|
+
<span>Photo by <a href="https://unsplash.com/@danfreemanphoto?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Dan Freeman</a> on <a href="https://unsplash.com/?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Unsplash</a></span>
|
Binary file
|
data/bin/sapristi
CHANGED
@@ -3,16 +3,25 @@
|
|
3
3
|
module Sapristi
|
4
4
|
module Linux
|
5
5
|
class ProcessManager
|
6
|
-
def self.execute_and_detach(cmd)
|
6
|
+
def self.execute_and_detach(cmd, out, err)
|
7
|
+
write_log_headers(out, err, cmd)
|
7
8
|
process_pid = begin
|
8
|
-
Process.spawn(cmd)
|
9
|
+
Process.spawn(cmd, out: [out, 'a'], err: [err, 'a'], pgroup: Process.getpgrp)
|
9
10
|
rescue StandardError
|
10
11
|
raise Error, "Error executing process: #{$ERROR_INFO}"
|
11
12
|
end
|
12
|
-
::Sapristi.logger.info "Launch #{cmd.split[0]}, process=#{process_pid}"
|
13
|
+
::Sapristi.logger.info "Launch #{cmd.split[0]}, process=#{process_pid} pgroup=#{Process.getpgrp}"
|
13
14
|
Process.detach process_pid
|
14
15
|
end
|
15
16
|
|
17
|
+
def self.write_log_headers(out, err, header)
|
18
|
+
[out, err].each { |file_name| write_header(file_name, header) }
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.write_header(file_name, header)
|
22
|
+
File.open(file_name, 'a') { |file| file.write "\n\n#{header}\n" }
|
23
|
+
end
|
24
|
+
|
16
25
|
def self.kill(waiter)
|
17
26
|
Process.kill 'KILL', waiter.pid
|
18
27
|
# sleep 1 # XLIB error for op code
|
@@ -59,6 +59,11 @@ module Sapristi
|
|
59
59
|
check_expected_geometry window, requested
|
60
60
|
end
|
61
61
|
|
62
|
+
def to_workspace(window, workspace)
|
63
|
+
@display.action_window(window.id, :move_to_desktop, workspace)
|
64
|
+
sleep TIME_TO_APPLY_DIMENSIONS
|
65
|
+
end
|
66
|
+
|
62
67
|
private
|
63
68
|
|
64
69
|
EXTENDED_HINTS = %w[maximized_vert maximized_horz].freeze
|
@@ -20,9 +20,11 @@ module Sapristi
|
|
20
20
|
end
|
21
21
|
end
|
22
22
|
|
23
|
+
# This method smells of :reek:TooManyStatements
|
23
24
|
def self.populate_options(opts, args)
|
24
25
|
opts.banner = 'Usage: sapristi [options]'
|
25
26
|
opts.on('-v', '--verbose', 'Verbose mode') { |value| args.verbose = value }
|
27
|
+
opts.on('-g', '--group GROUP', 'Use named group definitions') { |value| args.group = value }
|
26
28
|
opts.on('--dry-run', 'Dry run') { |value| args.dry = value }
|
27
29
|
opts.on('-f', '--file FILE', 'Read configuration from FILE') { |file| args.file = file }
|
28
30
|
opts.on('-h', '--help', 'Prints this help') do
|
@@ -39,16 +39,16 @@ module Sapristi
|
|
39
39
|
def apply_percentage
|
40
40
|
validate_percentage_field
|
41
41
|
|
42
|
-
(monitor_absolute * percentage).to_i + offset
|
42
|
+
(monitor_absolute * percentage - 1).to_i + offset
|
43
43
|
end
|
44
44
|
|
45
45
|
def offset
|
46
46
|
work_area = monitor['work_area']
|
47
47
|
|
48
48
|
case key
|
49
|
-
when 'X
|
49
|
+
when 'X'
|
50
50
|
work_area[0]
|
51
|
-
when 'Y
|
51
|
+
when 'Y'
|
52
52
|
work_area[1]
|
53
53
|
else
|
54
54
|
0
|
@@ -66,7 +66,7 @@ module Sapristi
|
|
66
66
|
end
|
67
67
|
|
68
68
|
def validate_percentage_field
|
69
|
-
min_percentage = { '
|
69
|
+
min_percentage = { 'Height' => 0.05, 'Width' => 0.05 }.fetch(key, 0)
|
70
70
|
unless Definition::TRANSLATIONS.include? key
|
71
71
|
raise "#{key}=#{raw}, using percentage in invalid field, valid=#{Definition::TRANSLATIONS.keys.join(', ')}"
|
72
72
|
end
|
data/lib/sapristi/definition.rb
CHANGED
@@ -3,8 +3,8 @@
|
|
3
3
|
module Sapristi
|
4
4
|
class Definition
|
5
5
|
TRANSLATIONS = {
|
6
|
-
'
|
7
|
-
'X
|
6
|
+
'Width' => 'work_area_width', 'Height' => 'work_area_height',
|
7
|
+
'X' => 'work_area_width', 'Y' => 'work_area_height'
|
8
8
|
}.freeze
|
9
9
|
NUMERIC_FIELDS = (TRANSLATIONS.keys + %w[Workspace]).freeze
|
10
10
|
|
@@ -21,7 +21,8 @@ module Sapristi
|
|
21
21
|
HEADERS.map { |key| "#{key}: #{raw_definition[key]}" }.join(', ')
|
22
22
|
end
|
23
23
|
|
24
|
-
attr_reader :raw_definition, :monitor, :
|
24
|
+
attr_reader :raw_definition, :monitor, :x, :y, :height, :width,
|
25
|
+
:workspace, :command, :title, :group
|
25
26
|
|
26
27
|
def hash
|
27
28
|
state.hash
|
@@ -42,7 +43,7 @@ module Sapristi
|
|
42
43
|
private
|
43
44
|
|
44
45
|
def normalize_variables
|
45
|
-
%w[Title Command X
|
46
|
+
%w[Title Command X Y Width Height Group].each do |key|
|
46
47
|
name = key.downcase.gsub(/-/, '_')
|
47
48
|
value = AttributeNormalizer.new(key, @raw_definition[key], @monitor).normalize
|
48
49
|
instance_variable_set "@#{name}".to_sym, value
|
@@ -59,11 +60,11 @@ module Sapristi
|
|
59
60
|
end
|
60
61
|
|
61
62
|
def validate_geometry(definition)
|
62
|
-
geometry_field_nil = %w[
|
63
|
+
geometry_field_nil = %w[Width Height X Y].find { |key| definition[key].nil? }
|
63
64
|
raise Error, "No #{geometry_field_nil} specified" if geometry_field_nil
|
64
65
|
end
|
65
66
|
|
66
|
-
HEADERS = %w[Title Command Monitor Workspace X
|
67
|
+
HEADERS = %w[Title Command Monitor Workspace X Y Width Height Group].freeze
|
67
68
|
|
68
69
|
def validate_headers(definition)
|
69
70
|
headers = definition.keys
|
@@ -31,8 +31,8 @@ module Sapristi
|
|
31
31
|
end
|
32
32
|
|
33
33
|
def validate_monitor_dimensions(normalized, monitor_width, monitor_height)
|
34
|
-
x_pos = normalized.
|
35
|
-
y_pos = normalized.
|
34
|
+
x_pos = normalized.x
|
35
|
+
y_pos = normalized.y
|
36
36
|
unless (0...monitor_width).include? x_pos
|
37
37
|
raise Error, "x=#{x_pos} is outside of monitor width dimension=0..#{monitor_width - 1}"
|
38
38
|
end
|
@@ -42,10 +42,10 @@ module Sapristi
|
|
42
42
|
end
|
43
43
|
|
44
44
|
def validate_work_area(normalized, monitor_width, monitor_height)
|
45
|
-
x_pos = normalized.
|
46
|
-
y_pos = normalized.
|
47
|
-
x_end = x_pos + normalized.
|
48
|
-
y_end = y_pos + normalized.
|
45
|
+
x_pos = normalized.x
|
46
|
+
y_pos = normalized.y
|
47
|
+
x_end = x_pos + normalized.width
|
48
|
+
y_end = y_pos + normalized.height
|
49
49
|
if x_end >= monitor_width
|
50
50
|
raise Error, "window x dimensions: [#{x_pos}, #{x_end}] exceeds monitor width [0..#{monitor_width - 1}]"
|
51
51
|
end
|
@@ -57,8 +57,8 @@ module Sapristi
|
|
57
57
|
MIN_X_SIZE = 50
|
58
58
|
MIN_Y_SIZE = 50
|
59
59
|
def validate_window_min_size(normalized)
|
60
|
-
window_width = normalized.
|
61
|
-
window_height = normalized.
|
60
|
+
window_width = normalized.width
|
61
|
+
window_height = normalized.height
|
62
62
|
raise Error, "window x size=#{window_width} less than #{MIN_X_SIZE}" if window_width < MIN_X_SIZE
|
63
63
|
raise Error, "window y size=#{window_height} less than #{MIN_Y_SIZE}" if window_height < MIN_Y_SIZE
|
64
64
|
end
|
@@ -11,15 +11,17 @@ module Sapristi
|
|
11
11
|
window = get_window definition.title, definition.command
|
12
12
|
|
13
13
|
@window_manager.move_resize(window,
|
14
|
-
[definition.
|
15
|
-
definition.
|
14
|
+
[definition.x, definition.y,
|
15
|
+
definition.width, definition.height])
|
16
|
+
@window_manager.to_workspace(window, definition.workspace)
|
17
|
+
window
|
16
18
|
end
|
17
19
|
|
18
20
|
private
|
19
21
|
|
20
22
|
def get_window(title, command)
|
21
23
|
(title && find_one_by_title(title)) ||
|
22
|
-
(command && @process_manager.detect_window_for_process(command)) ||
|
24
|
+
(command && @process_manager.detect_window_for_process(command, title)) ||
|
23
25
|
raise(Error, "Couldn't produce a window for this definition")
|
24
26
|
end
|
25
27
|
|
@@ -5,12 +5,13 @@ module Sapristi
|
|
5
5
|
def initialize
|
6
6
|
@display = OSFactory.new.window_manager
|
7
7
|
@process_manager = OSFactory.new.process_manager
|
8
|
+
@log = ::Sapristi.logger
|
8
9
|
end
|
9
10
|
|
10
|
-
def detect_window_for_process(command, timeout_in_seconds = 30)
|
11
|
+
def detect_window_for_process(command, title, timeout_in_seconds = 30)
|
11
12
|
save_pids_and_windows
|
12
13
|
|
13
|
-
process_window = wait_for_window(command, timeout_in_seconds)
|
14
|
+
process_window = wait_for_window(command, title, timeout_in_seconds)
|
14
15
|
|
15
16
|
if process_window
|
16
17
|
::Sapristi.logger.info " Found window title=#{process_window.title} for process=#{process_window.pid}!"
|
@@ -21,18 +22,18 @@ module Sapristi
|
|
21
22
|
|
22
23
|
private
|
23
24
|
|
24
|
-
attr_reader :previous_windows_ids, :previous_pids, :process_manager
|
25
|
+
attr_reader :previous_windows_ids, :previous_pids, :process_manager, :log
|
25
26
|
|
26
27
|
def save_pids_and_windows
|
27
28
|
@previous_windows_ids = @display.windows.map { |window| window[:id] }
|
28
29
|
@previous_pids = process_manager.user_pids
|
29
30
|
end
|
30
31
|
|
31
|
-
def wait_for_window(command, timeout_in_seconds)
|
32
|
+
def wait_for_window(command, title, timeout_in_seconds)
|
32
33
|
program = command.split[0]
|
33
|
-
waiter = process_manager.execute_and_detach command
|
34
|
+
waiter = process_manager.execute_and_detach command, '/tmp/sapristi.stdout', '/tmp/sapristi.stderr'
|
34
35
|
|
35
|
-
window = discover_window(waiter, program, timeout_in_seconds)
|
36
|
+
window = discover_window(waiter, program, title, timeout_in_seconds)
|
36
37
|
return window if window
|
37
38
|
|
38
39
|
raise Error, 'Error executing process, is dead' unless waiter.alive?
|
@@ -40,25 +41,59 @@ module Sapristi
|
|
40
41
|
process_manager.kill waiter
|
41
42
|
end
|
42
43
|
|
43
|
-
|
44
|
+
# This method smells of :reek:DuplicateMethodCall
|
45
|
+
# This method smells of :reek:LongParameterList
|
46
|
+
def discover_window(waiter, program, title, timeout_in_seconds)
|
44
47
|
start_time = Time.now
|
48
|
+
|
45
49
|
while Time.now - start_time < timeout_in_seconds # && waiter.alive?
|
46
|
-
process_window = detect_new_windows.find
|
47
|
-
window_for_waiter?(waiter, window) || window_for_command?(waiter, window, program)
|
48
|
-
end
|
50
|
+
process_window = detect_new_windows.find { |window| window_for?(waiter, program, title, window) }
|
49
51
|
|
50
52
|
return process_window if process_window
|
51
53
|
|
52
|
-
sleep 0.
|
54
|
+
sleep 0.2
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
# This method smells of :reek:DuplicateMethodCall
|
59
|
+
# This method smells of :reek:FeatureEnvy
|
60
|
+
# This method smells of :reek:LongParameterList
|
61
|
+
# This method smells of :reek:TooManyStatements
|
62
|
+
def window_for?(waiter, program, title, window)
|
63
|
+
window_pgroup = Process.getpgid(window.pid)
|
64
|
+
log.debug "Found new window: pid=#{window.pid}, ppid=#{window_pgroup}, id=#{window.id}, title=#{window.title}"
|
65
|
+
if window_for_waiter?(waiter, window)
|
66
|
+
log.debug "Found window by pid=#{waiter.pid}, id=#{window.id}, title=#{window.title}"
|
67
|
+
true
|
68
|
+
elsif window_for_process_group?(window)
|
69
|
+
log.debug "Found window by process group=#{Process.getpgid(window.pid)}, id=#{window.id}, title=#{window.title}"
|
70
|
+
true
|
71
|
+
elsif window_for_command?(window, program)
|
72
|
+
log.debug "Found window by program=#{program}, id=#{window.id}, title=#{window.title}"
|
73
|
+
true
|
74
|
+
elsif window_for_title?(window, title)
|
75
|
+
log.debug "Found window by title=#{title}, id=#{window.id}, title=#{window.title}"
|
76
|
+
true
|
77
|
+
else
|
78
|
+
log.warn "We can not be sure window '#{window.title}' with pid=#{window.pid} is related to program=#{program}, pid=#{waiter.pid}, status=#{waiter.status || 'dead'}"
|
79
|
+
false
|
53
80
|
end
|
54
81
|
end
|
55
82
|
|
83
|
+
def window_for_title?(window, title)
|
84
|
+
/#{title}/i.match(window.title)
|
85
|
+
end
|
86
|
+
|
56
87
|
def window_for_waiter?(waiter, window)
|
57
|
-
|
88
|
+
window.pid.eql?(waiter.pid)
|
89
|
+
end
|
90
|
+
|
91
|
+
def window_for_process_group?(window)
|
92
|
+
Process.getpgid(window.pid).eql?(Process.getpgrp)
|
58
93
|
end
|
59
94
|
|
60
|
-
def window_for_command?(
|
61
|
-
|
95
|
+
def window_for_command?(window, program)
|
96
|
+
process_manager.cmd_for_pid(window.pid).start_with?(program)
|
62
97
|
end
|
63
98
|
|
64
99
|
def detect_new_windows
|
data/lib/sapristi/sapristi.rb
CHANGED
@@ -14,13 +14,12 @@ module Sapristi
|
|
14
14
|
check_user_configuration(conf_file)
|
15
15
|
|
16
16
|
definitions = @configuration_loader.load(conf_file)
|
17
|
+
definitions = definitions.filter { |definition| definition.group.eql? @group } if @group
|
17
18
|
|
18
19
|
process definitions
|
19
20
|
end
|
20
21
|
|
21
|
-
|
22
|
-
@verbose
|
23
|
-
end
|
22
|
+
attr_reader :dry, :verbose
|
24
23
|
|
25
24
|
def verbose!
|
26
25
|
@verbose = true
|
@@ -33,24 +32,28 @@ module Sapristi
|
|
33
32
|
logger.level = :info if logger.level > Logger::INFO
|
34
33
|
end
|
35
34
|
|
36
|
-
def
|
37
|
-
@
|
35
|
+
def filter!(group)
|
36
|
+
@group = group
|
38
37
|
end
|
39
38
|
|
40
39
|
private
|
41
40
|
|
42
41
|
def process(definitions)
|
43
42
|
definitions.each_with_index do |definition, index|
|
44
|
-
|
45
|
-
@definition_processor.process_definition(definition) unless @dry
|
43
|
+
process_line(definition, index)
|
46
44
|
rescue Error => e
|
47
45
|
raise Error, "#{e.message}, line=#{index}"
|
48
46
|
end
|
49
47
|
end
|
50
48
|
|
49
|
+
def process_line(definition, index)
|
50
|
+
::Sapristi.logger.info "Process line #{index}: #{definition}"
|
51
|
+
@definition_processor.process_definition(definition) unless dry
|
52
|
+
end
|
53
|
+
|
51
54
|
def check_user_configuration(conf_file)
|
52
55
|
return unless conf_file.eql?(Sapristi.user_default_configuration_file) && !File.exist?(conf_file)
|
53
|
-
|
56
|
+
|
54
57
|
@configuration_loader.create_empty_configuration conf_file
|
55
58
|
end
|
56
59
|
|
data/lib/sapristi/version.rb
CHANGED
@@ -10,7 +10,7 @@ module Sapristi
|
|
10
10
|
@display = OSFactory.new.window_manager
|
11
11
|
end
|
12
12
|
|
13
|
-
def_delegators :@display, :windows, :close, :workspaces, :move_resize, :resize, :move
|
13
|
+
def_delegators :@display, :windows, :close, :workspaces, :move_resize, :resize, :move, :to_workspace
|
14
14
|
|
15
15
|
def find_window(title_regex)
|
16
16
|
@display.windows title: title_regex
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: sapristi
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.31
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Sapristi dev
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2020-12-
|
11
|
+
date: 2020-12-25 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -270,10 +270,12 @@ executables:
|
|
270
270
|
extensions: []
|
271
271
|
extra_rdoc_files: []
|
272
272
|
files:
|
273
|
+
- ".github/workflows/ruby.yml"
|
273
274
|
- ".gitignore"
|
274
275
|
- ".reek.yml"
|
275
276
|
- ".rspec"
|
276
277
|
- ".rubocop.yml"
|
278
|
+
- ".ruby-version"
|
277
279
|
- ".travis.yml"
|
278
280
|
- COMM-LICENSE.txt
|
279
281
|
- Gemfile
|
@@ -281,6 +283,7 @@ files:
|
|
281
283
|
- LICENSE.txt
|
282
284
|
- README.md
|
283
285
|
- Rakefile
|
286
|
+
- assets/images/sapristi.jpg
|
284
287
|
- bin/console
|
285
288
|
- bin/sapristi
|
286
289
|
- bin/setup
|