6 # an amazing widget manager for an awesome window manager
8 # Usage: amazing [options]
9 # -c, --config FILE Configuration file (~/.amazing.yml)
10 # -s, --screen ID Screen ID (0)
11 # -l, --log-level LEVEL Severity threshold (info)
12 # -i, --include SCRIPT Include a widgets script
13 # -u, --update WIDGET Update a widget and exit
14 # -w, --list-widgets List available widgets
15 # -h, --help You're looking at it
19 # * Battery: Remaining battery power in percentage
20 # * Maildir: Mail count in maildirs
21 # * ALSA: Various data for the ALSA mixer
22 # * Raggle: Unread posts in raggle
23 # * Memory: Various memory related data
24 # * Clock: Displays date and time
40 # field: instance method
63 # In this example tb_mail doesn't have an "every" setting and is instead
64 # updated manually with <tt>amazing -u tb_mail</tt>, perhaps in cron after fetching
65 # new mail via fdm, getmail, fetchmail or similar. A good idea is also to
66 # update after closing your MUA such as Mutt which could be done with
67 # shell functions, example:
76 # Widgets inherit from Widget, serves data via instance methods, signalizes
77 # errors by raising a WidgetError and processes widget options via @options.
78 # The init method is used instead of initialize. Here's an example:
80 # class MyWidget < Widget
81 # description "This is my widget"
82 # attr_reader :my_field
83 # alias_method :default, :my_field
86 # @my_field = @options["text"] || "No text configured"
87 # raise WidgetError, "oops!" if some_error?
91 # The ProcFile class can be used for parsing /proc files:
93 # cpuinfo = ProcFile.new("cpuinfo")
94 # cpuinfo[1]["model name"]
95 # #=> "AMD Turion(tm) 64 X2 Mobile Technology TL-50"
99 # * Validate existence of awesome socket
100 # * Update widgets without an interval on start (when there is a socket)
101 # * Maybe auto-include scripts from ~/.amazing/something
102 # * Self-documenting widgets (list fields and options)
103 # * Some widgets need to support multiple data sources
104 # * More widgets, duh
108 # Copyright (C) 2008 Dag Odenhall <dag.odenhall@gmail.com>
109 # Licensed under the Academic Free License version 3.0
110 # http://www.rosenlaw.com/AFL3.0.htm
121 # Communicate with awesome
123 # awesome = Awesome.new
124 # awesome.widget_tell(widget_id, "Hello, world")
125 # awesome = Awesome.new(1)
126 # awesome.tag_view(3)
127 # Awesome.new.client_zoom
129 attr_accessor :screen
131 def initialize(screen=0)
132 @screen = screen.to_i
135 def method_missing(method, *args)
136 IO.popen("awesome-client", IO::WRONLY) do |ac|
137 ac.puts "#@screen #{method} #{args.join(' ')}"
144 # cpuinfo = ProcFile.new("cpuinfo")
145 # cpuinfo[1]["model name"]
146 # #=> "AMD Turion(tm) 64 X2 Mobile Technology TL-50"
151 file = "/proc/#{file}" if file[0] != ?/
153 File.readlines(file).each do |line|
154 if sep = line.index(":")
155 @list[-1][line[0..sep-1].strip] = line[sep+1..-1].strip
160 @list.pop if @list[-1].empty?
164 @list.each do |section|
174 # Raised by widgets, and is then rescued and logged
175 class WidgetError < Exception
178 # Parent class for widget construction
180 # class MyWidget < Widget
181 # description "This is my widget"
182 # attr_reader :my_field
183 # alias_method :default, :my_field
186 # @my_field = @options["text"] || "No text configured"
187 # raise WidgetError, "oops!" if some_error?
191 def initialize(opts={})
193 init if respond_to? :init
196 def self.description(description=nil)
198 @description = description
207 description "Various data for the ALSA mixer"
209 alias_method :default, :volume
212 mixer = @options["mixer"] || "Master"
213 IO.popen("amixer get #{mixer}", IO::RDONLY) do |am|
215 volumes = out.scan(/\[(\d+)%\]/).flatten
217 volumes.each {|vol| @volume += vol.to_i }
218 @volume = @volume / volumes.size
223 class Battery < Widget
224 description "Remaining battery power in percentage"
225 attr_reader :percentage
226 alias_method :default, :percentage
229 battery = @options["battery"] || 1
230 batinfo = ProcFile.new("acpi/battery/BAT#{battery}/info")[0]
231 batstate = ProcFile.new("acpi/battery/BAT#{battery}/state")[0]
232 remaining = batstate["remaining capacity"].to_i
233 lastfull = batinfo["last full capacity"].to_i
234 @percentage = (remaining * 100) / lastfull.to_f
239 description "Displays date and time"
241 alias_method :default, :time
244 format = @options["format"] || "%R"
245 @time = Time.now.strftime(format)
249 class Maildir < Widget
250 description "Mail count in maildirs"
252 alias_method :default, :count
256 raise WidgetError, "No directories configured" unless @options["directories"]
257 @options["directories"].each do |glob|
258 glob = "#{ENV["HOME"]}/#{glob}" if glob[0] != ?/
259 @count += Dir["#{glob}/*"].size
264 class Memory < Widget
265 description "Various memory related data"
266 attr_reader :total, :free, :buffers, :cached, :usage
267 alias_method :default, :usage
270 meminfo = ProcFile.new("meminfo")[0]
271 @total = meminfo["MemTotal"].to_i
272 @free = meminfo["MemFree"].to_i
273 @buffers = meminfo["Buffers"].to_i
274 @cached = meminfo["Cached"].to_i
275 @usage = ((@total - @free - @cached - @buffers) * 100) / @total.to_f
279 class Raggle < Widget
280 description "Unread posts in raggle"
282 alias_method :default, :unread
285 feed_list = @options["feed_list_path"] || ".raggle/feeds.yaml"
286 feed_list = "#{ENV["HOME"]}/#{feed_list}" if feed_list[0] != ?/
287 feeds = YAML.load_file(feed_list)
288 feed_cache = @options["feed_cache_path"] || ".raggle/feed_cache.store"
289 feed_cache = "#{ENV["HOME"]}/#{feed_cache}" if feed_cache[0] != ?/
290 cache = PStore.new(feed_cache)
292 cache.transaction(false) do
294 cache[feed["url"]].each do |item|
295 @unread += 1 unless item["read?"]
303 # Parse and manage command line options
309 @options[:config] = "#{ENV["HOME"]}/.amazing.yml"
310 @options[:screens] = []
311 @options[:loglevel] = "info"
312 @options[:include] = []
313 @options[:update] = []
315 @parser = OptionParser.new do |opts|
316 opts.on("-c", "--config FILE", "Configuration file (~/.amazing.yml)") do |config|
317 @options[:config] = config
319 opts.on("-s", "--screen ID", "Screen ID (0)") do |screen|
320 @options[:screens] << screen
322 opts.on("-l", "--log-level LEVEL", "Severity threshold (info)") do |level|
323 @options[:loglevel] = level
325 opts.on("-i", "--include SCRIPT", "Include a widgets script") do |script|
326 @options[:include] << script
328 opts.on("-u", "--update WIDGET", "Update a widget and exit") do |widget|
329 @options[:update] << widget
331 opts.on("-w", "--list-widgets", "List available widgets") do
332 @options[:listwidgets] = true
334 opts.on("-h", "--help", "You're looking at it") do
335 @options[:help] = true
341 @options.keys.each do |key|
346 def parse(args=@args)
358 def []=(option, value)
359 @options[option] = value
363 # Command line interface runner
369 @log = Logger.new(STDOUT)
370 @options = Options.new(@args)
375 @log.fatal("Received SIGINT, exiting")
379 show_help if @options[:help]
383 list_widgets if @options[:listwidgets]
385 update_widgets unless @options[:update].empty?
388 @config["widgets"].each do |widget_name, settings|
389 if settings["every"] && count % settings["every"] == 0
391 @screens.each do |screen, awesome|
392 @log.debug("Updating widget #{widget_name} of type #{settings["type"]} on screen #{screen}")
394 opts = settings["options"] || {}
395 widget = Widgets.const_get(settings["type"]).new(opts)
396 field = settings["field"] || "default"
397 awesome.widget_tell(widget_name, widget.__send__(field))
400 rescue WidgetError => e
401 @log.error(settings["type"]) { e.message }
419 @log.level = Logger.const_get(@options[:loglevel].upcase)
421 @log.error("Unsupported log level #{@options[:loglevel].inspect}")
422 @log.level = Logger::INFO
427 scripts = (@options[:include] + @config["include"]).uniq
428 scripts.each do |script|
430 Widgets.module_eval("require #{script.inspect}")
432 @log.error("No such widget script #{script.inspect}")
438 Widgets.constants.each do |widget|
439 if description = Widgets.const_get(widget).description
440 puts "#{widget}: #{description}"
449 @log.info("Parsing configuration file")
451 @config = YAML.load_file(@options[:config])
453 @log.fatal("Unable to parse configuration file, exiting")
456 @config["include"] ||= []
457 @config["screens"] ||= []
462 @options[:screens].each do |screen|
463 @screens[screen.to_i] = Awesome.new(screen)
466 @config["screens"].each do |screen|
467 @screens[screen] = Awesome.new(screen)
470 @screens[0] = Awesome.new if @screens.empty?
474 @config["widgets"].each do |widget_name, settings|
475 next unless @options[:update].include? widget_name
476 opts = settings["options"] || {}
478 widget = Widgets.const_get(settings["type"]).new(opts)
479 field = settings["field"] || "default"
480 @screens.each do |screen, awesome|
481 @log.debug("Updating widget #{widget_name} of type #{settings["type"]} on screen #{screen}")
482 awesome.widget_tell(widget_name, widget.__send__(field))
484 rescue WidgetError => e
485 @log.error(settings["type"]) { e.message }
494 Amazing::CLI.new(ARGV).run