lerna 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
+ 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: []