dumon 0.1.7 → 0.2.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.
- data/README.md +10 -2
- data/bin/dumon +1 -1
- data/lib/dumon/omanager.rb +120 -45
- data/lib/dumon/ui.rb +140 -17
- data/lib/dumon/version.rb +1 -0
- data/lib/dumon.rb +122 -23
- data/lib/rrutils/confdb.rb +45 -0
- data/lib/rrutils/options.rb +91 -0
- data/lib/rrutils.rb +9 -0
- data/test/test_rrutils_options.rb +51 -0
- metadata +8 -3
data/README.md
CHANGED
|
@@ -25,7 +25,7 @@ Dual monitor manager for Linux with GTK2 based user interface represented by sys
|
|
|
25
25
|
* gem published on http://rubygems.org/gems/dumon
|
|
26
26
|
|
|
27
27
|
### START
|
|
28
|
-
> ruby -r dumon -e 'Dumon::run'
|
|
28
|
+
> ruby -r dumon -e 'Dumon::App.instance.run'
|
|
29
29
|
|
|
30
30
|
* or add GEM PATH (see 'gem environment') into your PATH and then
|
|
31
31
|
|
|
@@ -33,10 +33,18 @@ Dual monitor manager for Linux with GTK2 based user interface represented by sys
|
|
|
33
33
|
|
|
34
34
|
* or as daemon process
|
|
35
35
|
|
|
36
|
-
> ruby -r dumon -e 'Dumon::run true'
|
|
36
|
+
> ruby -r dumon -e 'Dumon::App.instance.run true'
|
|
37
37
|
|
|
38
38
|
> dumon --daemon
|
|
39
39
|
|
|
40
|
+
* start with given profile
|
|
41
|
+
|
|
42
|
+
> ruby -r dumon -e 'Dumon::App.instance.run' -s 'profile:Profile name'
|
|
43
|
+
|
|
44
|
+
* or
|
|
45
|
+
|
|
46
|
+
> dumon 'profile:Profile name'
|
|
47
|
+
|
|
40
48
|
### UPGRADE NOTICES
|
|
41
49
|
|
|
42
50
|
* see lib/dumon/version.rb
|
data/bin/dumon
CHANGED
data/lib/dumon/omanager.rb
CHANGED
|
@@ -4,73 +4,136 @@ module Dumon
|
|
|
4
4
|
# This class represents a base class defining how concrete sub-classes manage
|
|
5
5
|
# output devices available on your system.
|
|
6
6
|
class OutDeviceManager
|
|
7
|
+
include Rrutils::Options
|
|
7
8
|
|
|
8
9
|
###
|
|
9
10
|
# System tool to be used for output devices management.
|
|
10
|
-
|
|
11
|
+
attr_reader :stool
|
|
11
12
|
|
|
12
13
|
###
|
|
13
14
|
# Cached information about current output devices.
|
|
15
|
+
# Value will be updated by each invocation of #read.
|
|
16
|
+
#
|
|
14
17
|
# Format: {output_name=>{:default=>"AxB",:current=>"CxD",:resolutions=>[...], ...}
|
|
15
18
|
# Sample: {
|
|
16
19
|
# "LVDS1"=>{:resolutions=>["1600x900", "1024x768"], :default=>"1600x900"},
|
|
17
20
|
# "VGA1" =>{:resolutions=>["1920x1080", "720x400"], :default=>"1920x1080", :current=>"1920x1080"}
|
|
18
21
|
# }
|
|
19
|
-
|
|
22
|
+
attr_reader :outputs
|
|
20
23
|
|
|
21
24
|
###
|
|
22
|
-
#
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
25
|
+
# Switches output according to given mode and corresponding parameters.
|
|
26
|
+
#
|
|
27
|
+
# Possible options:
|
|
28
|
+
#
|
|
29
|
+
# Single output:
|
|
30
|
+
# {:mode=>:single, :out=>'VGA1', :resolution=>'1600x900'}
|
|
31
|
+
# Mirrored outputs:
|
|
32
|
+
# {:mode=>:mirror, :resolution=>'1600x900'}
|
|
33
|
+
# Sequence of outputs:
|
|
34
|
+
# {:mode=>:sequence, :outs=>['VGA1', 'LVDS1'], :resolutions=>['1920x1080', '1600x900'], :primary=>:none}
|
|
35
|
+
def switch(options)
|
|
36
|
+
# pre-conditions
|
|
37
|
+
verify_options(options, {
|
|
38
|
+
:mode => [:single, :mirror, :sequence],
|
|
39
|
+
:out => :optional, :outs => :optional,
|
|
40
|
+
:resolution => :optional, :resolutions => :optional,
|
|
41
|
+
:primary => :optional
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
mode = options[:mode].to_sym
|
|
45
|
+
|
|
46
|
+
case mode
|
|
47
|
+
when :single
|
|
48
|
+
verify_options(options, {:mode => [:single], :out => outputs.keys, :resolution => :optional})
|
|
49
|
+
out_name = options[:out]
|
|
50
|
+
# given resolution exists for given output
|
|
51
|
+
unless options[:resolution].nil?
|
|
52
|
+
assert(outputs[out_name][:resolutions].include?(options[:resolution]),
|
|
53
|
+
"unknown resolution: #{options[:resolution]}, output: #{out_name}")
|
|
54
|
+
end
|
|
55
|
+
single(out_name, options[:resolution])
|
|
56
|
+
when :mirror
|
|
57
|
+
verify_options(options, {:mode => [:mirror], :resolution => :mandatory})
|
|
58
|
+
# given resolution exist for all outputs
|
|
59
|
+
outputs.each do |k,v|
|
|
60
|
+
assert(v[:resolutions].include?(options[:resolution]), "unknown resolution: #{options[:resolution]}, output: #{k}")
|
|
61
|
+
end
|
|
62
|
+
mirror(options[:resolution])
|
|
63
|
+
when :sequence
|
|
64
|
+
verify_options(options, {:mode => [:sequence], :outs => :mandatory, :resolutions => :mandatory, :primary => :optional})
|
|
65
|
+
assert(options[:outs].is_a?(Array), 'parameter :outs has to be Array')
|
|
66
|
+
assert(options[:resolutions].is_a?(Array), 'parameter :resolutions has to be Array')
|
|
67
|
+
assert(options[:outs].size == options[:resolutions].size, 'size of :outs and :resolutions does not match')
|
|
68
|
+
assert(options[:outs].size > 1, 'sequence mode expects at least 2 outputs')
|
|
69
|
+
assert(outputs.keys.include?(options[:primary]), "unknown primary output: #{options[:primary]}") unless options[:primary].nil?
|
|
70
|
+
sequence(options[:outs], options[:resolutions], options[:primary])
|
|
71
|
+
end
|
|
26
72
|
|
|
27
|
-
|
|
28
|
-
# Switch to given single output device with given resolution.
|
|
29
|
-
def single(output, resolution, type=nil)
|
|
30
|
-
raise NotImplementedError, 'this should be overridden by concrete sub-class'
|
|
73
|
+
Dumon::App.instance.current_profile = options
|
|
31
74
|
end
|
|
32
75
|
|
|
33
76
|
###
|
|
34
|
-
#
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
###
|
|
40
|
-
# Distributes output to given devices with given order and resolution.
|
|
41
|
-
# *param* outputs in form [["LVDS1", "1600x900"], [VGA1", "1920x1080"]]
|
|
42
|
-
# *param* primary name of primary output
|
|
43
|
-
def sequence(outputs, primary)
|
|
77
|
+
# Reads info about current accessible output devices and their settings.
|
|
78
|
+
# Readed infos will be stored and accessible via reader 'outputs'.
|
|
79
|
+
def read
|
|
44
80
|
raise NotImplementedError, 'this should be overridden by concrete sub-class'
|
|
45
81
|
end
|
|
46
82
|
|
|
47
83
|
###
|
|
48
84
|
# Gets default resolution of given output device.
|
|
49
85
|
def default_resolution(output)
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
86
|
+
assert(!outputs.nil?, 'no outputs found')
|
|
87
|
+
assert(outputs.keys.include?(output), "unknown output: #{output}")
|
|
88
|
+
assert(outputs[output].keys.include?(:default), "no default resolution, output: #{output}")
|
|
53
89
|
|
|
54
|
-
|
|
90
|
+
outputs[output][:default]
|
|
55
91
|
end
|
|
56
92
|
|
|
57
93
|
###
|
|
58
94
|
# Gets list of common resolutions of all output devices.
|
|
59
95
|
def common_resolutions
|
|
60
|
-
|
|
96
|
+
assert(!outputs.nil?, 'no outputs found')
|
|
61
97
|
|
|
62
98
|
rslt = []
|
|
63
|
-
o1 =
|
|
64
|
-
|
|
65
|
-
|
|
99
|
+
o1 = outputs.keys.first
|
|
100
|
+
outputs[o1][:resolutions].each do |res|
|
|
101
|
+
outputs.keys.each do |o|
|
|
66
102
|
next if o === o1
|
|
67
|
-
rslt << res if
|
|
103
|
+
rslt << res if outputs[o][:resolutions].include?(res)
|
|
68
104
|
end
|
|
69
105
|
end
|
|
70
106
|
|
|
71
107
|
rslt
|
|
72
108
|
end
|
|
73
109
|
|
|
110
|
+
|
|
111
|
+
protected
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
###
|
|
115
|
+
# Switch to given single output device with given resolution.
|
|
116
|
+
# *param* output
|
|
117
|
+
# *resolution* nil for default resolution
|
|
118
|
+
def single(output, resolution=nil)
|
|
119
|
+
raise NotImplementedError, 'this should be overridden by concrete sub-class'
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
###
|
|
123
|
+
# Mirrors output on all devices with given resolution.
|
|
124
|
+
def mirror(resolution)
|
|
125
|
+
raise NotImplementedError, 'this should be overridden by concrete sub-class'
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
###
|
|
129
|
+
# Distributes output to given devices with given order and resolution.
|
|
130
|
+
# *param* outs in form ['VGA1', 'LVDS1']
|
|
131
|
+
# *resolutions* in form ['1920x1080', '1600x900']
|
|
132
|
+
# *param* primary name of primary output
|
|
133
|
+
def sequence(outs, resolutions, primary=:none)
|
|
134
|
+
raise NotImplementedError, 'this should be overridden by concrete sub-class'
|
|
135
|
+
end
|
|
136
|
+
|
|
74
137
|
end
|
|
75
138
|
|
|
76
139
|
|
|
@@ -82,11 +145,11 @@ module Dumon
|
|
|
82
145
|
# Constructor.
|
|
83
146
|
# Checks whether the 'xrandr' system tool is there.
|
|
84
147
|
def initialize
|
|
85
|
-
paths = ['
|
|
148
|
+
paths = ['/usr/bin/xrandr', 'xrandr']
|
|
86
149
|
paths.each do |path|
|
|
87
150
|
begin
|
|
88
151
|
`#{path}`
|
|
89
|
-
|
|
152
|
+
@stool = path
|
|
90
153
|
Dumon.logger.info "System tool found: #{path}"
|
|
91
154
|
break
|
|
92
155
|
rescue => e
|
|
@@ -96,11 +159,14 @@ module Dumon
|
|
|
96
159
|
|
|
97
160
|
raise "no system tool found, checked for #{paths}" if self.stool.nil?
|
|
98
161
|
|
|
162
|
+
# just to check if it works
|
|
99
163
|
self.read
|
|
100
164
|
end
|
|
101
165
|
|
|
102
166
|
def read #:nodoc:
|
|
167
|
+
@outputs = nil # clear previous info
|
|
103
168
|
rslt = {}
|
|
169
|
+
|
|
104
170
|
output = nil
|
|
105
171
|
xrandr_out = `#{self.stool} -q`
|
|
106
172
|
xrandr_out.each_line do |line|
|
|
@@ -115,15 +181,27 @@ module Dumon
|
|
|
115
181
|
rslt[output][:resolutions] << resolution
|
|
116
182
|
end
|
|
117
183
|
end
|
|
118
|
-
|
|
119
184
|
Dumon::logger.debug "Outputs found: #{rslt}"
|
|
120
|
-
|
|
185
|
+
|
|
186
|
+
# verify structure of readed infos
|
|
187
|
+
assert(!rslt.empty?, 'no outputs found')
|
|
188
|
+
rslt.keys.each do |k|
|
|
189
|
+
out_meta = rslt[k]
|
|
190
|
+
verify_options(out_meta, {:resolutions=>:mandatory, :default=>:mandatory, :current=>:optional})
|
|
191
|
+
assert(out_meta[:resolutions].size > 1, "no resolution found, output=#{k}")
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
@outputs = rslt
|
|
121
195
|
rslt
|
|
122
196
|
end
|
|
123
197
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
198
|
+
|
|
199
|
+
protected
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def single(output, resolution=nil) #:nodoc:
|
|
203
|
+
assert(!outputs.nil?, 'no outputs found')
|
|
204
|
+
assert(outputs.keys.include?(output), "unknown output: #{output}")
|
|
127
205
|
|
|
128
206
|
resolution = self.default_resolution(output) if resolution.nil?
|
|
129
207
|
|
|
@@ -137,7 +215,7 @@ module Dumon
|
|
|
137
215
|
end
|
|
138
216
|
|
|
139
217
|
def mirror(resolution) #:nodoc:
|
|
140
|
-
|
|
218
|
+
assert(!outputs.nil?, 'no outputs found')
|
|
141
219
|
|
|
142
220
|
cmd = "#{self.stool}"
|
|
143
221
|
self.outputs.keys.each { |o| cmd << " --output #{o} --mode #{resolution}" }
|
|
@@ -146,18 +224,15 @@ module Dumon
|
|
|
146
224
|
`#{cmd}`
|
|
147
225
|
end
|
|
148
226
|
|
|
149
|
-
def sequence(
|
|
150
|
-
raise 'not an array' unless outputs.kind_of?(Array)
|
|
151
|
-
outputs.each { |pair| raise 'item not a pair' if !pair.kind_of?(Array) and pair.size != 2 }
|
|
152
|
-
|
|
227
|
+
def sequence(outs, resolutions, primary=:none) #:nodoc:
|
|
153
228
|
cmd = "#{self.stool}"
|
|
154
|
-
for i in 0..
|
|
155
|
-
output =
|
|
156
|
-
resolution =
|
|
229
|
+
for i in 0..outs.size - 1
|
|
230
|
+
output = outs[i]
|
|
231
|
+
resolution = resolutions[i]
|
|
157
232
|
resolution = self.default_resolution(output) if resolution.nil?
|
|
158
233
|
cmd << " --output #{output} --mode #{resolution}"
|
|
159
234
|
cmd << ' --primary' if primary.to_s == output
|
|
160
|
-
cmd << " --right-of #{
|
|
235
|
+
cmd << " --right-of #{outs[i - 1]}" if i > 0
|
|
161
236
|
end
|
|
162
237
|
|
|
163
238
|
Dumon::logger.debug "Command: #{cmd}"
|
data/lib/dumon/ui.rb
CHANGED
|
@@ -6,7 +6,27 @@ module Dumon
|
|
|
6
6
|
|
|
7
7
|
###
|
|
8
8
|
# Output manager used to manipulate the output.
|
|
9
|
-
|
|
9
|
+
attr_reader :omanager
|
|
10
|
+
|
|
11
|
+
###
|
|
12
|
+
# Constructor.
|
|
13
|
+
def initialize
|
|
14
|
+
@omanager = new_omanager
|
|
15
|
+
Dumon::logger.debug "Used output manager: #{omanager.class.name}"
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
###
|
|
19
|
+
# Factory method to create a new object of output manager.<p/>
|
|
20
|
+
# Can be used as Dependency Injection (DI) entry point:
|
|
21
|
+
# you can reopen Dumon:Ui and redefine 'new_omanager' if you implement a new output manager.
|
|
22
|
+
# <pre>
|
|
23
|
+
# class Dumon::Ui
|
|
24
|
+
# def new_omanager; Dumon::XyManager.new; end
|
|
25
|
+
# end
|
|
26
|
+
# </pre>
|
|
27
|
+
def new_omanager(with=Dumon::XrandrManager)
|
|
28
|
+
with.new
|
|
29
|
+
end
|
|
10
30
|
|
|
11
31
|
###
|
|
12
32
|
# Renders the UI.
|
|
@@ -18,7 +38,7 @@ module Dumon
|
|
|
18
38
|
###
|
|
19
39
|
# Quits the application.
|
|
20
40
|
def quit
|
|
21
|
-
|
|
41
|
+
raise NotImplementedError, 'this should be overridden by concrete sub-class'
|
|
22
42
|
end
|
|
23
43
|
|
|
24
44
|
###
|
|
@@ -47,7 +67,6 @@ module Dumon
|
|
|
47
67
|
end
|
|
48
68
|
|
|
49
69
|
def quit #:nodoc:
|
|
50
|
-
super
|
|
51
70
|
Gtk.main_quit
|
|
52
71
|
end
|
|
53
72
|
|
|
@@ -68,7 +87,7 @@ module Dumon
|
|
|
68
87
|
|
|
69
88
|
###
|
|
70
89
|
# This class represents a user interface represented by system tray icon and its context menu.
|
|
71
|
-
class
|
|
90
|
+
class GtkTrayUi < GtkUi
|
|
72
91
|
|
|
73
92
|
def initialize #:nodoc:
|
|
74
93
|
super
|
|
@@ -77,8 +96,8 @@ module Dumon
|
|
|
77
96
|
# {"LVDS1" => "1600x900", "VGA1" => "800x600"}
|
|
78
97
|
@selected_resolution = {}
|
|
79
98
|
|
|
80
|
-
# primary output
|
|
81
|
-
@
|
|
99
|
+
# initial primary output
|
|
100
|
+
@primary_output = :none
|
|
82
101
|
|
|
83
102
|
@tray = Gtk::StatusIcon.new
|
|
84
103
|
@tray.visible = true
|
|
@@ -128,11 +147,11 @@ module Dumon
|
|
|
128
147
|
item = Gtk::SeparatorMenuItem.new
|
|
129
148
|
rslt.append(item)
|
|
130
149
|
|
|
131
|
-
# outputs
|
|
150
|
+
# single outputs
|
|
132
151
|
outputs.keys.each do |o|
|
|
133
152
|
item = Gtk::MenuItem.new("only #{o}")
|
|
134
153
|
item.signal_connect('activate') do
|
|
135
|
-
self.omanager.single
|
|
154
|
+
self.omanager.switch({:mode=>:single, :out=>o, :resolution=>@selected_resolution[o]})
|
|
136
155
|
# clear preferred resolution, by next rendering will be read from real state
|
|
137
156
|
@selected_resolution.clear
|
|
138
157
|
end
|
|
@@ -150,7 +169,7 @@ module Dumon
|
|
|
150
169
|
|
|
151
170
|
self.omanager.common_resolutions.each do |res|
|
|
152
171
|
si = Gtk::MenuItem.new(res)
|
|
153
|
-
si.signal_connect('activate') { self.omanager.mirror
|
|
172
|
+
si.signal_connect('activate') { self.omanager.switch({:mode=>:mirror, :resolution=>res}) }
|
|
154
173
|
submenu.append(si)
|
|
155
174
|
end
|
|
156
175
|
rslt.append(item)
|
|
@@ -160,7 +179,7 @@ module Dumon
|
|
|
160
179
|
rslt.append(item)
|
|
161
180
|
|
|
162
181
|
# primary output
|
|
163
|
-
item = Gtk::MenuItem.new('
|
|
182
|
+
item = Gtk::MenuItem.new('Primary output')
|
|
164
183
|
submenu = Gtk::Menu.new
|
|
165
184
|
item.set_submenu(submenu)
|
|
166
185
|
item.sensitive = (outputs.keys.size >= 2)
|
|
@@ -169,27 +188,27 @@ module Dumon
|
|
|
169
188
|
prims = outputs.keys.clone << :none
|
|
170
189
|
prims.each do |o|
|
|
171
190
|
si = Gtk::RadioMenuItem.new(radios, o.to_s)
|
|
172
|
-
si.active = (@
|
|
191
|
+
si.active = (@primary_output.to_s == o.to_s)
|
|
173
192
|
radios << si
|
|
174
|
-
si.signal_connect('activate') { @
|
|
193
|
+
si.signal_connect('activate') { @primary_output = o.to_s if si.active? }
|
|
175
194
|
submenu.append(si)
|
|
176
195
|
end
|
|
177
196
|
rslt.append(item)
|
|
178
197
|
|
|
179
|
-
# sequence
|
|
198
|
+
# sequence
|
|
180
199
|
if outputs.keys.size >= 2
|
|
181
200
|
o0 = outputs.keys[0]
|
|
182
201
|
o1 = outputs.keys[1]
|
|
183
202
|
item = Gtk::MenuItem.new("#{o0} left of #{o1}")
|
|
184
203
|
item.signal_connect('activate') do
|
|
185
|
-
|
|
204
|
+
omanager.switch({:mode=>:sequence, :outs=>[o0, o1], :resolutions=>[@selected_resolution[o0], @selected_resolution[o1]], :primary=>@primary_output})
|
|
186
205
|
# clear preferred resolution, by next rendering will be read from real state
|
|
187
206
|
@selected_resolution.clear
|
|
188
207
|
end
|
|
189
208
|
rslt.append(item)
|
|
190
209
|
item = Gtk::MenuItem.new("#{o1} left of #{o0}")
|
|
191
210
|
item.signal_connect('activate') do
|
|
192
|
-
|
|
211
|
+
omanager.switch({:mode=>:sequence, :outs=>[o1, o0], :resolutions=>[@selected_resolution[o1], @selected_resolution[o0]], :primary=>@primary_output})
|
|
193
212
|
# clear preferred resolution, by next rendering will be read from real state
|
|
194
213
|
@selected_resolution.clear
|
|
195
214
|
end
|
|
@@ -197,20 +216,124 @@ module Dumon
|
|
|
197
216
|
end
|
|
198
217
|
|
|
199
218
|
# separator
|
|
200
|
-
|
|
219
|
+
rslt.append(Gtk::SeparatorMenuItem.new)
|
|
220
|
+
|
|
221
|
+
# Profiles
|
|
222
|
+
item = Gtk::MenuItem.new('Profiles...')
|
|
223
|
+
item.signal_connect('activate') { self.profile_management_dialog }
|
|
201
224
|
rslt.append(item)
|
|
225
|
+
|
|
202
226
|
# About
|
|
203
227
|
item = Gtk::ImageMenuItem.new(Gtk::Stock::ABOUT)
|
|
204
228
|
item.signal_connect('activate') { self.about }
|
|
205
229
|
rslt.append(item)
|
|
206
230
|
# Quit
|
|
207
231
|
item = Gtk::ImageMenuItem.new(Gtk::Stock::QUIT)
|
|
208
|
-
item.signal_connect('activate') {
|
|
232
|
+
item.signal_connect('activate') { Dumon::App.instance.quit }
|
|
209
233
|
rslt.append(item)
|
|
210
234
|
|
|
211
235
|
rslt
|
|
212
236
|
end
|
|
213
237
|
|
|
238
|
+
###
|
|
239
|
+
# Applies a profile from configuration according selection in tree view.
|
|
240
|
+
def apply_profile(conf, prof_name)
|
|
241
|
+
profile = conf[:profiles][prof_name.to_sym]
|
|
242
|
+
profile[:mode] = profile[:mode].to_sym
|
|
243
|
+
omanager.switch profile
|
|
244
|
+
Dumon::logger.debug "Profile applied, name=#{prof_name}"
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
###
|
|
248
|
+
# Function to open a dialog box for profile management.
|
|
249
|
+
def profile_management_dialog
|
|
250
|
+
conf = Dumon::App.instance.read_config
|
|
251
|
+
|
|
252
|
+
# create the dialog
|
|
253
|
+
dialog = Gtk::Dialog.new('Profile management', nil, Gtk::Dialog::MODAL, [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_REJECT])
|
|
254
|
+
t = Gtk::Table.new(2, 2)
|
|
255
|
+
t.row_spacings = 5
|
|
256
|
+
t.column_spacings = 5
|
|
257
|
+
|
|
258
|
+
# save new profile
|
|
259
|
+
entry_store = Gtk::Entry.new
|
|
260
|
+
btn_save = Gtk::Button.new(Gtk::Stock::SAVE)
|
|
261
|
+
btn_save.signal_connect('clicked') do
|
|
262
|
+
if entry_store.text.size > 0
|
|
263
|
+
conf[:profiles][entry_store.text] = Dumon::App.instance.current_profile
|
|
264
|
+
Dumon::App.instance.write_config(conf)
|
|
265
|
+
Dumon::logger.debug "Stored profile, name=#{entry_store.text}"
|
|
266
|
+
dialog.destroy
|
|
267
|
+
end
|
|
268
|
+
end
|
|
269
|
+
# disable entry/button if no mode set (probably after start of Dumon)
|
|
270
|
+
if Dumon::App.instance.current_profile.nil?
|
|
271
|
+
entry_store.text = '<make a choice first>'
|
|
272
|
+
entry_store.set_sensitive false
|
|
273
|
+
btn_save.set_sensitive false
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
t.attach(Gtk::HBox.new(false, 5).pack_start(Gtk::Label.new('Profile name:'), false, false).add(entry_store), 0, 1, 0, 1)
|
|
277
|
+
t.attach(btn_save, 1, 2, 0, 1)
|
|
278
|
+
|
|
279
|
+
# select/delete existing profile
|
|
280
|
+
model = Gtk::ListStore.new(String)
|
|
281
|
+
treeview = Gtk::TreeView.new(model)
|
|
282
|
+
treeview.headers_visible = false
|
|
283
|
+
renderer = Gtk::CellRendererText.new
|
|
284
|
+
column = Gtk::TreeViewColumn.new('', renderer, :text => 0)
|
|
285
|
+
treeview.append_column(column)
|
|
286
|
+
|
|
287
|
+
conf[:profiles].keys.each do |k|
|
|
288
|
+
iter = model.append
|
|
289
|
+
iter.set_value 0, k.to_s
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
# apply
|
|
293
|
+
btn_apply = Gtk::Button.new(Gtk::Stock::APPLY)
|
|
294
|
+
btn_apply.signal_connect('clicked') do
|
|
295
|
+
selection = treeview.selection
|
|
296
|
+
if iter = selection.selected
|
|
297
|
+
apply_profile(conf, iter[0])
|
|
298
|
+
dialog.destroy
|
|
299
|
+
end
|
|
300
|
+
end
|
|
301
|
+
# double-click on treeview
|
|
302
|
+
treeview.signal_connect("row-activated") do |view, path|
|
|
303
|
+
if iter = view.model.get_iter(path)
|
|
304
|
+
apply_profile(conf, iter[0])
|
|
305
|
+
dialog.destroy
|
|
306
|
+
end
|
|
307
|
+
end
|
|
308
|
+
# delete
|
|
309
|
+
btn_delete = Gtk::Button.new(Gtk::Stock::DELETE)
|
|
310
|
+
btn_delete.signal_connect('clicked') do
|
|
311
|
+
selection = treeview.selection
|
|
312
|
+
if iter = selection.selected
|
|
313
|
+
prof_name = iter[0]
|
|
314
|
+
conf[:profiles].delete prof_name.to_sym
|
|
315
|
+
Dumon::App.instance.write_config(conf)
|
|
316
|
+
Dumon::logger.debug "Deleted profile, name=#{prof_name}"
|
|
317
|
+
dialog.destroy
|
|
318
|
+
end
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
t.attach(treeview, 0, 1, 1, 2)
|
|
322
|
+
t.attach(Gtk::VBox.new(false, 5).pack_start(btn_apply, false, false).pack_start(btn_delete, false, false), 1, 2, 1, 2)
|
|
323
|
+
|
|
324
|
+
dialog.vbox.add t
|
|
325
|
+
|
|
326
|
+
# ensure that the dialog box is destroyed when the user responds
|
|
327
|
+
dialog.signal_connect('response') do |w, code|
|
|
328
|
+
if Gtk::Dialog::RESPONSE_OK.eql?(code) and entry.text.size > 0
|
|
329
|
+
Dumon::App.instance.write(entry.text => Dumon::App.instance.current_profile)
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
dialog.destroy
|
|
333
|
+
end
|
|
334
|
+
dialog.show_all
|
|
335
|
+
end
|
|
336
|
+
|
|
214
337
|
end
|
|
215
338
|
|
|
216
339
|
end
|
data/lib/dumon/version.rb
CHANGED
|
@@ -2,6 +2,7 @@ module Dumon
|
|
|
2
2
|
|
|
3
3
|
# Version history.
|
|
4
4
|
VERSION_HISTORY = [
|
|
5
|
+
['0.2.0', '2013-03-12', 'Enh #5: Profiles; File based configuration'],
|
|
5
6
|
['0.1.7', '2013-02-13', 'Enh #4: About dialog'],
|
|
6
7
|
['0.1.6', '2013-02-11', 'BF #3: Crash by rendering popup menu if only one output is there'],
|
|
7
8
|
['0.1.5', '2013-02-08', 'Enh #2: Support for primary output'],
|
data/lib/dumon.rb
CHANGED
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
#!/usr/bin/ruby
|
|
2
2
|
|
|
3
|
+
require 'singleton'
|
|
3
4
|
require 'logger'
|
|
4
5
|
require 'gtk2'
|
|
6
|
+
require 'fileutils'
|
|
7
|
+
require 'rrutils'
|
|
5
8
|
require 'dumon/version'
|
|
9
|
+
require 'dumon/omanager'
|
|
10
|
+
require 'dumon/ui'
|
|
6
11
|
|
|
7
12
|
|
|
8
13
|
###
|
|
9
|
-
# This module represents
|
|
14
|
+
# This module represents namespace of Dumon tool.
|
|
10
15
|
module Dumon
|
|
11
16
|
|
|
12
|
-
autoload :XrandrManager, 'dumon/omanager'
|
|
13
|
-
autoload :Tray, 'dumon/ui'
|
|
14
|
-
|
|
15
17
|
class << self
|
|
16
18
|
|
|
17
19
|
###
|
|
@@ -22,37 +24,134 @@ module Dumon
|
|
|
22
24
|
|
|
23
25
|
|
|
24
26
|
###
|
|
25
|
-
#
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
27
|
+
# This class represents an entry point
|
|
28
|
+
class App
|
|
29
|
+
include ::Singleton
|
|
30
|
+
include Rrutils::Confdb
|
|
31
|
+
include Rrutils::Options
|
|
32
|
+
|
|
33
|
+
###
|
|
34
|
+
# User interface of Dumon tool.
|
|
35
|
+
attr_reader :ui
|
|
36
|
+
|
|
37
|
+
###
|
|
38
|
+
# Currently used profile.
|
|
39
|
+
attr_accessor :current_profile
|
|
40
|
+
|
|
41
|
+
###
|
|
42
|
+
# Constructor.
|
|
43
|
+
def initialize
|
|
44
|
+
@ui = new_ui
|
|
45
|
+
Dumon::logger.debug "Used UI: #{ui.class.name}"
|
|
46
|
+
|
|
47
|
+
# storage of preferred resolution for next rendering (will be cleared by output changing)
|
|
48
|
+
# {"LVDS1" => "1600x900", "VGA1" => "800x600"}
|
|
49
|
+
@selected_resolution = {}
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
###
|
|
53
|
+
# Factory method to create a new object of UI.<p/>
|
|
54
|
+
# Can be used as Dependency Injection (DI) entry point:
|
|
55
|
+
# you can reopen Dumon:App and redefine 'new_ui' if you implement a new UI class.
|
|
56
|
+
# <pre>
|
|
57
|
+
# class Dumon::App
|
|
58
|
+
# def new_ui; Dumon::XyUi.new; end
|
|
59
|
+
# end
|
|
60
|
+
# </pre>
|
|
61
|
+
def new_ui(with=Dumon::GtkTrayUi)
|
|
62
|
+
with.new
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
###
|
|
66
|
+
# Gets default config file.
|
|
67
|
+
def config_file(mode='r')
|
|
68
|
+
filename = "#{Dir.home}#{File::SEPARATOR}.config#{File::SEPARATOR}dumon.conf"
|
|
69
|
+
|
|
70
|
+
# check and create directory structure
|
|
71
|
+
dirname = File.dirname filename
|
|
72
|
+
::FileUtils.mkdir_p(dirname) unless Dir.exist?(dirname)
|
|
73
|
+
|
|
74
|
+
# create file if does not exist
|
|
75
|
+
File.open(filename, 'w').close unless File.exist? filename
|
|
76
|
+
|
|
77
|
+
File.open(filename, mode)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
###
|
|
81
|
+
# Reads Dumon's configuration.
|
|
82
|
+
def read_config
|
|
83
|
+
conf = read config_file
|
|
84
|
+
conf = keys_to_sym conf
|
|
85
|
+
|
|
86
|
+
# there can be a hook if config version is old
|
|
87
|
+
|
|
88
|
+
conf
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
###
|
|
92
|
+
# Writes Dumon's configuration.
|
|
93
|
+
def write_config(conf)
|
|
94
|
+
conf[:version] = VERSION
|
|
95
|
+
write(conf, config_file('w'))
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
###
|
|
99
|
+
# Runs the application.
|
|
100
|
+
def run(daemon=false)
|
|
101
|
+
# profile
|
|
102
|
+
prof_arg = ARGV.select {|i| i.start_with? 'profile:'}.first
|
|
103
|
+
unless prof_arg.nil?
|
|
104
|
+
prof_name = prof_arg.split(':')[1]
|
|
105
|
+
conf = read_config
|
|
106
|
+
raise "unknown profile, name=#{prof_name}" unless conf[:profiles][prof_name.to_sym]
|
|
107
|
+
Dumon::logger.info "Started with profile '#{prof_name}'"
|
|
108
|
+
ui.apply_profile(conf, prof_name)
|
|
36
109
|
end
|
|
110
|
+
|
|
111
|
+
# daemon mode
|
|
112
|
+
if daemon
|
|
113
|
+
if RUBY_VERSION < '1.9'
|
|
114
|
+
Dumon::logger.warn 'Daemon mode supported only in Ruby >= 1.9'
|
|
115
|
+
else
|
|
116
|
+
# Daemonize the process
|
|
117
|
+
# - stay in the current directory
|
|
118
|
+
# - don't redirect standard input, standard output and standard error to /dev/null
|
|
119
|
+
Dumon::logger.info 'Running as daemon...'
|
|
120
|
+
Process.daemon(true, true)
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
ui.render
|
|
37
125
|
end
|
|
38
126
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
127
|
+
###
|
|
128
|
+
# Quits cleanly the application.
|
|
129
|
+
def quit
|
|
130
|
+
ui.quit
|
|
131
|
+
Dumon::logger.info 'Terminted...'
|
|
132
|
+
end
|
|
42
133
|
|
|
43
|
-
end
|
|
134
|
+
end # App
|
|
44
135
|
|
|
45
136
|
end
|
|
46
137
|
|
|
47
138
|
|
|
48
|
-
#
|
|
139
|
+
# Default configuration of logging.
|
|
49
140
|
Dumon::logger = Logger.new(STDOUT)
|
|
50
141
|
Dumon::logger.level = Logger::INFO
|
|
51
142
|
|
|
52
143
|
Dumon::logger.info \
|
|
53
144
|
"Dumon #{Dumon::VERSION}, running on Ruby #{RUBY_VERSION} (#{RUBY_RELEASE_DATE}) [#{RUBY_PLATFORM}]"
|
|
54
145
|
|
|
146
|
+
# Capturing Ctrl+C to cleanly quit
|
|
147
|
+
trap('INT') do
|
|
148
|
+
Dumon::logger.debug 'Ctrl+C captured'
|
|
149
|
+
Dumon::App.instance.quit
|
|
150
|
+
end
|
|
151
|
+
|
|
55
152
|
|
|
56
|
-
# development
|
|
57
|
-
|
|
58
|
-
|
|
153
|
+
# development mode
|
|
154
|
+
if __FILE__ == $0
|
|
155
|
+
Dumon::logger.level = Logger::DEBUG
|
|
156
|
+
Dumon::App.instance.run(ARGV.include? '--daemon')
|
|
157
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
require 'json'
|
|
2
|
+
|
|
3
|
+
module Rrutils
|
|
4
|
+
|
|
5
|
+
###
|
|
6
|
+
# This modul represents a mixin for configuration
|
|
7
|
+
# that can be 'loaded from' or 'stored into' a persistent repository.
|
|
8
|
+
module Confdb
|
|
9
|
+
|
|
10
|
+
###
|
|
11
|
+
# Loads and returns a configuration from a file.
|
|
12
|
+
def read(input_stream=STDIN)
|
|
13
|
+
rslt = {}
|
|
14
|
+
unless input_stream.nil?
|
|
15
|
+
begin
|
|
16
|
+
rslt = JSON.load(input_stream)
|
|
17
|
+
Dumon::logger.debug "Configuration readed, keys=#{rslt.keys}"
|
|
18
|
+
rescue => e
|
|
19
|
+
Dumon::logger.warn "failed to read configuration: #{e.message}"
|
|
20
|
+
ensure
|
|
21
|
+
input_stream.close unless input_stream === STDIN
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
rslt
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
###
|
|
29
|
+
# Writes out the configuration to a given output stream.
|
|
30
|
+
def write(conf, output_stream=STDOUT)
|
|
31
|
+
raise ArgumentError, 'configuration not a hash' unless conf.is_a? Hash
|
|
32
|
+
|
|
33
|
+
begin
|
|
34
|
+
output_stream.write(JSON.pretty_generate(conf))
|
|
35
|
+
Dumon::logger.debug "Configuration written, keys=#{conf.keys}"
|
|
36
|
+
rescue => e
|
|
37
|
+
Dumon::logger.error "failed to write configuration: #{e.message}"
|
|
38
|
+
ensure
|
|
39
|
+
output_stream.close unless output_stream === STDOUT
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
end
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
module Rrutils
|
|
2
|
+
|
|
3
|
+
###
|
|
4
|
+
# This modul represents a mixin for a set of convenience methods
|
|
5
|
+
# that work with options representing method's parameters.
|
|
6
|
+
module Options
|
|
7
|
+
|
|
8
|
+
###
|
|
9
|
+
# Fails unless +test+ is a true value.
|
|
10
|
+
#
|
|
11
|
+
# +msg+ may be a String or a Proc.
|
|
12
|
+
# If no +msg+ is given, a default message will be used.
|
|
13
|
+
def assert(test, msg=nil)
|
|
14
|
+
msg ||= 'failed assertion'
|
|
15
|
+
unless test
|
|
16
|
+
msg = msg.call if Proc === msg
|
|
17
|
+
raise ArgumentError, msg
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
###
|
|
22
|
+
# Verifies given options against a pattern that defines checks applied on each option.
|
|
23
|
+
# The pattern is a Hash where property is an expected option's property and value can be:
|
|
24
|
+
# * :optional - corresponding option may or may not be presented
|
|
25
|
+
# * :mandatory - corresponding option has to be presented
|
|
26
|
+
# * Array - corresponding option has to be presented and value has to be in the given list
|
|
27
|
+
#
|
|
28
|
+
# Usage:
|
|
29
|
+
# verify_options(options_to_be_verified, {:foo=>:mandatory, :bar=>[true, false], :baz=>:optional})
|
|
30
|
+
#
|
|
31
|
+
def verify_options(options, pattern)
|
|
32
|
+
raise ArgumentError, 'options cannot be nil' if options.nil?
|
|
33
|
+
raise ArgumentError, 'options is not Hash' unless options.is_a? Hash
|
|
34
|
+
|
|
35
|
+
raise ArgumentError, 'pattern cannot be nil' if pattern.nil?
|
|
36
|
+
raise ArgumentError, 'pattern cannot be empty' if pattern.empty?
|
|
37
|
+
raise ArgumentError, 'pattern is not Hash' unless pattern.is_a? Hash
|
|
38
|
+
|
|
39
|
+
# unknown key?
|
|
40
|
+
options.keys.each do |k|
|
|
41
|
+
raise ArgumentError, "unknow option: #{k}" unless pattern.keys.include? k
|
|
42
|
+
end
|
|
43
|
+
# missing mandatory option?
|
|
44
|
+
pattern.each do |k,v|
|
|
45
|
+
# :mandatory
|
|
46
|
+
if v == :mandatory
|
|
47
|
+
raise ArgumentError, "missing mandatory option: #{k}" if !options.keys.include?(k) or options[k].nil?
|
|
48
|
+
elsif v.is_a? Array
|
|
49
|
+
raise ArgumentError, "value '#{options[k]}' not in #{v.inspect}, key=#{k}" unless v.include?(options[k])
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
options
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
###
|
|
57
|
+
# The same as <code>verify_options</code> with opportunity to define default values
|
|
58
|
+
# of paramaters that will be set if missing in options.
|
|
59
|
+
#
|
|
60
|
+
# Usage:
|
|
61
|
+
# verify_and_sanitize_options(options_to_be_verified, {:foo=>'defaultValue', :bar=>100})
|
|
62
|
+
#
|
|
63
|
+
def verify_and_sanitize_options(options, pattern, cloned=true)
|
|
64
|
+
opts = cloned ? options.clone : options
|
|
65
|
+
|
|
66
|
+
verify_options(opts, pattern)
|
|
67
|
+
|
|
68
|
+
# set default values if missing in options
|
|
69
|
+
pattern.select { |k,v| !v.nil? and v != :optional }.each do |k,v|
|
|
70
|
+
opts[k] = v if !opts.keys.include? k
|
|
71
|
+
end
|
|
72
|
+
opts
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
###
|
|
76
|
+
# Goes recursively through given Hash and converst all key into Symbol.
|
|
77
|
+
def keys_to_sym(options)
|
|
78
|
+
raise ArgumentError, 'not a hash' unless options.is_a? Hash
|
|
79
|
+
opts = options.clone
|
|
80
|
+
|
|
81
|
+
options.each do |k,v|
|
|
82
|
+
opts[k.to_sym] = opts.delete k unless k.is_a? Symbol
|
|
83
|
+
opts[k.to_sym] = keys_to_sym(v) if v.is_a? Hash
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
opts
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
end
|
data/lib/rrutils.rb
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
require 'test/unit'
|
|
2
|
+
require 'rrutils'
|
|
3
|
+
|
|
4
|
+
###
|
|
5
|
+
# This class tests Rrutils::Options module.
|
|
6
|
+
class TestRrutilsOptions < Test::Unit::TestCase
|
|
7
|
+
include Rrutils::Options
|
|
8
|
+
|
|
9
|
+
def test_verify_options_preconditions
|
|
10
|
+
assert_raise ArgumentError do verify_options(nil, {:a => 'A'}); end
|
|
11
|
+
assert_raise ArgumentError do verify_options('a string', {:a => 'A'}); end
|
|
12
|
+
assert_raise ArgumentError do verify_options({:b => 'B'}, nil); end
|
|
13
|
+
assert_raise ArgumentError do verify_options({:b => 'B'}, {}); end
|
|
14
|
+
assert_raise ArgumentError do verify_options({:b => 'B'}, 'a string'); end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def test_verify_options
|
|
18
|
+
opt_pattern = {:a => :mandatory, :b => :optional, :c => 'predefined', :d => [1, false]}
|
|
19
|
+
assert_nothing_thrown do verify_options({:a => 'A', :b => 'B', :c => 'C', :d => 1}, opt_pattern); end
|
|
20
|
+
assert_nothing_thrown do verify_options({:a => 'A', :d => 1}, opt_pattern); end
|
|
21
|
+
|
|
22
|
+
# missing mandatory
|
|
23
|
+
assert_raise ArgumentError do verify_options({}, opt_pattern); end
|
|
24
|
+
assert_raise ArgumentError do verify_options({:a => 'A'}, opt_pattern); end
|
|
25
|
+
assert_raise ArgumentError do verify_options({:d => 1}, opt_pattern); end
|
|
26
|
+
assert_raise ArgumentError do verify_options({:a => nil, :d => 1}, opt_pattern); end
|
|
27
|
+
assert_raise ArgumentError do verify_options({:a => 'A', :d => nil}, opt_pattern); end
|
|
28
|
+
# unknown key
|
|
29
|
+
assert_raise ArgumentError do verify_options({:a => 'A', :z => 2}, opt_pattern); end
|
|
30
|
+
# value not in predefined set
|
|
31
|
+
assert_raise ArgumentError do verify_options({:a => 'A', :d => 3}, opt_pattern); end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def test_verify_and_sanitize_options
|
|
35
|
+
opt_pattern = {:a => 'A', :b => 'B'}
|
|
36
|
+
options = {:a => 'X'}
|
|
37
|
+
opts = verify_and_sanitize_options(options, opt_pattern)
|
|
38
|
+
assert_equal 2, opts.size
|
|
39
|
+
assert_equal 'X', opts[:a]
|
|
40
|
+
assert_equal 'B', opts[:b]
|
|
41
|
+
|
|
42
|
+
# :optional cannot be set as default value
|
|
43
|
+
opt_pattern = {:a => :optional, :b => 'B'}
|
|
44
|
+
options = {}
|
|
45
|
+
opts = verify_and_sanitize_options(options, opt_pattern)
|
|
46
|
+
assert_equal 1, opts.size
|
|
47
|
+
assert !opts.include?(:a)
|
|
48
|
+
assert_equal 'B', opts[:b]
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: dumon
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
prerelease:
|
|
6
6
|
platform: ruby
|
|
7
7
|
authors:
|
|
@@ -9,7 +9,7 @@ authors:
|
|
|
9
9
|
autorequire:
|
|
10
10
|
bindir: bin
|
|
11
11
|
cert_chain: []
|
|
12
|
-
date: 2013-
|
|
12
|
+
date: 2013-03-12 00:00:00.000000000 Z
|
|
13
13
|
dependencies:
|
|
14
14
|
- !ruby/object:Gem::Dependency
|
|
15
15
|
name: gtk2
|
|
@@ -49,6 +49,10 @@ files:
|
|
|
49
49
|
- lib/dumon/version.rb
|
|
50
50
|
- lib/monitor24.png
|
|
51
51
|
- lib/monitor48.png
|
|
52
|
+
- lib/rrutils.rb
|
|
53
|
+
- lib/rrutils/confdb.rb
|
|
54
|
+
- lib/rrutils/options.rb
|
|
55
|
+
- test/test_rrutils_options.rb
|
|
52
56
|
homepage: http://github.com/veny/dumon
|
|
53
57
|
licenses: []
|
|
54
58
|
post_install_message:
|
|
@@ -74,4 +78,5 @@ rubygems_version: 1.8.23
|
|
|
74
78
|
signing_key:
|
|
75
79
|
specification_version: 3
|
|
76
80
|
summary: Dual monitor manager for Linux.
|
|
77
|
-
test_files:
|
|
81
|
+
test_files:
|
|
82
|
+
- test/test_rrutils_options.rb
|