sapristi 0.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.
@@ -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
@@ -0,0 +1,12 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+
10
+ .rspec_status
11
+ *.gem
12
+ Gemfile.lock
@@ -0,0 +1,7 @@
1
+ detectors:
2
+ IrresponsibleModule:
3
+ enabled: false
4
+
5
+ UncommunicativeVariableName:
6
+ accept:
7
+ - e
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format Fuubar
2
+ --color
3
+ --require spec_helper
@@ -0,0 +1,11 @@
1
+ AllCops:
2
+ NewCops: enable
3
+
4
+ Style/Documentation:
5
+ Enabled: false
6
+
7
+ Metrics/BlockLength:
8
+ ExcludedMethods: ['describe', 'context']
9
+
10
+ Style/StderrPuts:
11
+ Enabled: false
@@ -0,0 +1,7 @@
1
+ ---
2
+ sudo: false
3
+ language: ruby
4
+ cache: bundler
5
+ rvm:
6
+ - 2.6.0
7
+ before_install: gem install bundler -v 1.17.2
@@ -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
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }
6
+
7
+ # Specify your gem's dependencies in sapristi.gemspec
8
+ gemspec
@@ -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
@@ -0,0 +1,2 @@
1
+ The open source license is designed for you to use Saprist to build open source and personal projects. The Sapristi open source license is AGPLv3.
2
+ https://www.gnu.org/licenses/agpl-3.0.txt
@@ -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.
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/gem_tasks'
4
+ require 'rspec/core/rake_task'
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ task default: :spec
@@ -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__)
@@ -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__
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -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