lerna 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
+ SHA1:
3
+ metadata.gz: b7d1d9ed67341c040754c33479daf18d66bd4fc7
4
+ data.tar.gz: fc89547a2e6c6e55f437a82ad08bbdfa6589db80
5
+ SHA512:
6
+ metadata.gz: f8a4dd8509a4dd6771765971b1cfc2d6da6a877eb7f53f1a5641cb86631d0f4a06bf75570c8c796aa5c97a5c760457968b1ffe4f3c3900e78f040de6ca91e693
7
+ data.tar.gz: a54b275de9b370169e08117af82271f44b7c7f0227a589243dfa726b8604d490dadf6dce116c25cfbc20a3834240857c7b3eae276a6eb0e2f8cde19cdc7c28c4
@@ -0,0 +1,63 @@
1
+ # Lerna
2
+
3
+ > That creature, bred in the swamp of Lerna, used to go forth into the plain
4
+ > and ravage both the cattle and the country. Now the hydra had a huge body,
5
+ > with nine heads, eight mortal, but the middle one immortal.
6
+
7
+ [Bibliotheca](http://www.perseus.tufts.edu/hopper/text?doc=Perseus%3Atext%3A1999.01.0022%3Atext%3DLibrary%3Abook%3D2%3Achapter%3D5%3Asection%3D2)
8
+
9
+ ## What
10
+
11
+ Lerna is a tool to make itinerant computing easier.
12
+ It watches for changes to the connected displays and configures X.org to use
13
+ what it deems to be the best display.
14
+
15
+ If
16
+
17
+ * you use a Linux laptop,
18
+ * you use an external monitor, and
19
+ * you want to use a single display at a time
20
+
21
+ then Lerna might be useful to you.
22
+ If you want to use multiple displays, it won't be immediately useful, but it
23
+ might still be a good starting point.
24
+
25
+ ## How
26
+
27
+ ```sh
28
+ $ lerna
29
+ ```
30
+
31
+ You'll see output something like this:
32
+
33
+ [2014-08-02T04:10:06.592689Z #13059] Switching to DP2
34
+ [2014-08-02T04:11:17.008609Z #13059] DP2 => disconnected
35
+ [2014-08-02T04:11:17.008764Z #13059] Switching to LVDS1
36
+ [2014-08-02T04:11:21.521592Z #13059] DP2 => connected
37
+ [2014-08-02T04:11:21.521679Z #13059] Switching to DP2
38
+
39
+ To see more options, use:
40
+
41
+ ```sh
42
+ $ lerna --help
43
+ ```
44
+
45
+ An example Upstart script for Ubuntu (pre-15.04) is provided in the `support`
46
+ directory.
47
+ This assumes that `lerna` is in the path; if it's not, you'll need to adjust
48
+ this.
49
+
50
+ You can then use
51
+
52
+ ```sh
53
+ $ start lerna
54
+ ```
55
+
56
+ to start the job immediately; it should start and stop automatically with your
57
+ desktop session thereafter.
58
+
59
+ ## Wanted
60
+
61
+ * Example job for systemd
62
+ * More strategies
63
+ * Ability to read from a configuration file (and reload on change)
@@ -0,0 +1,16 @@
1
+ require "rspec/core/rake_task"
2
+
3
+ desc "Run the specs."
4
+ RSpec::Core::RakeTask.new do |t|
5
+ t.pattern = "spec/**/*_spec.rb"
6
+ t.verbose = false
7
+ end
8
+
9
+ task :default => [:spec]
10
+
11
+ if Gem.loaded_specs.key?('rubocop')
12
+ require 'rubocop/rake_task'
13
+ RuboCop::RakeTask.new
14
+
15
+ task(:default).prerequisites << task(:rubocop)
16
+ end
@@ -0,0 +1,60 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'optparse'
4
+ require 'lerna/runner'
5
+ require 'lerna/strategy'
6
+
7
+ LOGGER = lambda { |str|
8
+ time = Time.now.utc
9
+ timestamp = time.strftime('%Y-%m-%dT%H:%M:%S.') << '%06dZ' % time.usec
10
+ puts '[%s #%d] %s' % [timestamp, Process.pid, str]
11
+ }
12
+
13
+ options = {
14
+ strategies: %w[ dual-external external-digital-only internal-only ],
15
+ system: method(:system)
16
+ }
17
+
18
+ executable = File.basename(__FILE__)
19
+
20
+ parser = OptionParser.new { |opts|
21
+ opts.banner = "Usage: #{executable} [options]"
22
+ opts.on(
23
+ '-s', '--strategies', String,
24
+ 'Strategies in order of precedence, separated by commas',
25
+ "Default is #{options[:strategies].join(',')}",
26
+ "Available: #{Lerna::Strategy.registry.keys.join(' ')}"
27
+ ) do |str|
28
+ options[:strategies] = str.split(/,/)
29
+ end
30
+ opts.on(
31
+ '-d', '--dry-run',
32
+ 'Just log the actions that would be taken'
33
+ ) do
34
+ options[:system] = ->(*args) { LOGGER.call(args.join(' ')) }
35
+ end
36
+ opts.on(
37
+ "-h", "--help",
38
+ "Display this help message and exit"
39
+ ) do
40
+ puts opts
41
+ exit
42
+ end
43
+ }
44
+ parser.parse!
45
+
46
+ runner = Lerna::Runner.new(
47
+ logger: LOGGER,
48
+ strategies: options[:strategies],
49
+ system: options[:system]
50
+ )
51
+
52
+ trap('TERM') {
53
+ LOGGER.call('Exiting')
54
+ exit
55
+ }
56
+
57
+ loop do
58
+ runner.run
59
+ sleep 2
60
+ end
@@ -0,0 +1,46 @@
1
+ module Lerna
2
+ class Display
3
+ INTERNAL_TYPES = %w[ LVDS ]
4
+ DIGITAL_TYPES = %w[ LVDS DP HDMI DVI ]
5
+
6
+ def self.parse(line)
7
+ name, status, = line.split(/\s/)
8
+ new(name, status == 'connected')
9
+ end
10
+
11
+ def initialize(name, connected)
12
+ @name = name
13
+ @connected = connected
14
+ end
15
+
16
+ attr_reader :name
17
+
18
+ def connected?
19
+ @connected
20
+ end
21
+
22
+ def type
23
+ name.sub(/-?\d+$/, '')
24
+ end
25
+
26
+ def internal?
27
+ INTERNAL_TYPES.include?(type)
28
+ end
29
+
30
+ def external?
31
+ !internal?
32
+ end
33
+
34
+ def digital?
35
+ DIGITAL_TYPES.include?(type)
36
+ end
37
+
38
+ def analog?
39
+ !digital?
40
+ end
41
+
42
+ def ==(other)
43
+ [name, connected?] == [other.name, other.connected?]
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,11 @@
1
+ require 'lerna/display'
2
+
3
+ module Lerna
4
+ class DisplayEnumerator
5
+ def call(xrandr_output = `LC_ALL=C xrandr`)
6
+ xrandr_output.
7
+ scan(/^(?:[A-Z\-]+\d+) (?:dis)?connected/).
8
+ map { |line| Display.parse(line) }
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,56 @@
1
+ require 'lerna/strategy_selector'
2
+ require 'lerna/state'
3
+
4
+ module Lerna
5
+ class Runner
6
+ def initialize(logger:, strategies:, system:, state: State.new,
7
+ strategy_selector: StrategySelector.new)
8
+ @logger = logger
9
+ @strategies = strategies
10
+ @system = system
11
+ @state = state
12
+ @strategy_selector = strategy_selector
13
+ end
14
+
15
+ def run
16
+ state.scan!
17
+ return unless state.changed?
18
+
19
+ log state_summary
20
+ strategy = find_strategy
21
+ if strategy
22
+ log "Using #{strategy.class}"
23
+ apply_strategy strategy
24
+ else
25
+ log 'No applicable strategy found'
26
+ end
27
+ end
28
+
29
+ private
30
+
31
+ attr_reader :state, :strategy_selector, :strategies
32
+
33
+ def log(s)
34
+ @logger.call(s)
35
+ end
36
+
37
+ def state_summary
38
+ state.displays.
39
+ map { |d| "#{d.name}#{d.connected? ? '*' : ''}" }.
40
+ join(' ')
41
+ end
42
+
43
+ def find_strategy
44
+ strategy_selector.call(strategies, state.displays)
45
+ end
46
+
47
+ def apply_strategy(strategy)
48
+ system 'xrandr', *strategy.configuration
49
+ system 'xset dpms force on'
50
+ end
51
+
52
+ def system(*args)
53
+ @system.call(*args)
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,20 @@
1
+ require 'lerna/display_enumerator'
2
+
3
+ module Lerna
4
+ class State
5
+ def initialize(enumerator = DisplayEnumerator.new)
6
+ @enumerator = enumerator
7
+ end
8
+
9
+ attr_reader :displays
10
+
11
+ def scan!
12
+ @previous_displays = @displays
13
+ @displays = @enumerator.call
14
+ end
15
+
16
+ def changed?
17
+ @displays != @previous_displays
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,4 @@
1
+ Dir[File.expand_path('../strategies/*.rb', __FILE__)].each do |path|
2
+ name = File.basename(path, '.rb')
3
+ require "lerna/strategies/#{name}"
4
+ end
@@ -0,0 +1,32 @@
1
+ require 'lerna/strategy'
2
+
3
+ module Lerna
4
+ module Strategies
5
+ class DualExternal < Strategy
6
+ def applicable?
7
+ wanted_displays.length == 2
8
+ end
9
+
10
+ def configuration
11
+ [].tap { |conf|
12
+ disconnected = displays - wanted_displays
13
+ disconnected.each do |d|
14
+ conf << '--output' << d.name << '--off'
15
+ end
16
+ conf << '--output' << wanted_displays[0].name << '--auto'
17
+ conf << '--output' << wanted_displays[1].name << '--auto' <<
18
+ '--right-of' << wanted_displays[0].name
19
+ }
20
+ end
21
+
22
+ private
23
+
24
+ def wanted_displays
25
+ displays.
26
+ select(&:connected?).
27
+ select { |d| d.external? && d.digital? }.
28
+ sort_by(&:name)
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,27 @@
1
+ require 'lerna/strategy'
2
+
3
+ module Lerna
4
+ module Strategies
5
+ class ExternalDigitalOnly < Strategy
6
+ def applicable?
7
+ winner
8
+ end
9
+
10
+ def configuration
11
+ [].tap { |conf|
12
+ disconnected = displays - [winner]
13
+ disconnected.each do |d|
14
+ conf << '--output' << d.name << '--off'
15
+ end
16
+ conf << '--output' << winner.name << '--auto'
17
+ }
18
+ end
19
+
20
+ private
21
+
22
+ def winner
23
+ displays.select(&:connected?).find { |d| d.external? && d.digital? }
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,22 @@
1
+ require 'lerna/strategy'
2
+
3
+ module Lerna
4
+ module Strategies
5
+ class InternalOnly < Strategy
6
+ def applicable?
7
+ displays.select(&:connected?).all?(&:internal?)
8
+ end
9
+
10
+ def configuration
11
+ [].tap { |conf|
12
+ displays.reject(&:connected?).each do |d|
13
+ conf << '--output' << d.name << '--off'
14
+ end
15
+ displays.select(&:connected?).each do |d|
16
+ conf << '--output' << d.name << '--auto'
17
+ end
18
+ }
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,21 @@
1
+ module Lerna
2
+ class Strategy
3
+ def self.registry
4
+ @registry ||= {}
5
+ end
6
+
7
+ def self.inherited(subclass)
8
+ name = subclass.to_s.split(/::/).last
9
+ hyphenated = name.scan(/[A-Z][a-z_0-9]+/).map(&:downcase).join('-')
10
+ registry[hyphenated] = subclass
11
+ end
12
+
13
+ def initialize(displays)
14
+ @displays = displays
15
+ end
16
+
17
+ private
18
+
19
+ attr_reader :displays
20
+ end
21
+ end
@@ -0,0 +1,14 @@
1
+ require 'lerna/strategies'
2
+
3
+ module Lerna
4
+ class StrategySelector
5
+ def initialize(registry = Strategy.registry)
6
+ @registry = registry
7
+ end
8
+
9
+ def call(strategy_names, displays)
10
+ strategies = strategy_names.map { |s| @registry.fetch(s) }
11
+ strategies.map { |s| s.new(displays) }.find(&:applicable?)
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,3 @@
1
+ module Lerna
2
+ VERSION = '0.1.0'
3
+ end
@@ -0,0 +1,54 @@
1
+ require 'lerna/display_enumerator'
2
+
3
+ RSpec.describe Lerna::DisplayEnumerator do
4
+ subject {
5
+ described_class.new.call(xrandr_output)
6
+ }
7
+
8
+ context 'laptop with one external HDMI connected' do
9
+ let(:xrandr_output) {
10
+ <<END
11
+ Screen 0: minimum 320 x 200, current 1920 x 1200, maximum 32767 x 32767
12
+ LVDS1 connected (normal left inverted right x axis y axis)
13
+ 1366x768 60.0 +
14
+ 1360x768 59.8 60.0
15
+ 1024x768 60.0
16
+ 800x600 60.3 56.2
17
+ 640x480 59.9
18
+ VGA1 disconnected (normal left inverted right x axis y axis)
19
+ HDMI1 connected 1920x1200+0+0 (normal left inverted right x axis y axis) 518mm x 324mm
20
+ 1920x1200 60.0*+
21
+ 1600x1200 60.0
22
+ 1680x1050 59.9
23
+ 1280x1024 60.0
24
+ 1280x960 60.0
25
+ 1024x768 60.0
26
+ 800x600 60.3
27
+ 640x480 60.0
28
+ 720x400 70.1
29
+ DP1 disconnected (normal left inverted right x axis y axis)
30
+ HDMI2 disconnected (normal left inverted right x axis y axis)
31
+ HDMI3 disconnected (normal left inverted right x axis y axis)
32
+ DP2 disconnected (normal left inverted right x axis y axis)
33
+ DP3 disconnected (normal left inverted right x axis y axis)
34
+ VIRTUAL1 disconnected (normal left inverted right x axis y axis)
35
+ END
36
+ }
37
+
38
+ it 'reports nine displays' do
39
+ expect(subject.length).to eq(9)
40
+ end
41
+
42
+ it 'reports display names' do
43
+ expect(subject.map(&:name)).to eq(%w[
44
+ LVDS1 VGA1 HDMI1 DP1 HDMI2 HDMI3 DP2 DP3 VIRTUAL1
45
+ ])
46
+ end
47
+
48
+ it 'reports connection status' do
49
+ expect(subject.map(&:connected?)).to eq([
50
+ true, false, true, false, false, false, false, false, false
51
+ ])
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,117 @@
1
+ require 'lerna/display'
2
+
3
+ RSpec.describe Lerna::Display do
4
+ subject {
5
+ described_class.parse(xrandr_line)
6
+ }
7
+
8
+ context 'a connected LVDS display' do
9
+ let(:xrandr_line) {
10
+ 'LVDS1 connected (normal left inverted right x axis y axis)'
11
+ }
12
+
13
+ it { is_expected.to be_connected }
14
+ it { is_expected.to be_internal }
15
+ it { is_expected.to be_digital }
16
+
17
+ it 'has a name' do
18
+ expect(subject.name).to eq('LVDS1')
19
+ end
20
+
21
+ it 'has a type' do
22
+ expect(subject.type).to eq('LVDS')
23
+ end
24
+ end
25
+
26
+ context 'a connected HDMI display' do
27
+ let(:xrandr_line) {
28
+ 'HDMI1 connected 1920x1200+0+0 (normal left inverted right x axis y axis) 518mm x 324mm'
29
+ }
30
+
31
+ it { is_expected.to be_connected }
32
+ it { is_expected.not_to be_internal }
33
+ it { is_expected.to be_digital }
34
+
35
+ it 'has a name' do
36
+ expect(subject.name).to eq('HDMI1')
37
+ end
38
+
39
+ it 'has a type' do
40
+ expect(subject.type).to eq('HDMI')
41
+ end
42
+ end
43
+
44
+ context 'a disconnected DisplayPort display' do
45
+ let(:xrandr_line) {
46
+ 'DP1 disconnected (normal left inverted right x axis y axis)'
47
+ }
48
+
49
+ it { is_expected.not_to be_connected }
50
+ it { is_expected.not_to be_internal }
51
+ it { is_expected.to be_digital }
52
+
53
+ it 'has a name' do
54
+ expect(subject.name).to eq('DP1')
55
+ end
56
+
57
+ it 'has a type' do
58
+ expect(subject.type).to eq('DP')
59
+ end
60
+ end
61
+
62
+ context 'a disconnected VGA display' do
63
+ let(:xrandr_line) {
64
+ 'VGA1 disconnected (normal left inverted right x axis y axis)'
65
+ }
66
+
67
+ it { is_expected.not_to be_digital }
68
+ end
69
+
70
+ context 'a display with a hyphen in the type' do
71
+ let(:xrandr_line) {
72
+ 'S-video disconnected (normal left inverted right x axis y axis)'
73
+ }
74
+
75
+ it 'has a name' do
76
+ expect(subject.name).to eq('S-video')
77
+ end
78
+
79
+ it 'has a type' do
80
+ expect(subject.type).to eq('S-video')
81
+ end
82
+ end
83
+
84
+ context 'a display with a hyphen before the number' do
85
+ let(:xrandr_line) {
86
+ 'DVI-0 disconnected (normal left inverted right x axis y axis)'
87
+ }
88
+
89
+ it 'has a name' do
90
+ expect(subject.name).to eq('DVI-0')
91
+ end
92
+
93
+ it 'has a type' do
94
+ expect(subject.type).to eq('DVI')
95
+ end
96
+ end
97
+
98
+ context 'equality' do
99
+ it 'is true if displays have the same name and connectivity' do
100
+ a = described_class.new('HDMI1', true)
101
+ b = described_class.new('HDMI1', true)
102
+ expect(a).to eq(b)
103
+ end
104
+
105
+ it 'is false if displays have a different name' do
106
+ a = described_class.new('HDMI1', true)
107
+ b = described_class.new('VGA1', true)
108
+ expect(a).not_to eq(b)
109
+ end
110
+
111
+ it 'is false if displays have different connectivity' do
112
+ a = described_class.new('HDMI1', true)
113
+ b = described_class.new('HDMI1', false)
114
+ expect(a).not_to eq(b)
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,50 @@
1
+ require 'lerna/strategies/dual_external'
2
+
3
+ RSpec.describe Lerna::Strategies::DualExternal do
4
+ subject {
5
+ described_class.new(displays)
6
+ }
7
+
8
+ context 'when fewer than two external digital displays are connected' do
9
+ let(:displays) {
10
+ [
11
+ double(name: 'LVDS1', external?: false,
12
+ connected?: true, digital?: true),
13
+ double(name: 'DP1', external?: true,
14
+ connected?: true, digital?: true),
15
+ double(name: 'DP2', external?: true,
16
+ connected?: false, digital?: true),
17
+ double(name: 'VGA1', external?: true,
18
+ connected?: false, digital?: false)
19
+ ]
20
+ }
21
+
22
+ it { is_expected.not_to be_applicable }
23
+ end
24
+
25
+ context 'when two external digital displays are connected' do
26
+ let(:displays) {
27
+ [
28
+ double(name: 'LVDS1', external?: false,
29
+ connected?: true, digital?: true),
30
+ double(name: 'DP1', external?: true,
31
+ connected?: true, digital?: true),
32
+ double(name: 'DP2', external?: true,
33
+ connected?: true, digital?: true),
34
+ double(name: 'VGA1', external?: true,
35
+ connected?: false, digital?: false)
36
+ ]
37
+ }
38
+
39
+ it { is_expected.to be_applicable }
40
+
41
+ it 'configures the connected external display' do
42
+ expect(subject.configuration).to eq(%w[
43
+ --output LVDS1 --off
44
+ --output VGA1 --off
45
+ --output DP1 --auto
46
+ --output DP2 --auto --right-of DP1
47
+ ])
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,50 @@
1
+ require 'lerna/strategies/external_digital_only'
2
+
3
+ RSpec.describe Lerna::Strategies::ExternalDigitalOnly do
4
+ subject {
5
+ described_class.new(displays)
6
+ }
7
+
8
+ context 'when only one external digital display is connected' do
9
+ let(:displays) {
10
+ [
11
+ double(name: 'LVDS1', external?: false,
12
+ connected?: true, digital?: true),
13
+ double(name: 'DP1', external?: true,
14
+ connected?: true, digital?: true),
15
+ double(name: 'DP2', external?: true,
16
+ connected?: false, digital?: true),
17
+ double(name: 'VGA1', external?: true,
18
+ connected?: false, digital?: false)
19
+ ]
20
+ }
21
+
22
+ it { is_expected.to be_applicable }
23
+
24
+ it 'configures the connected external display' do
25
+ expect(subject.configuration).to eq(%w[
26
+ --output LVDS1 --off
27
+ --output DP2 --off
28
+ --output VGA1 --off
29
+ --output DP1 --auto
30
+ ])
31
+ end
32
+ end
33
+
34
+ context 'when no external digital display is connected' do
35
+ let(:displays) {
36
+ [
37
+ double(name: 'LVDS1', external?: false,
38
+ connected?: true, digital?: true),
39
+ double(name: 'DP1', external?: true,
40
+ connected?: false, digital?: true),
41
+ double(name: 'DP2', external?: true,
42
+ connected?: false, digital?: true),
43
+ double(name: 'VGA1', external?: true,
44
+ connected?: true, digital?: false)
45
+ ]
46
+ }
47
+
48
+ it { is_expected.not_to be_applicable }
49
+ end
50
+ end
@@ -0,0 +1,36 @@
1
+ require 'lerna/strategies/internal_only'
2
+
3
+ RSpec.describe Lerna::Strategies::InternalOnly do
4
+ subject {
5
+ described_class.new(displays)
6
+ }
7
+
8
+ context 'when only the internal display is connected' do
9
+ let(:displays) {
10
+ [
11
+ double(name: 'LVDS1', internal?: true, connected?: true),
12
+ double(name: 'DP1', internal?: false, connected?: false)
13
+ ]
14
+ }
15
+
16
+ it { is_expected.to be_applicable }
17
+
18
+ it 'configures the internal display' do
19
+ expect(subject.configuration).to eq(%w[
20
+ --output DP1 --off
21
+ --output LVDS1 --auto
22
+ ])
23
+ end
24
+ end
25
+
26
+ context 'when an internal and an external display are connected' do
27
+ let(:displays) {
28
+ [
29
+ double(name: 'LVDS1', internal?: true, connected?: true),
30
+ double(name: 'DP1', internal?: false, connected?: true)
31
+ ]
32
+ }
33
+
34
+ it { is_expected.not_to be_applicable }
35
+ end
36
+ end
@@ -0,0 +1,77 @@
1
+ require 'lerna/runner'
2
+
3
+ RSpec.describe Lerna::Runner do
4
+ subject {
5
+ described_class.new(
6
+ logger: logger,
7
+ system: system,
8
+ strategies: strategies,
9
+ state: state,
10
+ strategy_selector: strategy_selector
11
+ )
12
+ }
13
+ let(:logger) { double('logger', call: nil) }
14
+ let(:system) { double('system', call: nil) }
15
+ let(:strategies) { double('strategies') }
16
+ let(:strategy_selector) { double('strategy_selector', call: nil) }
17
+
18
+ after do
19
+ subject.run
20
+ end
21
+
22
+ context 'when the state has not changed' do
23
+ let(:state) { double('state', scan!: nil, changed?: false) }
24
+
25
+ it 'scans' do
26
+ expect(state).to receive(:scan!)
27
+ end
28
+
29
+ it 'does nothing' do
30
+ expect(system).not_to receive(:call)
31
+ end
32
+ end
33
+
34
+ context 'when the state has changed' do
35
+ let(:state) {
36
+ double('state', scan!: nil, changed?: true, displays: displays)
37
+ }
38
+ let(:displays) {
39
+ [double('display', name: 'ABC1', connected?: true)]
40
+ }
41
+
42
+ it 'scans' do
43
+ expect(state).to receive(:scan!)
44
+ end
45
+
46
+ it 'asks the strategy_selector for a strategy' do
47
+ expect(strategy_selector).to receive(:call).
48
+ with(strategies, displays).
49
+ and_return(nil)
50
+ end
51
+
52
+ context 'when a strategy is found' do
53
+ let(:strategy) {
54
+ double('strategy', configuration: %w[ --option --another ])
55
+ }
56
+ let(:strategy_selector) {
57
+ double('strategy_selector', call: strategy)
58
+ }
59
+
60
+ it 'calls xrandr with the strategy configuration' do
61
+ expect(system).to receive(:call).
62
+ with('xrandr', '--option', '--another')
63
+ end
64
+
65
+ it 'resets dpms' do
66
+ expect(system).to receive(:call).
67
+ with('xset dpms force on')
68
+ end
69
+ end
70
+
71
+ context 'when no strategy is found' do
72
+ it 'does nothing' do
73
+ expect(system).not_to receive(:call)
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,67 @@
1
+ require 'lerna/state'
2
+
3
+ RSpec.describe Lerna::State do
4
+ subject { described_class.new(enumerator) }
5
+
6
+ let(:enumerator) { -> { enumerations.shift } }
7
+
8
+ context 'after the first scan' do
9
+ let(:enumerations) {
10
+ [
11
+ [Lerna::Display.new('HDMI1', false)]
12
+ ]
13
+ }
14
+
15
+ before do
16
+ subject.scan!
17
+ end
18
+
19
+ it { is_expected.to be_changed }
20
+
21
+ it 'lists the current displays' do
22
+ expect(subject.displays).to eq([Lerna::Display.new('HDMI1', false)])
23
+ end
24
+ end
25
+
26
+ context 'when the connections have changed' do
27
+ let(:enumerations) {
28
+ [
29
+ [Lerna::Display.new('HDMI1', false)],
30
+ [Lerna::Display.new('HDMI1', true)]
31
+ ]
32
+ }
33
+
34
+ before do
35
+ 2.times do
36
+ subject.scan!
37
+ end
38
+ end
39
+
40
+ it { is_expected.to be_changed }
41
+
42
+ it 'lists the current displays' do
43
+ expect(subject.displays).to eq([Lerna::Display.new('HDMI1', true)])
44
+ end
45
+ end
46
+
47
+ context 'when the connections have not changed' do
48
+ let(:enumerations) {
49
+ [
50
+ [Lerna::Display.new('HDMI1', true)],
51
+ [Lerna::Display.new('HDMI1', true)]
52
+ ]
53
+ }
54
+
55
+ before do
56
+ 2.times do
57
+ subject.scan!
58
+ end
59
+ end
60
+
61
+ it { is_expected.not_to be_changed }
62
+
63
+ it 'lists the current displays' do
64
+ expect(subject.displays).to eq([Lerna::Display.new('HDMI1', true)])
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,30 @@
1
+ require 'lerna/strategy_selector'
2
+
3
+ RSpec.describe Lerna::StrategySelector do
4
+ subject {
5
+ described_class.new(
6
+ 'strategy_a' => strategy_a_class,
7
+ 'strategy_b' => strategy_b_class
8
+ )
9
+ }
10
+
11
+ let(:strategy_a_class) { double('StrategyA', new: strategy_a) }
12
+ let(:strategy_b_class) { double('StrategyB', new: strategy_b) }
13
+ let(:strategy_a) { double('strategy_a', applicable?: false) }
14
+ let(:strategy_b) { double('strategy_b', applicable?: true) }
15
+ let(:displays) { double('displays') }
16
+
17
+ it 'instantiates each class with the displays' do
18
+ expect(strategy_a_class).to receive(:new).with(displays).and_return(strategy_a)
19
+ expect(strategy_b_class).to receive(:new).with(displays).and_return(strategy_b)
20
+ subject.call(%w[ strategy_a strategy_b ], displays)
21
+ end
22
+
23
+ it 'returns the first applicable instance' do
24
+ expect(subject.call(%w[ strategy_a strategy_b ], displays)).to eq(strategy_b)
25
+ end
26
+
27
+ it 'returns nil if there is no applicable instance' do
28
+ expect(subject.call(%w[ strategy_a ], displays)).to be_nil
29
+ end
30
+ end
@@ -0,0 +1,9 @@
1
+ require 'lerna/strategy'
2
+ require 'lerna/strategies/external_digital_only'
3
+
4
+ RSpec.describe Lerna::Strategy do
5
+ it 'registers subclasses' do
6
+ expect(described_class.registry.fetch('external-digital-only')).
7
+ to eq(Lerna::Strategies::ExternalDigitalOnly)
8
+ end
9
+ end
@@ -0,0 +1,18 @@
1
+ # Run Lerna with Upstart on Ubuntu etc.
2
+ # Copy to ~/.config/upstart
3
+
4
+ description "Lerna multi-head automator"
5
+ author "Paul Battley <pbattley@gmail.com>"
6
+
7
+ # Start and stop with the desktop
8
+ start on desktop-start
9
+ stop on desktop-end
10
+
11
+ # Automatically restart process if crashed
12
+ respawn
13
+
14
+ # Logs go to ~/.cache/upstart/lerna.log by default
15
+ console log
16
+
17
+ # Start in foreground mode so it can be properly managed
18
+ exec lerna
metadata ADDED
@@ -0,0 +1,110 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: lerna
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Paul Battley
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-08-12 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rake
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rspec
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '3'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '3'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rubocop
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: 0.30.0
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: 0.30.0
55
+ description: Tame multi-head displays
56
+ email: pbattley@gmail.com
57
+ executables:
58
+ - lerna
59
+ extensions: []
60
+ extra_rdoc_files: []
61
+ files:
62
+ - README.md
63
+ - Rakefile
64
+ - bin/lerna
65
+ - lib/lerna/display.rb
66
+ - lib/lerna/display_enumerator.rb
67
+ - lib/lerna/runner.rb
68
+ - lib/lerna/state.rb
69
+ - lib/lerna/strategies.rb
70
+ - lib/lerna/strategies/dual_external.rb
71
+ - lib/lerna/strategies/external_digital_only.rb
72
+ - lib/lerna/strategies/internal_only.rb
73
+ - lib/lerna/strategy.rb
74
+ - lib/lerna/strategy_selector.rb
75
+ - lib/lerna/version.rb
76
+ - spec/display_enumerator_spec.rb
77
+ - spec/display_spec.rb
78
+ - spec/dual_external_strategy_spec.rb
79
+ - spec/external_digital_only_strategy_spec.rb
80
+ - spec/internal_only_strategy_spec.rb
81
+ - spec/runner_spec.rb
82
+ - spec/state_spec.rb
83
+ - spec/strategy_selector_spec.rb
84
+ - spec/strategy_spec.rb
85
+ - support/lerna.conf
86
+ homepage: http://github.com/threedaymonk/lerna
87
+ licenses:
88
+ - MIT
89
+ metadata: {}
90
+ post_install_message:
91
+ rdoc_options: []
92
+ require_paths:
93
+ - lib
94
+ required_ruby_version: !ruby/object:Gem::Requirement
95
+ requirements:
96
+ - - ">="
97
+ - !ruby/object:Gem::Version
98
+ version: 2.1.0
99
+ required_rubygems_version: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ requirements: []
105
+ rubyforge_project:
106
+ rubygems_version: 2.4.5
107
+ signing_key:
108
+ specification_version: 4
109
+ summary: Linux display manager
110
+ test_files: []