dumon 0.1.7 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -2,4 +2,4 @@
2
2
 
3
3
  require 'dumon'
4
4
 
5
- Dumon::run(ARGV[0] == '--daemon')
5
+ Dumon::App.instance.run(ARGV.include? '--daemon')
@@ -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
- attr_accessor :stool
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
- attr_accessor :outputs
22
+ attr_reader :outputs
20
23
 
21
24
  ###
22
- # Reads info about current accessible output devices and their settings.
23
- def read
24
- raise NotImplementedError, 'this should be overridden by concrete sub-class'
25
- end
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
- # Mirrors output on all devices with given resolution.
35
- def mirror(resolution)
36
- raise NotImplementedError, 'this should be overridden by concrete sub-class'
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
- raise 'no outputs' if self.outputs.nil? or self.outputs.empty?
51
- raise "unknown output: #{output}" unless self.outputs.keys.include?(output)
52
- raise "no default resolution, output: #{output}" unless self.outputs[output].keys.include?(:default)
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
- self.outputs[output][:default]
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
- raise 'no outputs' if self.outputs.nil? or self.outputs.empty?
96
+ assert(!outputs.nil?, 'no outputs found')
61
97
 
62
98
  rslt = []
63
- o1 = self.outputs.keys.first
64
- self.outputs[o1][:resolutions].each do |res|
65
- self.outputs.keys.each do |o|
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 self.outputs[o][:resolutions].include?(res)
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 = ['xrandr', '/usr/bin/xrandr']
148
+ paths = ['/usr/bin/xrandr', 'xrandr']
86
149
  paths.each do |path|
87
150
  begin
88
151
  `#{path}`
89
- self.stool = path
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
- self.outputs = rslt
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
- def single(output, resolution, type=nil) #:nodoc:
125
- self.read if self.outputs.nil? or self.outputs.empty?
126
- raise "uknown output: #{output}" unless self.outputs.keys.include?(output)
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
- self.read if self.outputs.nil? or self.outputs.empty?
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(outputs, primary=:none) #:nodoc:
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..outputs.size - 1
155
- output = outputs[i][0]
156
- resolution = outputs[i][1]
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 #{outputs[i - 1][0]}" if i > 0
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
- attr_accessor :omanager
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
- Dumon::logger.info "Terminted..."
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 Tray < GtkUi
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
- @primary = :none
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(o, @selected_resolution[o])
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(res) }
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('primary output')
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 = (@primary.to_s == o.to_s)
191
+ si.active = (@primary_output.to_s == o.to_s)
173
192
  radios << si
174
- si.signal_connect('activate') { @primary = o.to_s if si.active? }
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 (currently supporting only 2 output devices)
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
- self.omanager.sequence([[o0, @selected_resolution[o0]], [o1, @selected_resolution[o1]]], @primary)
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
- self.omanager.sequence([[o1, @selected_resolution[o1]], [o0, @selected_resolution[o0]]], @primary)
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
- item = Gtk::SeparatorMenuItem.new
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') { self.quit }
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 the entry point of Dumon tool.
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
- # Runs the application.
26
- def self.run(daemon=false)
27
- if daemon
28
- if RUBY_VERSION < '1.9'
29
- Dumon::logger.warn 'Daemon mode supported only in Ruby >= 1.9'
30
- else
31
- # Daemonize the process
32
- # - stay in the current directory
33
- # - don't redirect standard input, standard output and standard error to /dev/null
34
- Dumon::logger.info 'Running as daemon...'
35
- Process.daemon(true, true)
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
- ui = Dumon::Tray.new
40
- ui.omanager = Dumon::XrandrManager.new
41
- ui.render
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
- # Configuration of logging.
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
- #Dumon::logger.level = Logger::DEBUG
58
- #Dumon::run(ARGV[0] == '--daemon')
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,9 @@
1
+ ###
2
+ # This module represents namespace of Rrutils meaning 'Reasonable Ruby Utilities'
3
+ # - yet another general purpose utilities package.
4
+ module Rrutils
5
+
6
+ autoload :Confdb, 'rrutils/confdb'
7
+ autoload :Options, 'rrutils/options'
8
+
9
+ end
@@ -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.1.7
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-02-13 00:00:00.000000000 Z
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