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