sapristi 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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