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.
- 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: []
|