Threadify every widget update
[amazing.git] / lib / amazing / cli.rb
blobaf7a35c95f17f073e10bdec025515eae004f4a2a
1 # Copyright (C) 2008 Dag Odenhall <dag.odenhall@gmail.com>
2 # Licensed under the Academic Free License version 3.0
4 require 'logger'
5 require 'amazing/options'
6 require 'amazing/x11/display_name'
7 require 'amazing/config'
8 require 'amazing/string'
9 require 'amazing/widget'
10 require 'amazing/proc_file'
11 require 'amazing/widgets'
12 require 'amazing/awesome'
13 require 'timeout'
14 require 'fileutils'
15 require 'thread'
17 module Amazing
19   # Command line interface runner
20   #
21   #   CLI.run(ARGV)
22   class CLI
23     def initialize(args)
24       @args = args
25       @log = Logger.new(STDOUT)
26       @options = Options.new(@args)
27       begin
28         @display = X11::DisplayName.new
29       rescue X11::EmptyDisplayName => e
30         @log.warn("#{e.message}, falling back on :0")
31         @display = X11::DisplayName.new(":0")
32       rescue X11::InvalidDisplayName => e
33         @log.fatal("#{e.message}, exiting")
34         exit 1
35       end
36       @threads = []
37     end
39     def run
40       at_exit do
41         remove_pid
42         Thread.list.each {|t| t.exit }
43       end
44       trap("SIGINT") do
45         @log.fatal("Received SIGINT, exiting")
46         exit
47       end
48       @options.parse
49       show_help if @options[:help]
50       set_loglevel
51       stop_process(true) if @options[:stop]
52       load_scripts
53       list_widgets if @options[:listwidgets]
54       test_widget if @options[:test]
55       parse_config
56       wait_for_sockets
57       @awesome = Awesome.new(@display.display)
58       explicit_updates unless @options[:update] == []
59       stop_process
60       save_pid
61       update_non_interval
62       @config[:awesome].each do |awesome|
63         awesome[:widgets].each do |widget|
64           if widget[:interval]
65             @threads << Thread.new(awesome, widget) do |awesome, widget|
66               loop do
67                 Thread.new { update_widget(awesome[:screen], awesome[:statusbar], widget[:identifier]) }
68                 sleep widget[:interval]
69               end
70             end
71           end
72         end
73       end
74       @threads.each {|t| t.join }
75     end
77     private
79     def show_help
80       puts @options.help
81       exit
82     end
84     def set_loglevel
85       begin
86         @log.level = Logger.const_get(@options[:loglevel].upcase)
87       rescue NameError
88         @log.error("Unsupported log level #{@options[:loglevel].inspect}")
89         @log.level = Logger::INFO
90       end
91     end
93     def stop_process(quit=false)
94       begin
95         Process.kill("SIGINT", File.read("#{ENV["HOME"]}/.amazing/pids/#{@display.display}.pid").to_i) 
96         @log.warn("Killed older process") unless quit
97       rescue
98       end
99       exit if quit
100     end
102     def load_scripts
103       scripts = @options[:include]
104       if @options[:autoinclude]
105         scripts << Dir["#{ENV["HOME"]}/.amazing/widgets/*"]
106       end
107       scripts.flatten.each do |script|
108         if File.exist?(script)
109           @log.debug("Loading script #{script.inspect}")
110           begin
111             Widgets.module_eval(File.read(script), script)
112           rescue SyntaxError => e
113             @log.error("Bad syntax in #{script} at line #{e.to_s.scan(/:(\d+)/)}")
114           end
115         else
116           @log.error("No such widget script #{script.inspect}")
117         end
118       end
119     end
121     def parse_config
122       @log.debug("Parsing configuration file")
123       begin
124         @config = Config.new(@options[:config])
125       rescue
126         @log.fatal("Unable to parse configuration file, exiting")
127         exit 1
128       end
129     end
131     def list_widgets
132       if @options[:listwidgets] == true
133         longest_widget_name = Widgets.constants.inject {|a,b| a.length > b.length ? a : b }.length
134         Widgets.constants.sort.each do |widget|
135           widget_class = Widgets.const_get(widget)
136           puts "%-#{longest_widget_name}s : %s" % [widget, widget_class.description]
137         end
138       else
139         widget_class = Widgets.const_get(@options[:listwidgets].camel_case)
140         puts
141         puts "#{@options[:listwidgets].camel_case} - #{widget_class.description}"
142         puts
143         dependencies = widget_class.dependencies
144         unless dependencies.empty?
145           longest_dependency_name = dependencies.keys.inject {|a,b| a.to_s.length > b.to_s.length ? a : b }.to_s.length
146           longest_dependency_name = 10 if longest_dependency_name < 10
147           longest_description = dependencies.values.inject {|a,b| a.length > b.length ? a : b }.length
148           longest_description = 11 if longest_description < 11
149           puts " %-#{longest_dependency_name}s | DESCRIPTION" % "DEPENDENCY"
150           puts "-" * (longest_dependency_name + longest_description + 5)
151           dependencies.keys.sort.each do |dependency|
152             puts " %-#{longest_dependency_name}s | #{dependencies[dependency]}" % dependency
153           end
154           puts
155         end
156         options = widget_class.options
157         unless options.empty?
158           longest_option_name = options.keys.inject {|a,b| a.to_s.length > b.to_s.length ? a : b }.to_s.length
159           longest_option_name = 6 if longest_option_name < 6
160           longest_description = options.values.inject {|a,b| a[:description].length > b[:description].length ? a : b }[:description].length
161           longest_description = 11 if longest_description < 11
162           longest_default = options.values.inject {|a,b| a[:default].inspect.length > b[:default].inspect.length ? a : b }[:default].inspect.length
163           longest_default = 7 if longest_default < 7
164           puts " %-#{longest_option_name}s | %-#{longest_description}s | DEFAULT" % ["OPTION", "DESCRIPTION"]
165           puts "-" * (longest_option_name + longest_description + longest_default + 8)
166           options.keys.sort_by {|option| option.to_s }.each do |option|
167             puts " %-#{longest_option_name}s | %-#{longest_description}s | %s" % [option, options[option][:description], options[option][:default].inspect]
168           end
169           puts
170         end
171         fields = widget_class.fields
172         unless fields.empty?
173           longest_field_name = fields.keys.inject {|a,b| a.to_s.length > b.to_s.length ? a : b }.to_s.length
174           longest_field_name = 5 if longest_field_name < 5
175           longest_description = fields.values.inject {|a,b| a[:description].length > b[:description].length ? a : b }[:description].length
176           longest_description = 11 if longest_description < 11
177           longest_default = fields.values.inject {|a,b| a[:default].inspect.length > b[:default].inspect.length ? a : b }[:default].inspect.length
178           longest_default = 7 if longest_default < 7
179           puts " %-#{longest_field_name + 1}s | %-#{longest_description}s | DEFAULT" % ["FIELD", "DESCRIPTION"]
180           puts "-" * (longest_field_name + longest_description + longest_default + 9)
181           fields.keys.sort_by {|field| field.to_s }.each do |field|
182             puts " @%-#{longest_field_name}s | %-#{longest_description}s | %s" % [field, fields[field][:description], fields[field][:default].inspect]
183           end
184           puts
185         end
186       end
187       exit
188     end
190     def test_widget
191       widget = Widgets.const_get(@options[:test].camel_case)
192       settings = YAML.load("{#{ARGV[0]}}")
193       instance = widget.new(settings)
194       longest_field_name = widget.fields.merge({:default => nil}).keys.inject {|a,b| a.to_s.length > b.to_s.length ? a : b }.to_s.length
195       puts "@%-#{longest_field_name}s = %s" % [:default, instance.instance_variable_get(:@default).inspect]
196       widget.fields.keys.sort_by {|field| field.to_s }.each do |field|
197         puts "@%-#{longest_field_name}s = %s" % [field, instance.instance_variable_get("@#{field}".to_sym).inspect]
198       end
199       exit
200     end
202     def wait_for_sockets
203       @log.debug("Waiting for awesome control socket for display #{@display.display}")
204       begin
205         Timeout.timeout(30) do
206           sleep 1 until File.exist?("#{ENV["HOME"]}/.awesome_ctl.#{@display.display}")
207           @log.debug("Got socket for display #{@display.display}")
208         end
209       rescue Timeout::Error
210         @log.fatal("Socket for display #{@display.display} not created within 30 seconds, exiting")
211         exit 1
212       end
213     end
215     def explicit_updates
216       @config[:awesome].each do |awesome|
217         awesome[:widgets].each do |widget|
218           locator = "%s/%s/%s" % [widget[:identifier], awesome[:statusbar], awesome[:screen]]
219           next unless @options[:update] == :all || @options[:update].include?(locator)
220           @threads << Thread.new(awesome, widget) do |awesome, widget|
221             update_widget(awesome[:screen], awesome[:statusbar], widget[:identifier])
222           end
223         end
224       end
225       @threads.each {|t| t.join }
226       exit
227     end
229     def save_pid
230       path = "#{ENV["HOME"]}/.amazing/pids"
231       FileUtils.makedirs(path)
232       File.open("#{path}/#{@display.display}.pid", "w+") do |f|
233         f.write($$)
234       end
235     end
237     def remove_pid
238       File.delete("#{ENV["HOME"]}/.amazing/pids/#{@display.display}.pid") rescue Errno::ENOENT
239     end
241     def update_non_interval
242       @config[:awesome].each do |awesome|
243         awesome[:widgets].each do |widget|
244           next if widget[:interval]
245           @threads << Thread.new(awesome, widget) do |awesome, widget|
246             update_widget(awesome[:screen], awesome[:statusbar], widget[:identifier])
247           end
248         end
249       end
250       @threads.each {|t| t.join }
251     end
253     def update_widget(screen, statusbar, identifier)
254       threads = []
255       @config[:awesome].each do |awesome|
256         next unless screen == awesome[:screen] && statusbar == awesome[:statusbar]
257         awesome[:widgets].each do |widget|
258           next unless widget[:identifier] == identifier
259           @log.debug("Updating widget #{identifier} of type #{widget[:module]} on screen #{screen}")
260           threads << Thread.new(widget) do |widget|
261             begin
262               mod = Widgets.const_get(widget[:module]).new(widget)
263               if widget[:properties].empty?
264                 @awesome.widget_tell(screen, statusbar, identifier, widget[:property], mod.formatize)
265               end
266               widget[:properties].each do |property, format|
267                 @awesome.widget_tell(screen, statusbar, identifier, property, mod.formatize(format))
268               end
269             rescue WidgetError => e
270               @log.error(widget[:module]) { e.message }
271             end
272           end
273         end
274       end
275       threads.each {|t| t.join }
276     end
277   end