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 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