lerna 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/README.md +63 -0
- data/Rakefile +16 -0
- data/bin/lerna +60 -0
- data/lib/lerna/display.rb +46 -0
- data/lib/lerna/display_enumerator.rb +11 -0
- data/lib/lerna/runner.rb +56 -0
- data/lib/lerna/state.rb +20 -0
- data/lib/lerna/strategies.rb +4 -0
- data/lib/lerna/strategies/dual_external.rb +32 -0
- data/lib/lerna/strategies/external_digital_only.rb +27 -0
- data/lib/lerna/strategies/internal_only.rb +22 -0
- data/lib/lerna/strategy.rb +21 -0
- data/lib/lerna/strategy_selector.rb +14 -0
- data/lib/lerna/version.rb +3 -0
- data/spec/display_enumerator_spec.rb +54 -0
- data/spec/display_spec.rb +117 -0
- data/spec/dual_external_strategy_spec.rb +50 -0
- data/spec/external_digital_only_strategy_spec.rb +50 -0
- data/spec/internal_only_strategy_spec.rb +36 -0
- data/spec/runner_spec.rb +77 -0
- data/spec/state_spec.rb +67 -0
- data/spec/strategy_selector_spec.rb +30 -0
- data/spec/strategy_spec.rb +9 -0
- data/support/lerna.conf +18 -0
- metadata +110 -0
checksums.yaml
ADDED
@@ -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
|
data/README.md
ADDED
@@ -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)
|
data/Rakefile
ADDED
@@ -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
|
data/bin/lerna
ADDED
@@ -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
|
data/lib/lerna/runner.rb
ADDED
@@ -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
|
data/lib/lerna/state.rb
ADDED
@@ -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,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,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
|
data/spec/runner_spec.rb
ADDED
@@ -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
|
data/spec/state_spec.rb
ADDED
@@ -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
|
data/support/lerna.conf
ADDED
@@ -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: []
|