sapristi 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +12 -0
- data/.reek.yml +7 -0
- data/.rspec +3 -0
- data/.rubocop.yml +11 -0
- data/.travis.yml +7 -0
- data/COMM-LICENSE.txt +1 -0
- data/Gemfile +8 -0
- data/Guardfile +28 -0
- data/LICENSE.txt +2 -0
- data/README.md +39 -0
- data/Rakefile +8 -0
- data/bin/console +15 -0
- data/bin/sapristi +49 -0
- data/bin/setup +8 -0
- data/lib/sapristi.rb +33 -0
- data/lib/sapristi/adapters/linux/monitor_manager.rb +73 -0
- data/lib/sapristi/adapters/linux/process_manager.rb +36 -0
- data/lib/sapristi/adapters/linux/window_manager.rb +52 -0
- data/lib/sapristi/arguments_parser.rb +34 -0
- data/lib/sapristi/attribute_normalizer.rb +77 -0
- data/lib/sapristi/configuration_loader.rb +66 -0
- data/lib/sapristi/definition.rb +77 -0
- data/lib/sapristi/definition_parser.rb +66 -0
- data/lib/sapristi/definition_processor.rb +36 -0
- data/lib/sapristi/monitor.rb +33 -0
- data/lib/sapristi/monitor_manager.rb +39 -0
- data/lib/sapristi/new_process_window_detector.rb +74 -0
- data/lib/sapristi/sapristi.rb +61 -0
- data/lib/sapristi/version.rb +5 -0
- data/lib/sapristi/window_manager.rb +83 -0
- data/sapristi.gemspec +60 -0
- metadata +315 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 4eb894c76aeaf49b67d080e819ff77c14023712a6194303b16a306477ef33c95
|
4
|
+
data.tar.gz: 58f367a04dabf628064f678bda0ebb0a1bf53193fac090838944e3823e7d737c
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: a9334da04bfff62237ead2e376d24f71795c4486fafe01a346753b6c343782b5d322e47f7ae6fb5abc69918ec6e5324dc977dd25368d63d81c1b163aabfbe1c0
|
7
|
+
data.tar.gz: c2f55b6b0b508e0543b12e45bdbdcc49d1a5759435881c3bf967b5d9964633573cd410163e9e8ff0a0dfc3d92005b9fd19884be0416139eedcef87c8f6485018
|
data/.gitignore
ADDED
data/.reek.yml
ADDED
data/.rspec
ADDED
data/.rubocop.yml
ADDED
data/.travis.yml
ADDED
data/COMM-LICENSE.txt
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
The commercial license is designed to for you to use Sapristi for commercial use or/and any kind of organizations.
|
data/Gemfile
ADDED
data/Guardfile
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
group :red_green_refactor, halt_on_fail: true do
|
4
|
+
guard :rspec, cmd: 'rspec' do
|
5
|
+
watch(%r{^spec/.+_spec\.rb$})
|
6
|
+
watch(%r{^lib/(?:sapristi/)?(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" }
|
7
|
+
watch('spec/spec_helper.rb') { 'spec' }
|
8
|
+
watch('bin/sapristi') { 'spec/sapristi_runner_spec.rb' }
|
9
|
+
end
|
10
|
+
|
11
|
+
guard :rubocop, all_on_start: false, cli: ['--format', 'html', '-o', './tmp/rubocop.html'] do
|
12
|
+
watch(%r{^spec/.+\.rb$})
|
13
|
+
watch(%r{^lib/.+\.rb$})
|
14
|
+
watch('bin/sapristi')
|
15
|
+
end
|
16
|
+
|
17
|
+
guard :rubycritic do
|
18
|
+
watch(%r{^spec/.+\.rb$})
|
19
|
+
watch(%r{^lib/.+\.rb$})
|
20
|
+
watch('bin/sapristi')
|
21
|
+
end
|
22
|
+
|
23
|
+
guard :reek, all_on_start: false, run_all: false, cli: ['--format', 'html', '>', './tmp/reek.html'] do
|
24
|
+
watch(%r{^spec/.+\.rb$})
|
25
|
+
watch(%r{^lib/.+\.rb$})
|
26
|
+
watch('bin/sapristi')
|
27
|
+
end
|
28
|
+
end
|
data/LICENSE.txt
ADDED
data/README.md
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
# Sapristi
|
2
|
+
|
3
|
+
Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/sapristi`. To experiment with that code, run `bin/console` for an interactive prompt.
|
4
|
+
|
5
|
+
TODO: Delete this and the text above, and describe your gem
|
6
|
+
|
7
|
+
## Installation
|
8
|
+
|
9
|
+
Add this line to your application's Gemfile:
|
10
|
+
|
11
|
+
```ruby
|
12
|
+
gem 'sapristi'
|
13
|
+
```
|
14
|
+
|
15
|
+
And then execute:
|
16
|
+
|
17
|
+
$ bundle
|
18
|
+
|
19
|
+
Or install it yourself as:
|
20
|
+
|
21
|
+
$ gem install sapristi
|
22
|
+
|
23
|
+
## Usage
|
24
|
+
|
25
|
+
TODO: Write usage instructions here
|
26
|
+
|
27
|
+
## Development
|
28
|
+
|
29
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
30
|
+
|
31
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
32
|
+
|
33
|
+
## Contributing
|
34
|
+
|
35
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/sapristi-tool/sapristi.
|
36
|
+
|
37
|
+
## License
|
38
|
+
|
39
|
+
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.
|
data/Rakefile
ADDED
data/bin/console
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require 'bundler/setup'
|
5
|
+
require 'sapristi'
|
6
|
+
|
7
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
8
|
+
# with your gem easier. You can also use a different console, if you like.
|
9
|
+
|
10
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
11
|
+
# require "pry"
|
12
|
+
# Pry.start
|
13
|
+
|
14
|
+
require 'irb'
|
15
|
+
IRB.start(__FILE__)
|
data/bin/sapristi
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require 'bundler/setup'
|
5
|
+
require 'sapristi'
|
6
|
+
|
7
|
+
module Sapristi
|
8
|
+
class Runner
|
9
|
+
def initialize
|
10
|
+
@sapristi = Sapristi.new
|
11
|
+
end
|
12
|
+
|
13
|
+
def run(args)
|
14
|
+
options = ArgumentsParser.new.parse args
|
15
|
+
build(options)
|
16
|
+
|
17
|
+
options.file ? @sapristi.run(options.file) : @sapristi.run
|
18
|
+
rescue Error => e
|
19
|
+
exit_error 1, e.message
|
20
|
+
rescue StandardError => e
|
21
|
+
error_file = save_stacktrace e
|
22
|
+
|
23
|
+
exit_error 2, "Sapristi crashed, see #{error_file}"
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def build(options)
|
29
|
+
@sapristi.verbose! if options.verbose
|
30
|
+
@sapristi.dry! if options.dry
|
31
|
+
end
|
32
|
+
|
33
|
+
def save_stacktrace(error)
|
34
|
+
file = File.join '/tmp', "sapristi.stacktrace#{Time.new.to_i}.log"
|
35
|
+
File.open(file, 'w') do |f|
|
36
|
+
f.write "#{error.class}: #{error.message}\n\n"
|
37
|
+
f.write error.backtrace.join("\n")
|
38
|
+
end
|
39
|
+
file
|
40
|
+
end
|
41
|
+
|
42
|
+
def exit_error(status, message)
|
43
|
+
$stderr.puts message
|
44
|
+
exit status
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
Sapristi::Runner.new.run(ARGV) if $PROGRAM_NAME == __FILE__
|
data/bin/setup
ADDED
data/lib/sapristi.rb
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'sapristi/version'
|
4
|
+
require 'sapristi/attribute_normalizer'
|
5
|
+
require 'sapristi/monitor_manager'
|
6
|
+
require 'sapristi/definition_parser'
|
7
|
+
require 'sapristi/configuration_loader'
|
8
|
+
require 'sapristi/definition'
|
9
|
+
require 'sapristi/window_manager'
|
10
|
+
require 'sapristi/definition_processor'
|
11
|
+
require 'sapristi/sapristi'
|
12
|
+
require 'sapristi/arguments_parser'
|
13
|
+
require 'sapristi/adapters/linux/monitor_manager'
|
14
|
+
require 'sapristi/adapters/linux/window_manager'
|
15
|
+
require 'sapristi/adapters/linux/process_manager'
|
16
|
+
require 'sapristi/new_process_window_detector'
|
17
|
+
require 'sapristi/monitor'
|
18
|
+
require 'logger'
|
19
|
+
|
20
|
+
module Sapristi
|
21
|
+
class Error < StandardError; end
|
22
|
+
|
23
|
+
def self.logger
|
24
|
+
@logger ||= Logger.new($stdout).tap do |log|
|
25
|
+
log.progname = name
|
26
|
+
log.level = Logger::WARN
|
27
|
+
|
28
|
+
log.formatter = proc do |_severity, _datetime, _progname, msg|
|
29
|
+
"#{msg}\n"
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Sapristi
|
4
|
+
module Linux
|
5
|
+
class MonitorManager
|
6
|
+
RESOLUTION = '(?<x>[0-9]+)/[0-9]+x(?<y>[0-9]+)/[0-9]+'
|
7
|
+
OFFSET = '(?<offset_x>[0-9]+)\\+(?<offset_y>[0-9]+)'
|
8
|
+
MONITOR_LINE_REGEX = /^\s*+(?<id>[0-9]+):\s*\+(?<main>\*)?(?<name>[^\s]+)\s+#{RESOLUTION}\+#{OFFSET}.*$/.freeze
|
9
|
+
|
10
|
+
def initialize
|
11
|
+
# https://ruby-gnome2.osdn.jp/hiki.cgi?Gdk%3A%3ADisplay
|
12
|
+
@screen = Gdk::Screen.default
|
13
|
+
end
|
14
|
+
|
15
|
+
def monitors
|
16
|
+
list_monitors.split("\n")[1..nil]
|
17
|
+
.map { |line| extract_monitor_info(line) }
|
18
|
+
.each_with_object({}) { |monitor, memo| memo[monitor['name']] = monitor }
|
19
|
+
rescue StandardError => e
|
20
|
+
raise Error, "Error fetching monitor information: #{e}"
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def list_monitors
|
26
|
+
`xrandr --listmonitors`
|
27
|
+
end
|
28
|
+
|
29
|
+
def extract_monitor_info(line)
|
30
|
+
monitor_info = MonitorManager.parse_line line
|
31
|
+
Monitor.new monitor_info.merge work_area(monitor_info)
|
32
|
+
end
|
33
|
+
|
34
|
+
public
|
35
|
+
|
36
|
+
def self.parse_line(line)
|
37
|
+
matcher = line.match(MONITOR_LINE_REGEX)
|
38
|
+
matcher.names.each_with_object({}) do |name, memo|
|
39
|
+
value = matcher[name]
|
40
|
+
memo[name] = value&.match(/^[0-9]+$/) ? value.to_i : value
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def work_area(monitor_info)
|
45
|
+
area = monitors_work_area[monitor_info['id']]
|
46
|
+
{
|
47
|
+
'work_area' => MonitorManager.dimensions(area, monitor_info),
|
48
|
+
'work_area_width' => area.width,
|
49
|
+
'work_area_height' => area.height
|
50
|
+
}
|
51
|
+
end
|
52
|
+
|
53
|
+
def self.dimensions(work_area, monitor_info)
|
54
|
+
offset_x = monitor_info['offset_x']
|
55
|
+
offset_y = monitor_info['offset_y']
|
56
|
+
x_start = work_area.x
|
57
|
+
y_start = work_area.y
|
58
|
+
[
|
59
|
+
x_start - offset_x,
|
60
|
+
y_start - offset_y,
|
61
|
+
x_start + work_area.width - offset_x,
|
62
|
+
y_start + work_area.height - offset_y
|
63
|
+
]
|
64
|
+
end
|
65
|
+
|
66
|
+
def monitors_work_area
|
67
|
+
@screen.n_monitors.times.each_with_object({}) do |monitor_id, memo|
|
68
|
+
memo[monitor_id] = @screen.get_monitor_workarea(monitor_id)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Sapristi
|
4
|
+
module Linux
|
5
|
+
class ProcessManager
|
6
|
+
def execute_and_detach(cmd)
|
7
|
+
process_pid = begin
|
8
|
+
Process.spawn(cmd)
|
9
|
+
rescue StandardError
|
10
|
+
raise Error, "Error executing process: #{$ERROR_INFO}"
|
11
|
+
end
|
12
|
+
::Sapristi.logger.info "Launch #{cmd.split[0]}, process=#{process_pid}"
|
13
|
+
Process.detach process_pid
|
14
|
+
end
|
15
|
+
|
16
|
+
def kill(waiter)
|
17
|
+
Process.kill 'KILL', waiter.pid
|
18
|
+
# sleep 1 # XLIB error for op code
|
19
|
+
raise Error, 'Error executing process, it didn\'t open a window'
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.user_pids
|
23
|
+
user_id = `id -u`.strip
|
24
|
+
`ps -u #{user_id}`.split("\n")[1..nil].map(&:to_i)
|
25
|
+
end
|
26
|
+
|
27
|
+
def cmd_for_pid(pid)
|
28
|
+
cmd = "ps -o cmd -p #{pid}"
|
29
|
+
line = `#{cmd}`.split("\n")[1]
|
30
|
+
raise Error, "No process found pid=#{pid}" unless line
|
31
|
+
|
32
|
+
line
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'wmctrl'
|
4
|
+
|
5
|
+
module Sapristi
|
6
|
+
module Linux
|
7
|
+
class WindowManager
|
8
|
+
def initialize(display = WMCtrl.display)
|
9
|
+
@display = display
|
10
|
+
end
|
11
|
+
|
12
|
+
attr_reader :display
|
13
|
+
|
14
|
+
def close(window)
|
15
|
+
@display.action_window(window.id, :close)
|
16
|
+
|
17
|
+
#
|
18
|
+
# sleep to allow a Graceful Dead to the window process
|
19
|
+
#
|
20
|
+
# X Error of failed request: BadWindow (invalid Window parameter)
|
21
|
+
# Major opcode of failed request: 20 (X_GetProperty)
|
22
|
+
# Resource id in failed request: 0x2200008
|
23
|
+
# Serial number of failed request: 1095
|
24
|
+
# Current serial number in output stream: 1095
|
25
|
+
sleep TIME_TO_APPLY_DIMENSIONS
|
26
|
+
end
|
27
|
+
|
28
|
+
def windows(args = {})
|
29
|
+
@display.windows args
|
30
|
+
end
|
31
|
+
|
32
|
+
def workspaces
|
33
|
+
@display.desktops
|
34
|
+
end
|
35
|
+
|
36
|
+
GRAVITY = 0
|
37
|
+
TIME_TO_APPLY_DIMENSIONS = 0.25
|
38
|
+
|
39
|
+
def move_resize(window, geometry)
|
40
|
+
remove_extended_hints(window) if window.maximized_horizontally? || window.maximized_vertically?
|
41
|
+
@display.action_window(window.id, :move_resize, GRAVITY, *geometry)
|
42
|
+
sleep TIME_TO_APPLY_DIMENSIONS
|
43
|
+
end
|
44
|
+
|
45
|
+
EXTENDED_HINTS = %w[maximized_vert maximized_horz].freeze
|
46
|
+
|
47
|
+
def remove_extended_hints(window)
|
48
|
+
display.action_window(window.id, :change_state, 'remove', *EXTENDED_HINTS)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'optparse'
|
4
|
+
require 'ostruct'
|
5
|
+
|
6
|
+
module Sapristi
|
7
|
+
class ArgumentsParser
|
8
|
+
def initialize
|
9
|
+
@args = OpenStruct.new
|
10
|
+
end
|
11
|
+
|
12
|
+
def parse(options)
|
13
|
+
ArgumentsParser.build_parser(@args).parse!(options)
|
14
|
+
@args
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.build_parser(args)
|
18
|
+
OptionParser.new do |opts|
|
19
|
+
ArgumentsParser.populate_options(opts, args)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.populate_options(opts, args)
|
24
|
+
opts.banner = 'Usage: sapristi [options]'
|
25
|
+
opts.on('-v', '--verbose', 'Verbose mode') { |value| args.verbose = value }
|
26
|
+
opts.on('--dry-run', 'Dry run') { |value| args.dry = value }
|
27
|
+
opts.on('-f', '--file FILE', 'Read configuration from FILE') { |file| args.file = file }
|
28
|
+
opts.on('-h', '--help', 'Prints this help') do
|
29
|
+
puts opts
|
30
|
+
exit
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|