]> git.rmz.io Git - dotfiles.git/commitdiff
awesome: add awesompd widget + mpd bindings
authorSamir Benmendil <samir.benmendil@gmail.com>
Mon, 13 Jan 2014 12:12:51 +0000 (13:12 +0100)
committerSamir Benmendil <samir.benmendil@gmail.com>
Mon, 13 Jan 2014 12:12:51 +0000 (13:12 +0100)
18 files changed:
awesome/bindings.lua
awesome/topbar.lua
awesome/widgets/awesompd.lua [new file with mode: 0644]
awesome/widgets/awesompd/README.md [new file with mode: 0644]
awesome/widgets/awesompd/asyncshell.lua [new file with mode: 0755]
awesome/widgets/awesompd/awesompd.lua [new file with mode: 0644]
awesome/widgets/awesompd/icons/check_icon.png [new file with mode: 0644]
awesome/widgets/awesompd/icons/default_album_cover.png [new file with mode: 0644]
awesome/widgets/awesompd/icons/next_icon.png [new file with mode: 0644]
awesome/widgets/awesompd/icons/pause_icon.png [new file with mode: 0644]
awesome/widgets/awesompd/icons/play_icon.png [new file with mode: 0644]
awesome/widgets/awesompd/icons/play_pause_icon.png [new file with mode: 0644]
awesome/widgets/awesompd/icons/prev_icon.png [new file with mode: 0644]
awesome/widgets/awesompd/icons/radio_icon.png [new file with mode: 0644]
awesome/widgets/awesompd/icons/stop_icon.png [new file with mode: 0644]
awesome/widgets/awesompd/jamendo.lua [new file with mode: 0644]
awesome/widgets/awesompd/rcsample.lua [new file with mode: 0644]
awesome/widgets/awesompd/utf8.lua [new file with mode: 0644]

index 6a72d0ba9c5ff5691c7ce73cf8446efd88ff17d6..1f68d7cb3c26c9a174cee750b3ffe81e669efd65 100644 (file)
@@ -5,7 +5,7 @@
 -- However, you can use another modifier like Mod1, but it may interact with others.
 modkey = "Mod4"
 
--- {{{ Mouse bindings
+-- {{{1 Mouse bindings
 root.buttons(awful.util.table.join(
     awful.button({ }, 3, function () mymainmenu:toggle() end),
     awful.button({ }, 4, awful.tag.viewnext),
@@ -16,11 +16,10 @@ clientbuttons = awful.util.table.join(
     awful.button({ }, 1, function (c) client.focus = c; c:raise() end),
     awful.button({ modkey }, 1, awful.mouse.client.move),
     awful.button({ modkey }, 3, awful.mouse.client.resize))
--- }}}
 
--- {{{ Global Keys
+-- {{{1 Global Keys
 globalkeys = awful.util.table.join(
-    -- {{{ Awesome Keys
+    -- {{{2 Awesome Keys
     awful.key({ modkey,           }, "Left",   awful.tag.viewprev       ),
     awful.key({ modkey,           }, "Right",  awful.tag.viewnext       ),
     awful.key({ modkey,           }, "Escape", awful.tag.history.restore),
@@ -71,9 +70,8 @@ globalkeys = awful.util.table.join(
               end),
     -- Menubar
     awful.key({ modkey }, "p", function() menubar.show() end),
-    -- }}}
 
-    -- {{{ My Keys
+    -- {{{2 My Keys
     -- Menu
     awful.key({ modkey }, "w", function () mymainmenu:show({ coords = { x = 0, y = 0}}) end),
     -- Lock
@@ -97,19 +95,15 @@ globalkeys = awful.util.table.join(
     awful.key({ }, "XF86AudioRaiseVolume",  function () awful.util.spawn("ponymix increase 5") end),
     awful.key({ }, "XF86AudioLowerVolume",  function () awful.util.spawn("ponymix decrease 5") end),
     awful.key({ }, "XF86AudioMute",         function () awful.util.spawn("ponymix toggle")     end),
-    -- Clementine
-    awful.key({ }, "XF86AudioPlay",         function () awful.util.spawn("clementine -t")     end),
-    awful.key({ }, "XF86AudioNext",         function () awful.util.spawn("clementine -f")     end),
-    awful.key({ }, "XF86AudioPrev",         function () awful.util.spawn("clementine -r")     end),
-    awful.key({ "Shift" }, "XF86AudioRaiseVolume",  function () awful.util.spawn("clementine --volume-increase-by 5") end),
-    awful.key({ "Shift" }, "XF86AudioLowerVolume",  function () awful.util.spawn("clementine --volume-decrease-by 5") end)
-    -- }}}
+    -- mpd
+    awful.key({ }, "XF86AudioPlay",         function () awful.util.spawn("mpc toggle") end),
+    awful.key({ }, "XF86AudioNext",         function () awful.util.spawn("mpc next")   end),
+    awful.key({ }, "XF86AudioPrev",         function () awful.util.spawn("mpc prev")   end)
 )
--- }}}
 
--- {{{ Client keys
+-- {{{1 Client keys
 clientkeys = awful.util.table.join(
-    -- {{{ Awesome Client Keys
+    -- {{{2 Awesome Client Keys
     awful.key({ modkey,           }, "f",      function (c) c.fullscreen = not c.fullscreen  end),
     awful.key({ modkey, "Shift"   }, "c",      function (c) c:kill()                         end),
     awful.key({ modkey, "Control" }, "space",  awful.client.floating.toggle                     ),
@@ -125,9 +119,8 @@ clientkeys = awful.util.table.join(
             c.maximized_horizontal = not c.maximized_horizontal
             c.maximized_vertical   = not c.maximized_vertical
         end),
-    -- }}}
 
-    -- {{{ My Client Keys
+    -- {{{2 My Client Keys
     -- Move client to screen
     awful.key({ modkey, "Shift" }, "#94",      awful.client.movetoscreen ),
     -- ontop and below
@@ -135,8 +128,8 @@ clientkeys = awful.util.table.join(
     awful.key({ modkey,           }, "b",      function (c) c.below = not c.below            end)
     -- }}}
 )
--- }}}
 
+-- {{{1 Tags
 -- Bind all key numbers to tags.
 -- Be careful: we use keycodes to make it works on any keyboard layout.
 -- This should map on the top row of your keyboard, usually 1 to 9.
@@ -173,5 +166,3 @@ for i = 1, 9 do
                       end
                   end))
 end
-
--- }}}
index 1bc397879b0fef9642a890d0ae237328119cc4c1..c5057c079c8ec0f048b6cedf0fce593b2cef7d28 100644 (file)
@@ -90,6 +90,7 @@ for s = 1, screen.count() do
     local right_layout = wibox.layout.fixed.horizontal()
     if s == 1 then right_layout:add(wibox.widget.systray()) end
     right_layout:add(separator)
+    right_layout:add(require("widgets/awesompd"))
     right_layout:add(require("widgets/volume_widget"))
     if utils.host == "chronos" then
         right_layout:add(separator)
diff --git a/awesome/widgets/awesompd.lua b/awesome/widgets/awesompd.lua
new file mode 100644 (file)
index 0000000..ea8ea2a
--- /dev/null
@@ -0,0 +1,47 @@
+local awesompd = require("widgets/awesompd/awesompd")
+local musicwidget
+
+musicwidget = awesompd:create() -- Create awesompd widget
+musicwidget.font = "Liberation Mono" -- Set widget font 
+musicwidget.scrolling = true -- If true, the text in the widget will be scrolled
+musicwidget.output_size = 30 -- Set the size of widget in symbols
+musicwidget.update_interval = 1 -- Set the update interval in seconds
+-- Set the folder where icons are located
+musicwidget.path_to_icons = "/home/ramsi/.config/awesome/widgets/awesompd/icons" 
+-- Set the default music format for Jamendo streams. You can change
+-- this option on the fly in awesompd itself.
+-- possible formats: awesompd.FORMAT_MP3, awesompd.FORMAT_OGG
+musicwidget.jamendo_format = awesompd.FORMAT_OGG
+-- If true, song notifications for Jamendo tracks and local tracks will also contain
+-- album cover image.
+musicwidget.show_album_cover = true
+-- Specify how big in pixels should an album cover be. Maximum value
+-- is 100.
+musicwidget.album_cover_size = 50
+-- This option is necessary if you want the album covers to be shown
+-- for your local tracks.
+musicwidget.mpd_config = "/home/ramsi/.config/mpd/mpd.conf"
+-- Specify the browser you use so awesompd can open links from
+-- Jamendo in it.
+musicwidget.browser = "mimeo"
+-- Specify decorators on the left and the right side of the
+-- widget. Or just leave empty strings if you decorate the widget
+-- from outside.
+musicwidget.ldecorator = ""
+musicwidget.rdecorator = " "
+-- Set all the servers to work with (here can be any servers you use)
+musicwidget.servers = {
+    { server = "localhost",
+        port = 6600 } }
+-- Set the buttons of the widget
+musicwidget:register_buttons({
+    { "",        awesompd.MOUSE_LEFT,        musicwidget:command_playpause()   },
+    { "Control", awesompd.MOUSE_SCROLL_UP,   musicwidget:command_prev_track()  },
+    { "Control", awesompd.MOUSE_SCROLL_DOWN, musicwidget:command_next_track()  },
+    { "",        awesompd.MOUSE_SCROLL_UP,   musicwidget:command_volume_up()   },
+    { "",        awesompd.MOUSE_SCROLL_DOWN, musicwidget:command_volume_down() },
+    { "",        awesompd.MOUSE_RIGHT,       musicwidget:command_show_menu()   },
+})
+musicwidget:run() -- After all configuration is done, run the widget
+
+return musicwidget.widget
diff --git a/awesome/widgets/awesompd/README.md b/awesome/widgets/awesompd/README.md
new file mode 100644 (file)
index 0000000..7a73137
--- /dev/null
@@ -0,0 +1,24 @@
+## Description ##
+
+This is an advanced MPD widget\client for AwesomeWM.
+
+For the detailed installation guide please see
+http://awesome.naquadah.org/wiki/Awesompd_widget .
+
+Also you can find an example of the widget configuration in the file
+rcsample.lua.
+
+## Version explanation ##
+
+Use this version with Awesome v3.5.x. If you are using Awesome
+v.3.4.x, please consider using
+[this](https://github.com/alexander-yakushev/awesompd/tree/awesome-3.4)
+version of the widget.
+
+### Changes in 1.1.0 ###
+
+* Album covers are now also shown for the local tracks (images are taken from the current track's folder)
+* When the Jamendo track is playing you can visit artist's or album's page from the Jamendo menu
+* Notification now shows the album name for the current track (for both local and Jamendo tracks)
+* A few minor modifications and bugfixes
+
diff --git a/awesome/widgets/awesompd/asyncshell.lua b/awesome/widgets/awesompd/asyncshell.lua
new file mode 100755 (executable)
index 0000000..c62284e
--- /dev/null
@@ -0,0 +1,71 @@
+-- Asynchronous io.popen for Awesome WM.
+-- How to use...
+-- ...asynchronously:
+-- asyncshell.request('wscript -Kiev', function(f) wwidget.text = f:read("*l") end)
+-- ...synchronously
+-- wwidget.text = asyncshell.demand('wscript -Kiev', 5):read("*l") or "Error"
+
+local awful = require('awful')
+
+asyncshell = {}
+asyncshell.request_table = {}
+asyncshell.id_counter = 0
+asyncshell.folder = "/tmp/asyncshell"
+asyncshell.file_template = asyncshell.folder .. '/req'
+
+-- Create a directory for asynchell response files
+os.execute("mkdir -p " .. asyncshell.folder)
+
+-- Returns next tag - unique identifier of the request
+local function next_id()
+   asyncshell.id_counter = (asyncshell.id_counter + 1) % 100000
+   return asyncshell.id_counter
+end
+
+-- Sends an asynchronous request for an output of the shell command.
+-- @param command Command to be executed and taken output from
+-- @param callback Function to be called when the command finishes
+-- @return Request ID
+function asyncshell.request(command, callback)
+   local id = next_id()
+   local tmpfname = asyncshell.file_template .. id
+   asyncshell.request_table[id] = {callback = callback}
+   local req =
+      string.format("bash -c '%s > %s; " ..
+                    'echo "asyncshell.deliver(%s)" | ' ..
+                    "awesome-client' 2> /dev/null",
+                 string.gsub(command, "'", "'\\''"), tmpfname, id, tmpfname)
+   awful.util.spawn(req, false)
+   return id
+end
+
+-- Calls the remembered callback function on the output of the shell
+-- command.
+-- @param id Request ID
+-- @param output The output file of the shell command to be delievered
+function asyncshell.deliver(id)
+   if asyncshell.request_table[id] and
+      asyncshell.request_table[id].callback then
+      local output = io.open(asyncshell.file_template .. id, 'r')
+      asyncshell.request_table[id].callback(output)
+   end
+end
+
+-- Sends a synchronous request for an output of the command. Waits for
+-- the output, but if the given timeout expires returns nil.
+-- @param command Command to be executed and taken output from
+-- @param timeout Maximum amount of time to wait for the result
+-- @return File handler on success, nil otherwise
+function asyncshell.demand(command, timeout)
+   local id = next_id()
+   local tmpfname = asyncshell.file_template .. id
+   local f = io.popen(string.format("(%s > %s; echo asyncshell_done) & " ..
+                                    "(sleep %s; echo asynchell_timeout)",
+                                    command, tmpfname, timeout))
+   local result = f:read("*line")
+   if result == "asyncshell_done" then
+      return io.open(tmpfname)
+   end
+end
+
+return asyncshell
diff --git a/awesome/widgets/awesompd/awesompd.lua b/awesome/widgets/awesompd/awesompd.lua
new file mode 100644 (file)
index 0000000..8494003
--- /dev/null
@@ -0,0 +1,1219 @@
+---------------------------------------------------------------------------
+-- @author Alexander Yakushev <yakushev.alex@gmail.com>
+-- @copyright 2010-2013 Alexander Yakushev
+-- @release v1.2.4
+---------------------------------------------------------------------------
+
+local wibox = require("wibox")
+local awful = require('awful')
+local beautiful = require('beautiful')
+local naughty = require('naughty')
+local format = string.format
+
+local module_path = (...):match ("(.+/)[^/]+$") or ""
+
+local awesompd = {}
+
+-- Function for checking icons and modules. Checks if a file exists,
+-- and if it does, returns the path to file, nil otherwise.
+function awesompd.try_load(file)
+   if awful.util.file_readable(file) then
+      return file
+   end
+end
+
+-- Function for loading modules.
+function awesompd.try_require(module)
+   if awesompd.try_load(awful.util.getdir("config") .. '/'..
+                     module_path .. module .. ".lua") then
+      return require(module_path .. module)
+   else
+      return require(module)
+   end
+end
+
+local utf8 = awesompd.try_require("utf8")
+asyncshell = awesompd.try_require("asyncshell")
+local jamendo = awesompd.try_require("jamendo")
+
+-- Constants
+awesompd.PLAYING = "Playing"
+awesompd.PAUSED = "Paused"
+awesompd.STOPPED = "MPD stopped"
+awesompd.DISCONNECTED = "Disconnected"
+
+awesompd.MOUSE_LEFT = 1
+awesompd.MOUSE_MIDDLE = 2
+awesompd.MOUSE_RIGHT = 3
+awesompd.MOUSE_SCROLL_UP = 4
+awesompd.MOUSE_SCROLL_DOWN = 5
+
+awesompd.NOTIFY_VOLUME = 1
+awesompd.NOTIFY_REPEAT = 2
+awesompd.NOTIFY_RANDOM = 3
+awesompd.NOTIFY_SINGLE = 4
+awesompd.NOTIFY_CONSUME = 5
+awesompd.FORMAT_MP3 = jamendo.FORMAT_MP3
+awesompd.FORMAT_OGG = jamendo.FORMAT_OGG
+awesompd.ESCAPE_SYMBOL_MAPPING = {}
+awesompd.ESCAPE_SYMBOL_MAPPING["&"] = "&amp;"
+-- Menus do not handle symbol escaping correctly, so they need their
+-- own mapping.
+awesompd.ESCAPE_MENU_SYMBOL_MAPPING = {}
+awesompd.ESCAPE_MENU_SYMBOL_MAPPING["&"] = "'n'"
+
+-- /// Current track variables and functions /// 
+
+-- Returns a string for the given track to be displayed in the widget
+-- and notification.
+function awesompd.get_display_name(track)
+   if track.display_name then
+      return track.display_name
+   elseif track.artist_name and track.track_name then
+      return track.artist_name .. " - " .. track.name
+   end
+end
+
+-- Returns a track display name, album name (if exists) and album
+-- release year (if exists).
+function awesompd.get_extended_info(track)
+   local result = awesompd.get_display_name(track)
+   if track.album_name then
+      result = result .. "\n" .. track.album_name
+   end
+   if track.year then
+      result = result .. "\n" .. track.year
+   end
+   return result
+end
+
+-- Returns true if the current status is either PLAYING or PAUSED
+function awesompd:playing_or_paused()
+   return self.status == awesompd.PLAYING 
+      or self.status == awesompd.PAUSED
+end
+
+-- /// Helper functions ///
+
+-- Just like awful.util.pread, but takes an argument how to read like
+-- "*line" or "*all".
+function awesompd.pread(com, mode)
+   local f = io.popen(com, 'r')
+   local result = nil
+   if f then
+      result = f:read(mode)
+      f:close()
+   end
+   return result
+end
+
+-- Slightly modified function awful.util.table.join.
+function awesompd.ajoin(buttons)
+    local result = {}
+    for i = 1, #buttons do
+        if buttons[i] then
+            for k, v in pairs(buttons[i]) do
+                if type(k) == "number" then
+                    table.insert(result, v)
+                else
+                    result[k] = v
+                end
+            end
+        end
+    end
+    return result
+ end
+
+-- Splits a given string with linebreaks into an array.
+function awesompd.split(s)
+   local l = { n = 0 }
+   if s == "" then
+      return l
+   end
+   s = s .. "\n"
+   local f = function (s) 
+                l.n = l.n + 1
+               l[l.n] = s
+            end
+   local p = "%s*(.-)%s*\n%s*"
+   s = string.gsub(s,p,f)
+   return l
+end
+
+-- Returns the given string if it is not nil or non-empty, otherwise
+-- returns nil.
+local function non_empty(s)
+   if s and s ~= "" then
+      return s
+   end
+end
+
+-- Icons
+
+function awesompd.load_icons(path)
+   awesompd.ICONS = {}
+   awesompd.ICONS.PLAY = awesompd.try_load(path .. "/play_icon.png")
+   awesompd.ICONS.PAUSE = awesompd.try_load(path .. "/pause_icon.png")
+   awesompd.ICONS.PLAY_PAUSE = awesompd.try_load(path .. "/play_pause_icon.png")
+   awesompd.ICONS.STOP = awesompd.try_load(path .. "/stop_icon.png")
+   awesompd.ICONS.NEXT = awesompd.try_load(path .. "/next_icon.png")
+   awesompd.ICONS.PREV = awesompd.try_load(path .. "/prev_icon.png")
+   awesompd.ICONS.CHECK = awesompd.try_load(path .. "/check_icon.png")
+   awesompd.ICONS.RADIO = awesompd.try_load(path .. "/radio_icon.png")
+   awesompd.ICONS.DEFAULT_ALBUM_COVER = 
+      awesompd.try_load(path .. "/default_album_cover.png")
+end
+
+-- Function that returns a new awesompd object.
+function awesompd:create()
+-- Initialization
+   local instance = {}
+   setmetatable(instance,self)
+   self.__index = self
+   instance.current_server = 1
+   instance.widget = wibox.layout.fixed.horizontal()
+   instance.notification = nil
+   instance.scroll_pos = 1
+   instance.text = ""
+   instance.to_notify = false
+   instance.album_cover = nil
+   instance.current_track = { }
+   instance.recreate_menu = true
+   instance.recreate_playback = true
+   instance.recreate_list = true
+   instance.recreate_servers = true
+   instance.recreate_options = true
+   instance.recreate_jamendo_formats = true
+   instance.recreate_jamendo_order = true
+   instance.recreate_jamendo_browse = true
+   instance.current_number = 0
+   instance.menu_shown = false
+   instance.state_volume = "NaN"
+   instance.state_repeat = "NaN"
+   instance.state_random = "NaN"
+   instance.state_single = "NaN"
+   instance.state_consume = "NaN"
+
+-- Default user options
+   instance.servers = { { server = "localhost", port = 6600 } }
+   instance.font = "Monospace"
+   instance.font_color = beautiful.fg_normal
+   instance.background = beautiful.bg_normal
+   instance.scrolling = true
+   instance.output_size = 30
+   instance.update_interval = 10
+   instance.path_to_icons = ""
+   instance.ldecorator = " "
+   instance.rdecorator = " "
+   instance.jamendo_format = awesompd.FORMAT_MP3
+   instance.show_album_cover = true
+   instance.album_cover_size = 50
+   instance.browser = "firefox"
+   
+-- Widget configuration
+   instance.widget:connect_signal("mouse::enter", function(c)
+                                                 instance:notify_track()
+                                              end)
+   instance.widget:connect_signal("mouse::leave", function(c)
+                                                 instance:hide_notification()
+                                              end)
+   return instance
+end
+
+-- Registers timers for the widget
+function awesompd:run()
+   self.load_icons(self.path_to_icons)
+   jamendo.set_current_format(self.jamendo_format)
+   if self.album_cover_size > 100 then
+      self.album_cover_size = 100
+   end
+
+   self.text_widget = wibox.widget.textbox()
+   if self.widget_icon then
+      self.icon_widget = wibox.widget.imagebox()
+      self.icon_widget:set_image(self.widget_icon)
+      self.widget:add(self.icon_widget)
+   end
+   self.widget:add(self.text_widget)
+
+   self:update_track()
+   self:check_playlists()
+
+   if scheduler then
+      scheduler.register_recurring("awesompd_scroll", 1,
+                                   function() self:update_widget() end)
+      scheduler.register_recurring("awesompd_update", self.update_interval,
+                                   function() self:update_track() end)
+   else
+      self.update_widget_timer = timer({ timeout = 1 })
+      self.update_widget_timer:connect_signal("timeout", function()
+                                                 self:update_widget()
+                                                         end)
+      self.update_widget_timer:start()
+      self.update_track_timer = timer({ timeout = self.update_interval })
+      self.update_track_timer:connect_signal("timeout", function()
+                                                self:update_track()
+                                                        end)
+      self.update_track_timer:start()
+   end
+end
+
+-- Function that registers buttons on the widget.
+function awesompd:register_buttons(buttons)
+   widget_buttons = {}
+   self.global_bindings = {}
+   for b=1, #buttons do
+      if type(buttons[b][1]) == "string" then
+         mods = { buttons[b][1] }
+      else
+         mods = buttons[b][1]
+      end
+      if type(buttons[b][2]) == "number" then 
+         -- This is a mousebinding, bind it to the widget
+         table.insert(widget_buttons, 
+                      awful.button(mods, buttons[b][2], buttons[b][3]))
+      else 
+         -- This is a global keybinding, remember it for later usage in append_global_keys
+         table.insert(self.global_bindings, awful.key(mods, buttons[b][2], buttons[b][3]))
+      end
+   end
+   self.widget:buttons(self.ajoin(widget_buttons))
+end
+
+-- Takes the current table with keybindings and adds widget's own
+-- global keybindings that were specified in register_buttons.
+-- If keytable is not specified, then adds bindings to default
+-- globalkeys table. If specified, then adds bindings to keytable and
+-- returns it.
+function awesompd:append_global_keys(keytable)
+   if keytable then
+      for i = 1, #self.global_bindings do
+         keytable = awful.util.table.join(keytable, self.global_bindings[i])
+      end
+      return keytable
+   else
+      for i = 1, #self.global_bindings do
+         globalkeys = awful.util.table.join(globalkeys, self.global_bindings[i])
+      end
+   end
+end
+
+-- /// Group of mpc command functions ///
+
+-- Returns a mpc command with all necessary parameters. Boolean
+-- human_readable argument configures if the command special
+-- formatting of the output (to be later used in parsing) should not
+-- be used.
+function awesompd:mpcquery(human_readable)
+   local result =
+      "mpc -h " .. self.servers[self.current_server].server ..
+      " -p " .. self.servers[self.current_server].port .. " "
+   if human_readable then
+      return result
+   else
+      return result ..' -f "%file%-<>-%name%-<>-%title%-<>-%artist%-<>-%album%" '
+   end
+end
+
+-- Takes a command to mpc and a hook that is provided with awesompd
+-- instance and the result of command execution.
+function awesompd:command(com,hook)
+   local file = io.popen(self:mpcquery() .. com)
+   if hook then
+      hook(self,file)
+   end
+   file:close()
+end
+
+-- Takes a command to mpc and read mode and returns the result.
+function awesompd:command_read(com, mode)
+   mode = mode or "*line"
+   self:command(com, function(_, f)
+                        result = f:read(mode)
+                     end)
+   return result
+end
+
+function awesompd:command_playpause()
+   return function()
+             self:command("toggle",self.update_track)
+          end
+end
+
+function awesompd:command_next_track()
+   return function()
+             self:command("next",self.update_track)
+          end
+end
+
+function awesompd:command_prev_track()
+   return function()
+             self:command("seek 0")
+             self:command("prev",self.update_track)
+          end
+end
+
+function awesompd:command_stop()
+   return function()
+             self:command("stop",self.update_track)
+          end
+end
+
+function awesompd:command_play_specific(n)
+   return function()
+             self:command("play " .. n,self.update_track)
+          end
+end
+
+function awesompd:command_volume_up()
+   return function()
+             self:command("volume +5")
+             self:update_track() -- Nasty! I should replace it with proper callback later.
+             self:notify_state(self.NOTIFY_VOLUME)
+          end
+end
+
+function awesompd:command_volume_down()
+   return function()
+             self:command("volume -5")
+             self:update_track()
+             self:notify_state(self.NOTIFY_VOLUME)
+          end
+end
+
+function awesompd:command_load_playlist(name)
+   return function()
+             self:command("load \"" .. name .. "\"", function() 
+                                              self.recreate_menu = true 
+                                           end)
+          end
+end
+
+function awesompd:command_replace_playlist(name)
+   return function()
+             self:command("clear")
+             self:command("load \"" .. name .. "\"")
+             self:command("play 1", self.update_track)
+          end
+end
+
+function awesompd:command_clear_playlist()
+   return function()
+             self:command("clear", self.update_track)
+             self.recreate_list = true
+             self.recreate_menu = true
+          end
+end
+
+function awesompd:command_open_in_browser(link)
+   return function()
+             if self.browser then
+                awful.util.spawn(self.browser .. " '" .. link .. "'")
+             end
+          end
+end
+
+--- Change to the previous server.
+function awesompd:command_previous_server()
+   return function()
+             servers = table.getn(self.servers)
+             if servers == 1 or servers == nil then
+                return
+             else
+                if self.current_server > 1 then
+                   self:change_server(self.current_server - 1)
+                else
+                   self:change_server(servers)
+                end
+             end
+          end
+end
+
+--- Change to the previous server.
+function awesompd:command_next_server()
+   return function()
+             servers = table.getn(self.servers)
+             if servers == 1 or servers == nil then
+                return
+             else
+                if self.current_server < servers then
+                   self:change_server(self.current_server + 1)
+                else
+                   self:change_server(1)
+                end
+             end
+          end
+end
+
+-- /// End of mpc command functions ///
+
+-- /// Menu generation functions ///
+
+function awesompd:command_show_menu()
+   return 
+   function()
+      self:hide_notification()
+      if self.recreate_menu then 
+         local new_menu = {}
+         if self.main_menu ~= nil then 
+            self.main_menu:hide() 
+         end 
+         if self.status ~= awesompd.DISCONNECTED
+         then 
+            self:check_list() 
+            self:check_playlists()
+            local jamendo_menu = { { "Search by", 
+                                     { { "Nothing (Top 100)", self:menu_jamendo_top() },
+                                       { "Artist", self:menu_jamendo_search_by(jamendo.SEARCH_ARTIST) },
+                                       { "Album", self:menu_jamendo_search_by(jamendo.SEARCH_ALBUM) },
+                                       { "Tag", self:menu_jamendo_search_by(jamendo.SEARCH_TAG) }}} }
+            local browse_menu = self:menu_jamendo_browse()
+            if browse_menu then 
+               table.insert(jamendo_menu, browse_menu)
+            end
+            table.insert(jamendo_menu, self:menu_jamendo_format())
+            table.insert(jamendo_menu, self:menu_jamendo_order())
+
+            new_menu = { { "Playback", self:menu_playback() },
+                         { "Options", self:menu_options() },
+                         { "List", self:menu_list() },
+                         { "Playlists", self:menu_playlists() },
+                         { "Jamendo", jamendo_menu } }
+         end 
+         table.insert(new_menu, { "Servers", self:menu_servers() }) 
+         self.main_menu = awful.menu({ items = new_menu, theme = { width = 300 } }) 
+         self.recreate_menu = false 
+      end 
+      self.main_menu:toggle() 
+   end 
+end
+
+-- Returns an icon for a checkbox menu item if it is checked, nil
+-- otherwise.
+function awesompd:menu_item_toggle(checked)
+   return checked and self.ICONS.CHECK or nil
+end
+
+-- Returns an icon for a radiobox menu item if it is selected, nil
+-- otherwise.
+function awesompd:menu_item_radio(selected)
+   return selected and self.ICONS.RADIO or nil
+end
+
+-- Returns the playback menu. Menu contains of:
+-- Play\Pause - always
+-- Previous - if the current track is not the first 
+-- in the list and playback is not stopped
+-- Next - if the current track is not the last 
+-- in the list and playback is not stopped
+-- Stop - if the playback is not stopped
+-- Clear playlist - always
+function awesompd:menu_playback()
+   if self.recreate_playback then
+      local new_menu = {}
+      table.insert(new_menu, { "Play\\Pause", 
+                               self:command_toggle(), 
+                               self.ICONS.PLAY_PAUSE })
+      if self:playing_or_paused() then
+         if self.list_array and self.list_array[self.current_number-1] then
+            table.insert(new_menu, 
+                         { "Prev: " .. 
+                           awesompd.protect_string(jamendo.replace_link(
+                                                      self.list_array[self.current_number - 1]),
+                                                   true),
+                        self:command_prev_track(), self.ICONS.PREV })
+         end
+         if self.list_array and self.current_number ~= #self.list_array then
+            table.insert(new_menu, 
+                         { "Next: " .. 
+                           awesompd.protect_string(jamendo.replace_link(
+                                                      self.list_array[self.current_number + 1]), 
+                                                   true), 
+                        self:command_next_track(), self.ICONS.NEXT })
+         end
+         table.insert(new_menu, { "Stop", self:command_stop(), self.ICONS.STOP })
+         table.insert(new_menu, { "", nil })
+      end
+      table.insert(new_menu, { "Clear playlist", self:command_clear_playlist() })
+      self.recreate_playback = false
+      playback_menu = new_menu
+   end
+   return playback_menu
+end
+
+-- Returns the current playlist menu. Menu consists of all elements in the playlist.
+function awesompd:menu_list()
+   if self.recreate_list then
+      local new_menu = {}
+      if self.list_array then
+         local total_count = #self.list_array 
+         local start_num = (self.current_number - 15 > 0) and self.current_number - 15 or 1
+         local end_num = (self.current_number + 15 < total_count ) and self.current_number + 15 or total_count
+         for i = start_num, end_num do
+            table.insert(new_menu, { jamendo.replace_link(self.list_array[i]),
+                                     self:command_play_specific(i),
+                                     self.current_number == i and 
+                                        (self.status == self.PLAYING and self.ICONS.PLAY or self.ICONS.PAUSE)
+                                     or nil} )
+         end
+      end
+      self.recreate_list = false
+      self.list_menu = new_menu
+   end
+   return self.list_menu
+end
+            
+-- Returns the playlists menu. Menu consists of all files in the playlist folder.
+function awesompd:menu_playlists()
+   if self.recreate_playlists then
+      local new_menu = {}
+      if #self.playlists_array > 0 then
+        for i = 1, #self.playlists_array do
+           local submenu = {}
+           submenu[1] = { "Add to current", self:command_load_playlist(self.playlists_array[i]) }
+           submenu[2] = { "Replace current", self:command_replace_playlist(self.playlists_array[i]) }
+           new_menu[i] = { self.playlists_array[i], submenu }
+        end
+        table.insert(new_menu, {"", ""}) -- This is a separator
+      end
+      table.insert(new_menu, { "Refresh", function() self:check_playlists() end })
+      self.recreate_playlists = false
+      self.playlists_menu = new_menu
+   end
+   return self.playlists_menu
+end
+
+-- Returns the server menu. Menu consists of all servers specified by user during initialization.
+function awesompd:menu_servers()
+   if self.recreate_servers then
+      local new_menu = {}
+      for i = 1, #self.servers do
+        table.insert(new_menu, {"Server: " .. self.servers[i].server .. 
+                                ", port: " .. self.servers[i].port,
+                             function() self:change_server(i) end,
+                              self:menu_item_radio(i == self.current_server)})
+      end
+      self.servers_menu = new_menu
+   end
+   return self.servers_menu
+end
+
+-- Returns the options menu. Menu works like checkboxes for it's elements.
+function awesompd:menu_options()
+   if self.recreate_options then 
+      local new_menu = { { "Repeat", self:menu_toggle_repeat(), 
+                           self:menu_item_toggle(self.state_repeat == "on")},
+                         { "Random", self:menu_toggle_random(), 
+                           self:menu_item_toggle(self.state_random == "on")},
+                         { "Single", self:menu_toggle_single(), 
+                           self:menu_item_toggle(self.state_single == "on")},
+                         { "Consume", self:menu_toggle_consume(), 
+                           self:menu_item_toggle(self.state_consume == "on")} }
+      self.options_menu = new_menu
+      self.recreate_options = false      
+   end
+   return self.options_menu
+end
+
+function awesompd:menu_toggle_random()
+   return function()
+             self:command("random",self.update_track)
+             self:notify_state(self.NOTIFY_RANDOM)
+          end
+end
+
+function awesompd:menu_toggle_repeat()
+   return function()
+             self:command("repeat",self.update_track)
+             self:notify_state(self.NOTIFY_REPEAT)
+          end
+end
+
+function awesompd:menu_toggle_single()
+   return function()
+             self:command("single",self.update_track)
+             self:notify_state(self.NOTIFY_SINGLE)
+          end
+end
+
+function awesompd:menu_toggle_consume()
+   return function()
+             self:command("consume",self.update_track)
+             self:notify_state(self.NOTIFY_CONSUME)
+          end
+end
+
+function awesompd:menu_jamendo_top()
+   return 
+   function ()
+      local track_table = jamendo.return_track_table()
+      if not track_table then
+         self:show_notification("Can't connect to Jamendo server", "Please check your network connection")
+      else
+         self:add_jamendo_tracks(track_table)
+         self:show_notification("Jamendo Top 100 by " ..
+                                jamendo.current_request_table.params.order.short_display,
+                                format("Added %s tracks to the playlist",
+                                       #track_table))
+      end
+   end
+end
+
+function awesompd:menu_jamendo_format()
+   if self.recreate_jamendo_formats then
+      local setformat =
+         function(format)
+            return function()
+                      jamendo.set_current_format(format)
+                      self.recreate_menu = true
+                      self.recreate_jamendo_formats = true
+                   end
+         end
+
+      local iscurr = 
+         function(f)
+            return jamendo.current_request_table.params.streamencoding.value
+               == f.value
+         end
+
+      local new_menu = {}
+      for _, format in pairs(jamendo.ALL_FORMATS) do
+         table.insert(new_menu, { format.display, setformat(format),
+                                  self:menu_item_radio(iscurr(format))})
+      end
+      self.recreate_jamendo_formats = false
+      self.jamendo_formats_menu = { 
+         "Format: " ..
+            jamendo.current_request_table.params.streamencoding.short_display,
+         new_menu }
+   end
+   return self.jamendo_formats_menu
+end
+
+function awesompd:menu_jamendo_browse()
+   if self.recreate_jamendo_browse and self.browser 
+      and self.current_track.unique_name then
+      local track = jamendo.get_track_by_link(self.current_track.unique_name)
+      local new_menu
+      if track then
+         local artist_link = 
+            "http://www.jamendo.com/artist/" .. track.artist_link_name
+         local album_link =
+            "http://www.jamendo.com/album/" .. track.album_id
+         new_menu = { { "Artist's page" , 
+                        self:command_open_in_browser(artist_link) },
+                      { "Album's page" ,
+                        self:command_open_in_browser(album_link) } }
+         self.jamendo_browse_menu = { "Browse on Jamendo", new_menu }
+      else
+         self.jamendo_browse_menu = nil
+      end
+   end
+   return self.jamendo_browse_menu
+end
+
+function awesompd:menu_jamendo_order()
+   if self.recreate_jamendo_order then
+      local setorder =
+         function(order)
+            return function()
+                      jamendo.set_current_order(order)
+                      self.recreate_menu = true
+                      self.recreate_jamendo_order = true
+                   end
+         end
+
+      local iscurr = 
+         function(o)
+            return jamendo.current_request_table.params.order.value
+               == o.value
+         end
+
+      local new_menu = {}
+      for _, order in pairs(jamendo.ALL_ORDERS) do
+         table.insert(new_menu, { order.display, setorder(order),
+                                  self:menu_item_radio(iscurr(order))})
+      end
+      self.recreate_jamendo_order = false
+      self.jamendo_order_menu = { 
+         "Order: " ..
+            jamendo.current_request_table.params.order.short_display,
+         new_menu }
+   end
+   return self.jamendo_order_menu
+end
+
+function awesompd:menu_jamendo_search_by(what)
+   return function()
+             local callback = 
+                function(s)
+                   local result = jamendo.search_by(what, s)
+                   if result then
+                      local track_count = #result.tracks
+                      self:add_jamendo_tracks(result.tracks)
+                      self:show_notification(format("%s \"%s\" was found",
+                                                    what.display,
+                                                    result.search_res.name),
+                                             format("Added %s tracks to the playlist",
+                                                    track_count))
+                   else
+                      self:show_notification("Search failed",
+                                             format("%s \"%s\" was not found",
+                                                    what.display, s))
+                   end
+                end
+             self:display_inputbox("Search music on Jamendo",
+                                   what.display, callback)
+          end
+end
+
+-- Checks if the current playlist has changed after the last check.
+function awesompd:check_list()
+   local bus = io.popen(self:mpcquery(true) .. "playlist")
+   local info = bus:read("*all")
+   bus:close()
+   if info ~= self.list_line then
+      self.list_line = info
+      if string.len(info) > 0 then
+        self.list_array = self.split(string.sub(info,1,string.len(info)))
+      else
+        self.list_array = {}
+      end
+      self.recreate_menu = true
+      self.recreate_list = true
+   end
+end
+
+-- Checks if the collection of playlists changed after the last check.
+function awesompd:check_playlists()
+   local bus = io.popen(self:mpcquery(true) .. "lsplaylists")
+   local info = bus:read("*all")
+   bus:close()
+   if info ~= self.playlists_line then
+      self.playlists_line = info
+      if string.len(info) > 0 then
+        self.playlists_array = self.split(info)
+      else
+        self.playlists_array = {}
+      end
+      self.recreate_menu = true
+      self.recreate_playlists = true
+   end
+end
+
+-- Changes the current server to the specified one.
+function awesompd:change_server(server_number)
+   self.current_server = server_number
+   self:hide_notification()
+   self.recreate_menu = true
+   self.recreate_playback = true
+   self.recreate_list = true
+   self.recreate_playlists = true
+   self.recreate_servers = true
+   self:update_track()
+end
+
+function awesompd:add_jamendo_tracks(track_table)
+   for i = 1, #track_table do
+      self:command("add '" .. string.gsub(track_table[i].stream, '\\/', '/') .. "'")
+   end
+   self.recreate_menu = true
+   self.recreate_list = true
+end
+
+-- /// End of menu generation functions ///
+
+function awesompd:show_notification(hint_title, hint_text, hint_image)
+   self:hide_notification()
+   self.notification = naughty.notify({ title      =  hint_title
+                                       , text       = awesompd.protect_string(hint_text)
+                                       , timeout    = 5
+                                       , position   = "top_right"
+                                        , icon       = hint_image
+                                        , icon_size  = self.album_cover_size
+                                     })
+end
+
+function awesompd:hide_notification()
+   if self.notification ~= nil then
+      naughty.destroy(self.notification)
+      self.notification = nil
+   end
+end
+
+function awesompd:notify_track()
+   if self:playing_or_paused() then
+      local caption = self.status_text
+      local nf_text = self.get_display_name(self.current_track)
+      local al_cover = nil
+      if self.show_album_cover then
+         nf_text = self.get_extended_info(self.current_track)
+         al_cover = self.current_track.album_cover
+      end
+      self:show_notification(caption, nf_text, al_cover)
+   end
+end
+
+function awesompd:notify_state(state_changed)
+   state_array = { "Volume: " .. self.state_volume ,
+                  "Repeat: " .. self.state_repeat ,
+                  "Random: " .. self.state_random ,
+                  "Single: " .. self.state_single ,
+                  "Consume: " .. self.state_consume }
+   state_header = state_array[state_changed]
+   table.remove(state_array,state_changed)
+   full_state = state_array[1]
+   for i = 2, #state_array do
+      full_state = full_state .. "\n" .. state_array[i]
+   end
+   self:show_notification(state_header, full_state)
+end
+
+function awesompd:wrap_output(text)
+   return format('<span font="%s" color="%s" background="%s">%s%s%s</span>',
+                 self.font, self.font_color, self.background,
+                 (text == "" and "" or self.ldecorator), awesompd.protect_string(text),
+                 (text == "" and "" or self.rdecorator))
+end
+
+-- This function actually sets the text on the widget.
+function awesompd:set_text(text)
+   self.text_widget:set_markup(self:wrap_output(text))
+end
+
+function awesompd.find_pattern(text, pattern, start)
+   return utf8.sub(text, string.find(text, pattern, start))
+end
+
+-- Scroll the given text by the current number of symbols.
+function awesompd:scroll_text(text)
+   local result = text
+   if self.scrolling then
+      if self.output_size < utf8.len(text) then
+         text = text .. " - "
+         if self.scroll_pos + self.output_size - 1 > utf8.len(text) then
+            result = utf8.sub(text, self.scroll_pos)
+            result = result .. utf8.sub(text, 1, self.scroll_pos + self.output_size - 1 - utf8.len(text))
+            self.scroll_pos = self.scroll_pos + 1
+            if self.scroll_pos > utf8.len(text) then
+               self.scroll_pos = 1
+            end
+         else
+            result = utf8.sub(text, self.scroll_pos, self.scroll_pos + self.output_size - 1)
+            self.scroll_pos = self.scroll_pos + 1
+         end
+      end
+   end
+   return result
+end
+
+-- This function is called every second.
+function awesompd:update_widget()
+   self:set_text(self:scroll_text(self.text))
+   self:check_notify()
+end
+
+-- This function is called by update_track each time content of
+-- the widget must be changed.
+function awesompd:update_widget_text()
+   if self:playing_or_paused() then
+      self.text = self.get_display_name(self.current_track)
+   else
+      self.text = self.status
+   end
+end
+
+-- Checks if notification should be shown and shows if positive.
+function awesompd:check_notify()
+   if self.to_notify then
+      self:notify_track()
+      self.to_notify = false
+   end
+end
+
+function awesompd:notify_connect()
+   self:show_notification("Connected", "Connection established to " .. self.servers[self.current_server].server ..
+                " on port " .. self.servers[self.current_server].port)
+end
+
+function awesompd:notify_disconnect()
+   self:show_notification("Disconnected", "Cannot connect to " .. self.servers[self.current_server].server ..
+                " on port " .. self.servers[self.current_server].port)
+end
+
+function awesompd:update_track(file)
+   local file_exists = (file ~= nil)
+   if not file_exists then
+      file = io.popen(self:mpcquery())
+   end
+   local track_line = file:read("*line")
+   local status_line = file:read("*line")
+   local options_line = file:read("*line")
+   if not file_exists then
+      file:close()
+   end
+
+   if not track_line or string.len(track_line) == 0 then
+      if self.status ~= awesompd.DISCONNECTED then
+        self:notify_disconnect()
+        self.recreate_menu = true
+         self.status = awesompd.DISCONNECTED
+         self.current_track = { }
+         self:update_widget_text()
+      end
+   else
+      if self.status == awesompd.DISCONNECTED then
+        self:notify_connect()
+        self.recreate_menu = true
+         self:update_widget_text()
+      end
+      if string.find(track_line,"volume:") or string.find(track_line,"Updating DB") then
+        if self.status ~= awesompd.STOPPED then
+            self.status = awesompd.STOPPED
+           self.current_number = 0
+           self.recreate_menu = true
+           self.recreate_playback = true
+           self.recreate_list = true
+            self.album_cover = nil
+            self.current_track = { }
+            self:update_widget_text()
+        end
+         self:update_state(track_line)
+      else
+         self:update_state(options_line)
+         local _, _, new_file, station, title, artist, album =
+            string.find(track_line, "(.*)%-<>%-(.*)%-<>%-(.*)%-<>%-(.*)%-<>%-(.*)")
+         local display_name, force_update = artist .. " - " .. title, false
+         -- The following code checks if the current track is an
+         -- Internet link. Internet radios change tracks, but the
+         -- current file stays the same, so we should manually compare
+         -- its title.
+         if string.match(new_file, "http://") and
+            -- The following line is awful. This needs to be replaced ASAP.
+            not string.match(new_file, "http://storage%-new%.newjamendo%.com") then
+            album = non_empty(station) or ""
+            display_name = non_empty(title) or new_file
+            if display_name ~= self.current_track.display_name then
+               force_update = true
+            end
+         end
+        if new_file ~= self.current_track.unique_name or force_update then
+            self.current_track = jamendo.get_track_by_link(new_file)
+            if not self.current_track then
+               self.current_track = { display_name = display_name,
+                                      album_name = album }
+            end
+            self.current_track.unique_name = new_file
+            if self.show_album_cover then
+               self.current_track.album_cover = self:get_cover(new_file)
+            end
+           self.to_notify = true
+           self.recreate_menu = true
+           self.recreate_playback = true
+           self.recreate_list = true
+           self.current_number = tonumber(self.find_pattern(status_line,"%d+"))
+            self:update_widget_text()
+
+            -- If the track is not the last, asynchronously download
+            -- the cover for the next track.
+            if self.list_array and self.current_number ~= #self.list_array then
+               -- Get the link (in case it is Jamendo stream) to the next track
+               local next_track = 
+                  self:command_read('playlist -f "%file%" | head -' .. 
+                                    self.current_number + 1 .. ' | tail -1', "*line")
+               jamendo.try_get_cover_async(next_track)
+            end
+        end
+        local tmp_pst = string.find(status_line,"%d+%:%d+%/")
+        local progress = self.find_pattern(status_line,"%#%d+/%d+") .. " " .. string.sub(status_line,tmp_pst)
+         local new_status = awesompd.PLAYING
+        if string.find(status_line,"paused") then
+            new_status = awesompd.PAUSED
+        end
+        if new_status ~= self.status then
+           self.to_notify = true
+           self.recreate_list = true
+            self.status = new_status
+            self:update_widget_text()
+        end
+        self.status_text = self.status .. " " .. progress
+      end
+   end
+end
+
+function awesompd:update_state(state_string)
+   self.state_volume = self.find_pattern(state_string,"%d+%% ")
+   if string.find(state_string,"repeat: on") then
+      self.state_repeat = self:check_set_state(self.state_repeat, "on")
+   else
+      self.state_repeat = self:check_set_state(self.state_repeat, "off")
+   end
+   if string.find(state_string,"random: on") then
+      self.state_random = self:check_set_state(self.state_random, "on")
+   else
+      self.state_random = self:check_set_state(self.state_random, "off")
+   end
+   if string.find(state_string,"single: on") then
+      self.state_single = self:check_set_state(self.state_single, "on")
+   else
+      self.state_single = self:check_set_state(self.state_single, "off")
+   end
+   if string.find(state_string,"consume: on") then
+      self.state_consume = self:check_set_state(self.state_consume, "on")
+   else
+      self.state_consume = self:check_set_state(self.state_consume, "off")
+   end
+end
+
+function awesompd:check_set_state(statevar, val)
+   if statevar ~= val then
+      self.recreate_menu = true
+      self.recreate_options = true
+   end
+   return val
+end
+
+function awesompd:run_prompt(welcome,hook)
+   awful.prompt.run({ prompt = welcome },
+                   self.promptbox[mouse.screen].widget,
+                   hook)
+end
+
+-- Replaces control characters with escaped ones.
+-- for_menu - defines if the special escable table for menus should be
+-- used.
+function awesompd.protect_string(str, for_menu)
+   if for_menu then
+      return utf8.replace(str, awesompd.ESCAPE_MENU_SYMBOL_MAPPING)
+   else
+      return utf8.replace(str, awesompd.ESCAPE_SYMBOL_MAPPING)
+   end
+end
+
+-- Initialize the inputbox.
+function awesompd:init_inputbox()
+   local width = 200
+   local height = 30
+   local border_color = beautiful.bg_focus or '#535d6c'
+   local margin = 4
+   local wbox = wibox({ name = "awmpd_ibox", height = height , width = width, 
+                        border_color = border_color, border_width = 1 })
+   local ws = screen[mouse.screen].workarea
+
+   wbox.screen = mouse.screen
+   wbox.ontop = true
+
+   local wprompt = awful.widget.prompt()
+   local wtbox = wibox.widget.textbox()
+   local wtmarginbox = wibox.layout.margin(wtbox, margin)
+   local tw, th = wtbox:fit(-1, -1)
+   wbox:geometry({ x = ws.width - width - 5, y = 25,
+                   width = 200, height = th * 2 + margin})
+   local layout = wibox.layout.flex.vertical()
+   layout:add(wtmarginbox)
+   layout:add(wprompt)
+   wbox:set_widget(layout)
+   self.inputbox = { wibox = wbox,
+                     title = wtbox,
+                     prompt = wprompt }
+end
+
+-- Displays an inputbox on the screen (looks like naughty with prompt).
+-- title_text - bold text on the first line
+-- prompt_text - preceding text on the second line
+-- hook - function that will be called with input data
+-- Use it like this:
+-- self:display_inputbox("Search music on Jamendo", "Artist", print)
+function awesompd:display_inputbox(title_text, prompt_text, hook)
+   if not self.inputbox then
+      self:init_inputbox()
+   end
+   if self.inputbox.wibox.visible then -- Inputbox already exists, replace it
+      keygrabber.stop()
+   end
+
+   local exe_callback = function(s)
+                           hook(s)
+                           self.inputbox.wibox.visible = false
+                        end
+   local done_callback = function()
+                            self.inputbox.wibox.visible = false
+                         end
+   self.inputbox.title:set_markup("<b>" .. title_text .. "</b>")
+   awful.prompt.run( { prompt = " " .. prompt_text .. ": ", bg_cursor = "#222222" }, 
+                     self.inputbox.prompt.widget,
+                     exe_callback, nil, nil, nil, done_callback, nil, nil)
+   self.inputbox.wibox.visible = true
+end
+
+-- Gets the cover for the given track. First looks in the Jamendo
+-- cache. If the track is not a Jamendo stream, looks in local
+-- folders. If there is no cover art either returns the default album
+-- cover.
+function awesompd:get_cover(track)
+   return jamendo.try_get_cover(track) or
+   self:try_get_local_cover(track) or self.ICONS.DEFAULT_ALBUM_COVER
+end
+
+-- Tries to find an album cover for the track that is currently
+-- playing.
+function awesompd:try_get_local_cover(current_file)
+   if self.mpd_config then
+      local result
+      -- First find the music directory in MPD configuration file
+      local _, _, music_folder = string.find(
+         self.pread('cat ' .. self.mpd_config .. ' | grep -v "#" | grep music_directory', "*line"),
+         'music_directory%s+"(.+)"')
+      music_folder = music_folder .. "/"
+      
+      -- If the music_folder is specified with ~ at the beginning,
+      -- replace it with user home directory
+      if string.sub(music_folder, 1, 1) == "~" then
+         local user_folder = self.pread("echo ~", "*line")
+         music_folder = user_folder .. string.sub(music_folder, 2)
+      end
+
+      -- Get the path to the file currently playing.
+      local _, _, current_file_folder = string.find(current_file, '(.+%/).*')
+
+      -- Check if the current file is not some kind of http stream or
+      -- Spotify track (like spotify:track:5r65GeuIoebfJB5sLcuPoC)
+      if not current_file_folder or string.match(current_file, "%w+://") then
+          return -- Let the default image to be the cover
+      end
+
+      local folder = music_folder .. current_file_folder
+      
+      -- Get all images in the folder. Also escape occasional single
+      -- quotes in folder name.
+      local request = format("ls '%s' | grep -P '\\.jpg|\\.png|\\.gif|\\.jpeg'",
+                             string.gsub(folder, "'", "'\\''"))
+
+      local covers = self.pread(request, "*all")
+      local covers_table = self.split(covers)
+      
+      if covers_table.n > 0 then
+         result = folder .. covers_table[1]
+         if covers_table.n > 1 then
+            -- Searching for front cover with grep because Lua regular
+            -- expressions suck:[
+            local front_cover = 
+               self.pread('echo "' .. covers .. 
+                          '" | grep -P -i "cover|front|folder|albumart" | head -n 1', "*line")
+            if front_cover then
+               result = folder .. front_cover
+            end
+         end
+      end
+      return result
+   end   
+end
+
+-- /// Deprecated, left for some backward compatibility in
+-- configuration ///
+
+function awesompd:command_toggle()
+   return self:command_playpause()
+end
+
+return awesompd
diff --git a/awesome/widgets/awesompd/icons/check_icon.png b/awesome/widgets/awesompd/icons/check_icon.png
new file mode 100644 (file)
index 0000000..dc5cba1
Binary files /dev/null and b/awesome/widgets/awesompd/icons/check_icon.png differ
diff --git a/awesome/widgets/awesompd/icons/default_album_cover.png b/awesome/widgets/awesompd/icons/default_album_cover.png
new file mode 100644 (file)
index 0000000..b952ba4
Binary files /dev/null and b/awesome/widgets/awesompd/icons/default_album_cover.png differ
diff --git a/awesome/widgets/awesompd/icons/next_icon.png b/awesome/widgets/awesompd/icons/next_icon.png
new file mode 100644 (file)
index 0000000..918ff89
Binary files /dev/null and b/awesome/widgets/awesompd/icons/next_icon.png differ
diff --git a/awesome/widgets/awesompd/icons/pause_icon.png b/awesome/widgets/awesompd/icons/pause_icon.png
new file mode 100644 (file)
index 0000000..83a92ee
Binary files /dev/null and b/awesome/widgets/awesompd/icons/pause_icon.png differ
diff --git a/awesome/widgets/awesompd/icons/play_icon.png b/awesome/widgets/awesompd/icons/play_icon.png
new file mode 100644 (file)
index 0000000..83230d1
Binary files /dev/null and b/awesome/widgets/awesompd/icons/play_icon.png differ
diff --git a/awesome/widgets/awesompd/icons/play_pause_icon.png b/awesome/widgets/awesompd/icons/play_pause_icon.png
new file mode 100644 (file)
index 0000000..e54beb6
Binary files /dev/null and b/awesome/widgets/awesompd/icons/play_pause_icon.png differ
diff --git a/awesome/widgets/awesompd/icons/prev_icon.png b/awesome/widgets/awesompd/icons/prev_icon.png
new file mode 100644 (file)
index 0000000..8e16381
Binary files /dev/null and b/awesome/widgets/awesompd/icons/prev_icon.png differ
diff --git a/awesome/widgets/awesompd/icons/radio_icon.png b/awesome/widgets/awesompd/icons/radio_icon.png
new file mode 100644 (file)
index 0000000..ffdf065
Binary files /dev/null and b/awesome/widgets/awesompd/icons/radio_icon.png differ
diff --git a/awesome/widgets/awesompd/icons/stop_icon.png b/awesome/widgets/awesompd/icons/stop_icon.png
new file mode 100644 (file)
index 0000000..6bef770
Binary files /dev/null and b/awesome/widgets/awesompd/icons/stop_icon.png differ
diff --git a/awesome/widgets/awesompd/jamendo.lua b/awesome/widgets/awesompd/jamendo.lua
new file mode 100644 (file)
index 0000000..3851e0a
--- /dev/null
@@ -0,0 +1,518 @@
+---------------------------------------------------------------------------
+-- @author Alexander Yakushev <yakushev.alex@gmail.com>
+-- @copyright 2011-2013 Alexander Yakushev
+-- @release v1.2.4
+---------------------------------------------------------------------------
+
+-- Grab environment
+local awful = require('awful')
+
+local jamendo = {}
+
+-- UTILITY STUFF
+-- Checks whether file specified by filename exists.
+local function file_exists(filename, mode)
+   mode = mode or 'r'
+   f = io.open(filename, mode)
+   if f then
+      f:close()
+      return true
+   else
+      return false
+   end
+end
+
+local function str_interpose(coll, sep)
+   if #coll == 0 then
+      return ""
+   end
+   local result = coll[1]
+   for i = 2, #coll do
+      result = result .. sep .. coll[i]
+   end
+   print(result)
+   return result
+end
+
+-- Global variables
+jamendo.FORMAT_MP3 = { display = "MP3 (128k)",
+                       short_display = "MP3",
+                       value = "mp31" }
+jamendo.FORMAT_OGG = { display = "Ogg Vorbis (q4)",
+                       short_display = "Ogg",
+                       value = "ogg2" }
+jamendo.ORDER_RATINGDAILY = { display = "Daily rating",
+                              short_display = "daily rating",
+                              value = "ratingday_desc" }
+jamendo.ORDER_RATINGWEEKLY = { display = "Weekly rating",
+                               short_display = "weekly rating",
+                               value = "ratingweek_desc" }
+jamendo.ORDER_RATINGTOTAL = { display = "All time rating",
+                              short_display = "all time rating",
+                              value = "ratingtotal_desc" }
+jamendo.ORDER_RANDOM = { display = "Random",
+                         short_display = "random",
+                         value = "random_desc" }
+jamendo.ORDER_RELEVANCE = { display = "None (consecutive)",
+                            short_display = "none",
+                            value = "searchweight_desc" }
+jamendo.SEARCH_ARTIST = { display = "Artist",
+                          unit = "artist",
+                          value = "artist_id" }
+jamendo.SEARCH_ALBUM = { display = "Album",
+                         unit = "album",
+                         value = "album_id" }
+jamendo.SEARCH_TAG = { display = "Tag",
+                       unit = "tag",
+                       value = "tag_id" }
+jamendo.ALL_FORMATS = { jamendo.FORMAT_MP3, jamendo.FORMAT_OGG }
+jamendo.ALL_ORDERS = { ORDER_RELEVANCE, ORDER_RANDOM, ORDER_RATINGDAILY,
+                       ORDER_RATINGWEEKLY, ORDER_RATINGTOTAL }
+
+jamendo.current_request_table = { unit = "track",
+                                  fields = {"id", "artist_url", "artist_name", "name", 
+                                            "stream", "album_image", "album_name" },
+                                  joins = { "track_album", "album_artist" },
+                                  params = { streamencoding = jamendo.FORMAT_MP3,
+                                             order = jamendo.ORDER_RATINGWEEKLY,
+                                             n = 100 }}
+
+-- Local variables
+local jamendo_list = {}
+local cache_file = awful.util.getdir ("cache").."/jamendo_cache"
+local cache_header = "[version=1.1.0]"
+local album_covers_folder = awful.util.getdir("cache") .. "/jamendo_covers/"
+local default_mp3_stream = nil
+local search_template = { fields = { "id", "name" },
+                          joins = {},
+                          params = { order = ORDER_RELEVANCE,
+                                     n = 1}}
+
+-- DEPRECATED. Will be removed in the next major release.
+-- Returns default stream number for MP3 format. Requests API for it
+-- not more often than every hour.
+local function get_default_mp3_stream()
+   if not default_mp3_stream or 
+      (os.time() - default_mp3_stream.last_checked) > 3600 then
+      local trygetlink = 
+         jamendo.perform_request("echo $(curl -w %{redirect_url} " .. 
+                                 "'http://api.jamendo.com/get2/stream/track/redirect/" .. 
+                                 "?streamencoding="..jamendo.FORMAT_MP3.value.."&id=729304')")
+         local _, _, prefix = string.find(trygetlink, "stream(%d+)%.jamendo%.com")
+         default_mp3_stream = { id = prefix, last_checked = os.time() }
+   end
+   return default_mp3_stream.id
+end
+
+-- Returns the track ID from the given link to Jamendo stream. If the
+-- given text is not the Jamendo stream returns nil.
+function jamendo.get_id_from_link(link)
+   local _, _, id = string.find(link,"storage%-new.newjamendo.com/%?trackid=(%d+)")
+   return id
+end
+
+-- Returns link to music stream for the given track ID. Uses MP3
+-- format and the default stream for it.
+local function get_link_by_id(id)
+   -- This function is subject to change in the future.
+   return string.format("http://storage-new.newjamendo.com?trackid=%s&format=mp31&u=0", id)
+end
+
+-- -- Returns the album id for given music stream.
+-- function get_album_id_by_link(link)
+--    local id = get_id_from_link(link, true)
+--    if id and jamendo_list[id] then
+--       return jamendo_list[id].album_id
+--    end
+-- end
+
+-- Returns the track table for the given music stream.
+function jamendo.get_track_by_link(link)
+   local id = jamendo.get_id_from_link(link, true)
+   if id and jamendo_list[id] then
+      return jamendo_list[id]
+   end
+end
+
+-- If a track is actually a Jamendo stream, replace it with normal
+-- track name.
+function jamendo.replace_link(track_name)
+   local track = jamendo.get_track_by_link(track_name)
+   if track then
+      return track.display_name
+   else
+      return track_name
+   end
+end
+
+-- Returns table of track IDs, names and other things based on the
+-- request table.
+function jamendo.return_track_table(request_table)
+   local req_string = jamendo.form_request(request_table)
+   local response = jamendo.perform_request(req_string)
+   if not response then
+      return nil -- Bad internet connection
+   end
+   local parse_table = jamendo.parse_json(response)
+   for i = 1, #parse_table do
+      if parse_table[i].stream == "" then
+         -- Some songs don't have Ogg stream, use MP3 instead
+         parse_table[i].stream = get_link_by_id(parse_table[i].id)
+      end
+      _, _, parse_table[i].artist_link_name = 
+         string.find(parse_table[i].artist_url, "\\/artist\\/(.+)")
+      -- Remove Jamendo escape slashes
+      parse_table[i].artist_name =
+         string.gsub(parse_table[i].artist_name, "\\/", "/")
+      parse_table[i].name = string.gsub(parse_table[i].name, "\\/", "/")
+
+      parse_table[i].display_name = 
+         parse_table[i].artist_name .. " - " .. parse_table[i].name
+      -- Do Jamendo a favor, extract album_id for the track yourself
+      -- from album_image link :)
+      local _, _, album_id = 
+         string.find(parse_table[i].album_image, "\\/(%d+)\\/covers")
+      parse_table[i].album_id = album_id or 0
+      -- Save fetched tracks for further caching
+      jamendo_list[parse_table[i].id] = parse_table[i]
+   end
+   jamendo.save_cache()
+   return parse_table
+end
+
+-- Generates the request to Jamendo API based on provided request
+-- table. If request_table is nil, uses current_request_table instead.
+-- For all values that do not exist in request_table use ones from
+-- current_request_table.
+-- return - HTTP-request
+function jamendo.form_request(request_table)
+   local curl_str = "curl -A 'Mozilla/4.0' -fsm 5 \"%s\""
+   local url = "http://api.jamendo.com/get2/%s/%s/json/%s/?%s"
+   request_table = request_table or jamendo.current_request_table
+   
+   local fields = request_table.fields or jamendo.current_request_table.fields
+   local joins = request_table.joins or jamendo.current_request_table.joins
+   local unit = request_table.unit or jamendo.current_request_table.unit
+   
+   -- Form fields string (like field1+field2+fieldN)
+   local f_string = str_interpose(fields, "+")
+   -- Form joins string
+   local j_string = str_interpose(joins, "+")
+
+   local params = {}
+   -- If parameters where supplied in request_table, add them to the
+   -- parameters in current_request_table.
+   if request_table.params and 
+      request_table.params ~= jamendo.current_request_table.params then
+      -- First fill params with current_request_table parameters
+      for k, v in pairs(jamendo.current_request_table.params) do
+         params[k] = v
+      end
+      -- Then add and overwrite them with request_table parameters
+      for k, v in pairs(request_table.params) do
+         params[k] = v
+      end
+   else -- Or just use current_request_table.params
+      params = jamendo.current_request_table.params
+   end
+   -- Form parameter string (like param1=value1&param2=value2)
+   local param_string = ""
+   for k, v in pairs(params) do
+      if type(v) == "table" then
+         v = v.value
+      end
+      v = string.gsub(v, " ", "+")
+      param_string = param_string .. "&" .. k .. "=" .. v
+   end
+
+   return string.format(curl_str, string.format(url, f_string, unit, j_string, param_string))
+end
+
+-- Primitive function for parsing Jamendo API JSON response.  Does not
+-- support arrays. Supports only strings and numbers as values.
+-- Provides basic safety (correctly handles special symbols like comma
+-- and curly brackets inside strings)
+-- text - JSON text
+function jamendo.parse_json(text)
+   local parse_table = {}
+   local block = {}
+   local i = 0
+   local inblock = false
+   local instring = false
+   local curr_key = nil
+   local curr_val = nil
+   while i and i < string.len(text) do
+      if not inblock then -- We are not inside the block, find next {
+         i = string.find(text, "{", i+1)
+         inblock = true
+         block = {}
+      else
+         if not curr_key then -- We haven't found key yet
+            if not instring then -- We are not in string, check for more tags
+               local j = string.find(text, '"', i+1)
+               local k = string.find(text, '}', i+1)
+               if j and j < k then -- There are more tags in this block
+                  i = j
+                  instring = true
+               else -- Block is over, we found its ending
+                  i = k
+                  inblock = false
+                  table.insert(parse_table, block)
+               end
+            else -- We are in string, find its ending
+               _, i, curr_key = string.find(text,'(.-[^%\\])"', i+1)
+               instring = false
+            end
+         else -- We have the key, let's find the value
+            if not curr_val then -- Value is not found yet
+               if not instring then -- Not in string, check if value is string
+                  local j = string.find(text, '"', i+1)
+                  local k = string.find(text, '[,}]', i+1)
+                  if j and j < k then -- Value is string
+                     i = j
+                     instring = true
+                  else -- Value is int
+                     _, i, curr_val = string.find(text,'(%d+)', i+1)
+                  end
+               else -- We are in string, find its ending
+                  local j = string.find(text, '"', i+1)
+                  if j == i+1 then -- String is empty
+                     i = j
+                     curr_val = ""
+                  else
+                     _, i, curr_val = string.find(text,'(.-[^%\\])"', i+1)
+                     curr_val = jamendo.utf8_codes_to_symbols(curr_val)
+                  end
+                  instring = false
+               end
+            else -- We have both key and value, add it to table
+               block[curr_key] = curr_val
+               curr_key = nil
+               curr_val = nil
+            end
+         end
+      end
+   end
+   return parse_table
+end
+
+-- Jamendo returns Unicode symbols as \uXXXX. Lua does not transform
+-- them into symbols so we need to do it ourselves.
+function jamendo.utf8_codes_to_symbols (s)
+   local hexnums = "[%dabcdefABCDEF]"
+   local pattern = string.format("\\u(%s%s%s%s?)", 
+                                 hexnums, hexnums, hexnums, hexnums)
+   local decode = function(code)
+                     code = tonumber(code, 16)
+                     if code < 128 then -- one-byte symbol
+                        return string.char(code)
+                     elseif code < 2048 then -- two-byte symbol
+                        -- Grab high and low bytes
+                        local hi = math.floor(code / 64)
+                        local lo = math.fmod(code, 64)
+                        -- Return symbol as \hi\lo
+                        return string.char(hi + 192, lo + 128)
+                     elseif code < 65536 then
+                        -- Grab high, middle and low bytes
+                        local hi = math.floor(code / 4096)
+                        local leftover = code - hi * 4096
+                        local mi = math.floor(leftover / 64)
+                        leftover = leftover - mi * 64
+                        local lo = math.fmod(leftover, 64)
+                        -- Return symbol as \hi\mi\lo
+                        return string.char(hi + 224, mi + 160, lo + 128)
+                     elseif code < 1114112 then
+                        -- Grab high, highmiddle, lowmiddle and low bytes
+                        local hi = math.floor(code / 262144)
+                        local leftover = code - hi * 262144
+                        local hm = math.floor(leftover / 4096)
+                        leftover = leftover - hm * 4096
+                        local lm = math.floor(leftover / 64)
+                        local lo = math.fmod(leftover, 64)
+                        -- Return symbol as \hi\hm\lm\lo
+                        return string.char(hi + 240, hm + 128, lm + 128, lo + 128)
+                     else -- It is not Unicode symbol at all
+                        return tostring(code)
+                     end
+                  end
+   return string.gsub(s, pattern, decode)
+end
+
+-- Retrieves mapping of track IDs to track names and album IDs to
+-- avoid redundant queries when Awesome gets restarted.
+local function retrieve_cache()
+   local bus = io.open(cache_file)
+   local track = {}
+   if bus then
+      local header = bus:read("*line")
+      if header == cache_header then 
+         for l in bus:lines() do
+            local _, _, id, artist_link_name, album_name, album_id, track_name = 
+               string.find(l,"(%d+)-([^-]+)-([^-]+)-(%d+)-(.+)")
+            track = {}
+            track.id = id
+            track.artist_link_name = string.gsub(artist_link_name, '\\_', '-')
+            track.album_name = string.gsub(album_name, '\\_', '-')
+            track.album_id = album_id
+            track.display_name = track_name
+            jamendo_list[id] = track
+         end
+      else 
+         -- We encountered an outdated version of the cache
+         -- file. Let's just remove it.
+         awful.util.spawn("rm -f " .. cache_file)
+      end
+   end
+end
+
+-- Saves track IDs to track names and album IDs mapping into the cache
+-- file.
+function jamendo.save_cache()
+   local bus = io.open(cache_file, "w")
+   bus:write(cache_header .. "\n")
+   for id,track in pairs(jamendo_list) do
+      bus:write(string.format("%s-%s-%s-%s-%s\n", id, 
+                              string.gsub(track.artist_link_name, '-', '\\_'),
+                              string.gsub(track.album_name, '-', '\\_'),
+                              track.album_id, track.display_name))
+   end
+   bus:flush()
+   bus:close()
+end
+
+-- Retrieve cache on initialization
+retrieve_cache()
+
+-- Returns a filename of the album cover and formed wget request that
+-- downloads the album cover for the given track name. If the album
+-- cover already exists returns nil as the second argument.
+function jamendo.fetch_album_cover_request(track_id)
+   local track = jamendo_list[track_id]
+   local album_id = track.album_id
+
+   if album_id == 0 then -- No cover for tracks without album!
+      return nil
+   end
+   local file_path = album_covers_folder .. album_id .. ".jpg"
+
+   if not file_exists(file_path) then -- We need to download it  
+      -- First check if cache directory exists
+      f = io.popen('test -d ' .. album_covers_folder .. ' && echo t')
+      if f:read("*line") ~= 't' then
+         awful.util.spawn("mkdir " .. album_covers_folder)
+      end
+      f:close()
+      
+      if not track.album_image then      -- Wow! We have album_id, but
+         local a_id = tostring(album_id) --don't have album_image. Well,
+         local prefix =                  --it happens.
+            string.sub(a_id, 1, #a_id - 3)
+         track.album_image = 
+            string.format("http://imgjam.com/albums/s%s/%s/covers/1.100.jpg",
+                          prefix == "" and 0 or prefix, a_id)
+      end
+      
+      return file_path, string.format("wget %s -O %s 2> /dev/null",
+                                      track.album_image, file_path)
+   else -- Cover already downloaded, return its filename and nil
+      return file_path, nil
+   end
+end
+
+-- Returns a file containing an album cover for given track id.  First
+-- searches in the cache folder. If file is not there, fetches it from
+-- the Internet and saves into the cache folder.
+function jamendo.get_album_cover(track_id)
+   local file_path, fetch_req = jamendo.fetch_album_cover_request(track_id)
+   if fetch_req then
+      local f = io.popen(fetch_req)
+      f:close()
+
+      -- Let's check if file is finally there, just in case
+      if not file_exists(file_path) then
+         return nil
+      end
+   end
+   return file_path
+end
+
+-- Same as get_album_cover, but downloads (if necessary) the cover
+-- asynchronously.
+function jamendo.get_album_cover_async(track_id)
+   local file_path, fetch_req = jamendo.fetch_album_cover_request(track_id)
+   if fetch_req then
+      asyncshell.request(fetch_req)
+   end
+end
+
+-- Checks if track_name is actually a link to Jamendo stream. If true
+-- returns the file with album cover for the track.
+function jamendo.try_get_cover(track_name)
+   local id = jamendo.get_id_from_link(track_name)
+   if id then
+      return jamendo.get_album_cover(id)
+   end
+end
+
+-- Same as try_get_cover, but calls get_album_cover_async inside.
+function jamendo.try_get_cover_async(track_name)
+   local id = jamendo.get_id_from_link(track_name)
+   if id then
+      return jamendo.get_album_cover_async(id)
+   end
+end
+
+-- Returns the track table for given query and search method.
+-- what - search method - SEARCH_ARTIST, ALBUM or TAG
+-- s - string to search
+function jamendo.search_by(what, s)
+   -- Get a default request and set unit and query
+   local req = search_template
+   req.unit = what.unit
+   req.params.searchquery = s
+   local resp = jamendo.perform_request(jamendo.form_request(req))
+   if resp then
+      local search_res = jamendo.parse_json(resp)[1]
+      
+      if search_res then
+         -- Now when we got the search result, find tracks filtered by
+         -- this result.
+         local params = {}
+         params[what.value] = search_res.id
+         req = { params = params }
+         local track_table = jamendo.return_track_table(req)
+         return { search_res = search_res, tracks = track_table }
+      end
+   end
+end
+
+-- Executes request_string with io.popen and returns the response.
+function jamendo.perform_request(request_string)
+   local bus = assert(io.popen(request_string,'r'))
+   local response = bus:read("*all")
+   bus:close()
+   -- Curl with popen can sometimes fail to fetch data when the
+   -- connection is slow. Let's try again if it fails.
+   if #response == 0 then
+      bus = assert(io.popen(request_string,'r'))
+      response = bus:read("*all")
+      bus:close()
+      -- If it still can't read anything, return nil
+      if #response ~= 0 then
+         return nil
+      end
+   end
+   return response
+end
+
+-- Sets default streamencoding in current_request_table.
+function jamendo.set_current_format(format)
+   jamendo.current_request_table.params.streamencoding = format
+end
+
+-- Sets default order in current_request_table.
+function jamendo.set_current_order(order)
+   jamendo.current_request_table.params.order = order
+end
+
+return jamendo
diff --git a/awesome/widgets/awesompd/rcsample.lua b/awesome/widgets/awesompd/rcsample.lua
new file mode 100644 (file)
index 0000000..18a7a23
--- /dev/null
@@ -0,0 +1,508 @@
+-- Standard awesome library
+local gears = require("gears")
+local awful = require("awful")
+awful.rules = require("awful.rules")
+require("awful.autofocus")
+-- Widget and layout library
+local wibox = require("wibox")
+-- Theme handling library
+local beautiful = require("beautiful")
+-- Notification library
+local naughty = require("naughty")
+local menubar = require("menubar")
+
+-- {{{ Error handling
+-- Check if awesome encountered an error during startup and fell back to
+-- another config (This code will only ever execute for the fallback config)
+if awesome.startup_errors then
+   naughty.notify({ preset = naughty.config.presets.critical,
+                    title = "Oops, there were errors during startup!",
+                    text = awesome.startup_errors })
+end
+
+-- Handle runtime errors after startup
+do
+   local in_error = false
+   awesome.connect_signal("debug::error", function (err)
+                             -- Make sure we don't go into an endless error loop
+                             if in_error then return end
+                             in_error = true
+
+                             naughty.notify({ preset = naughty.config.presets.critical,
+                                              title = "Oops, an error happened!",
+                                              text = err })
+                             in_error = false
+                                          end)
+end
+-- }}}
+
+-- {{{ Variable definitions
+-- Themes define colours, icons, and wallpapers
+beautiful.init("/usr/share/awesome/themes/default/theme.lua")
+
+-- This is used later as the default terminal and editor to run.
+terminal = "xterm"
+editor = os.getenv("EDITOR") or "nano"
+editor_cmd = terminal .. " -e " .. editor
+
+-- Default modkey.
+-- Usually, Mod4 is the key with a logo between Control and Alt.
+-- If you do not like this or do not have such a key,
+-- I suggest you to remap Mod4 to another key using xmodmap or other tools.
+-- However, you can use another modifier like Mod1, but it may interact with others.
+modkey = "Mod4"
+
+-- Table of layouts to cover with awful.layout.inc, order matters.
+local layouts =
+   {
+   awful.layout.suit.floating,
+   awful.layout.suit.tile,
+   awful.layout.suit.tile.left,
+   awful.layout.suit.tile.bottom,
+   awful.layout.suit.tile.top,
+   awful.layout.suit.fair,
+   awful.layout.suit.fair.horizontal,
+   awful.layout.suit.spiral,
+   awful.layout.suit.spiral.dwindle,
+   awful.layout.suit.max,
+   awful.layout.suit.max.fullscreen,
+   awful.layout.suit.magnifier
+   }
+   -- }}}
+
+   -- {{{ Wallpaper
+   if beautiful.wallpaper then
+      for s = 1, screen.count() do
+         gears.wallpaper.maximized(beautiful.wallpaper, s, true)
+      end
+   end
+   -- }}}
+
+   -- {{{ Tags
+   -- Define a tag table which hold all screen tags.
+   tags = {}
+   for s = 1, screen.count() do
+      -- Each screen has its own tag table.
+      tags[s] = awful.tag({ 1, 2, 3, 4, 5, 6, 7, 8, 9 }, s, layouts[1])
+   end
+   -- }}}
+
+   -- {{{ Menu
+   -- Create a laucher widget and a main menu
+   myawesomemenu = {
+      { "manual", terminal .. " -e man awesome" },
+      { "edit config", editor_cmd .. " " .. awesome.conffile },
+      { "restart", awesome.restart },
+      { "quit", awesome.quit }
+   }
+
+   mymainmenu = awful.menu({ items = { { "awesome", myawesomemenu, beautiful.awesome_icon },
+                                       { "open terminal", terminal }
+                                     }
+                           })
+
+   mylauncher = awful.widget.launcher({ image = beautiful.awesome_icon,
+                                        menu = mymainmenu })
+
+   -- Menubar configuration
+   menubar.utils.terminal = terminal -- Set the terminal for applications that require it
+   -- }}}
+
+   -- {{{ Wibox
+   -- Create a textclock widget
+   mytextclock = awful.widget.textclock()
+
+   -- BEGIN OF AWESOMPD WIDGET DECLARATION
+
+   local awesompd = require('awesompd/awesompd')
+
+   musicwidget = awesompd:create() -- Create awesompd widget
+   musicwidget.font = "Liberation Mono" -- Set widget font
+   -- musicwidget.font_color = "#FFFFFF" --Set widget font color
+   -- musicwidget.background = "#000000" --Set widget background color
+   musicwidget.scrolling = true -- If true, the text in the widget will be scrolled
+   musicwidget.output_size = 30 -- Set the size of widget in symbols
+   musicwidget.update_interval = 10 -- Set the update interval in seconds
+
+   -- Set the folder where icons are located (change username to your login name)
+   musicwidget.path_to_icons = "/home/unlogic/.config/awesome/icons"
+
+   -- Set the path to the icon to be displayed on the widget itself
+   -- musicwidget.widget_icon = "/path/to/icon"
+
+   -- Set the default music format for Jamendo streams. You can change
+   -- this option on the fly in awesompd itself.
+   -- possible formats: awesompd.FORMAT_MP3, awesompd.FORMAT_OGG
+   musicwidget.jamendo_format = awesompd.FORMAT_MP3
+
+   -- Specify the browser you use so awesompd can open links from
+   -- Jamendo in it.
+   musicwidget.browser = "firefox"
+
+   -- If true, song notifications for Jamendo tracks and local tracks
+   -- will also contain album cover image.
+   musicwidget.show_album_cover = true
+
+   -- Specify how big in pixels should an album cover be. Maximum value
+   -- is 100.
+   musicwidget.album_cover_size = 50
+
+   -- This option is necessary if you want the album covers to be shown
+   -- for your local tracks.
+   musicwidget.mpd_config = "/home/unlogic/.mpdconf"
+
+   -- Specify decorators on the left and the right side of the
+   -- widget. Or just leave empty strings if you decorate the widget
+   -- from outside.
+   musicwidget.ldecorator = " "
+   musicwidget.rdecorator = " "
+
+   -- Set all the servers to work with (here can be any servers you use)
+   musicwidget.servers = {
+      { server = "localhost",
+        port = 6600 },
+      { server = "192.168.0.72",
+        port = 6600 }
+   }
+
+   -- Set the buttons of the widget. Keyboard keys are working in the
+   -- entire Awesome environment. Also look at the line 352.
+   musicwidget:register_buttons(
+      { { "", awesompd.MOUSE_LEFT, musicwidget:command_playpause() },
+        { "Control", awesompd.MOUSE_SCROLL_UP, musicwidget:command_prev_track() },
+        { "Control", awesompd.MOUSE_SCROLL_DOWN, musicwidget:command_next_track() },
+        { "", awesompd.MOUSE_SCROLL_UP, musicwidget:command_volume_up() },
+        { "", awesompd.MOUSE_SCROLL_DOWN, musicwidget:command_volume_down() },
+        { "", awesompd.MOUSE_RIGHT, musicwidget:command_show_menu() },
+        { "", "XF86AudioLowerVolume", musicwidget:command_volume_down() },
+        { "", "XF86AudioRaiseVolume", musicwidget:command_volume_up() },
+        { modkey, "Pause", musicwidget:command_playpause() } })
+
+   musicwidget:run() -- After all configuration is done, run the widget
+
+   -- END OF AWESOMPD WIDGET DECLARATION
+   -- Don't forget to add the widget to the wibox. It is done on the line 244.
+
+   -- Create a wibox for each screen and add it
+   mywibox = {}
+   mypromptbox = {}
+   mylayoutbox = {}
+   mytaglist = {}
+   mytaglist.buttons = awful.util.table.join(
+      awful.button({ }, 1, awful.tag.viewonly),
+      awful.button({ modkey }, 1, awful.client.movetotag),
+      awful.button({ }, 3, awful.tag.viewtoggle),
+      awful.button({ modkey }, 3, awful.client.toggletag),
+      awful.button({ }, 4, function(t) awful.tag.viewnext(awful.tag.getscreen(t)) end),
+      awful.button({ }, 5, function(t) awful.tag.viewprev(awful.tag.getscreen(t)) end)
+                                            )
+   mytasklist = {}
+   mytasklist.buttons = awful.util.table.join(
+      awful.button({ }, 1, function (c)
+                      if c == client.focus then
+                         c.minimized = true
+                      else
+                         -- Without this, the following
+                         -- :isvisible() makes no sense
+                         c.minimized = false
+                         if not c:isvisible() then
+                            awful.tag.viewonly(c:tags()[1])
+                         end
+                         -- This will also un-minimize
+                         -- the client, if needed
+                         client.focus = c
+                         c:raise()
+                      end
+                           end),
+      awful.button({ }, 3, function ()
+                      if instance then
+                         instance:hide()
+                         instance = nil
+                      else
+                         instance = awful.menu.clients({ width=250 })
+                      end
+                           end),
+      awful.button({ }, 4, function ()
+                      awful.client.focus.byidx(1)
+                      if client.focus then client.focus:raise() end
+                           end),
+      awful.button({ }, 5, function ()
+                      awful.client.focus.byidx(-1)
+                      if client.focus then client.focus:raise() end
+                           end))
+
+   for s = 1, screen.count() do
+      -- Create a promptbox for each screen
+      mypromptbox[s] = awful.widget.prompt()
+      -- Create an imagebox widget which will contains an icon indicating which layout we're using.
+      -- We need one layoutbox per screen.
+      mylayoutbox[s] = awful.widget.layoutbox(s)
+      mylayoutbox[s]:buttons(awful.util.table.join(
+                                awful.button({ }, 1, function () awful.layout.inc(layouts, 1) end),
+                                awful.button({ }, 3, function () awful.layout.inc(layouts, -1) end),
+                                awful.button({ }, 4, function () awful.layout.inc(layouts, 1) end),
+                                awful.button({ }, 5, function () awful.layout.inc(layouts, -1) end)))
+      -- Create a taglist widget
+      mytaglist[s] = awful.widget.taglist(s, awful.widget.taglist.filter.all, mytaglist.buttons)
+
+      -- Create a tasklist widget
+      mytasklist[s] = awful.widget.tasklist(s, awful.widget.tasklist.filter.currenttags, mytasklist.buttons)
+
+      -- Create the wibox
+      mywibox[s] = awful.wibox({ position = "top", screen = s })
+
+      -- Widgets that are aligned to the left
+      local left_layout = wibox.layout.fixed.horizontal()
+      left_layout:add(mylauncher)
+      left_layout:add(mytaglist[s])
+      left_layout:add(mypromptbox[s])
+
+      -- Widgets that are aligned to the right
+      local right_layout = wibox.layout.fixed.horizontal()
+      if s == 1 then right_layout:add(wibox.widget.systray()) end
+      right_layout:add(musicwidget.widget)  -- Widget is added here.
+      right_layout:add(mytextclock)
+      right_layout:add(mylayoutbox[s])
+
+      -- Now bring it all together (with the tasklist in the middle)
+      local layout = wibox.layout.align.horizontal()
+      layout:set_left(left_layout)
+      layout:set_middle(mytasklist[s])
+      layout:set_right(right_layout)
+
+      mywibox[s]:set_widget(layout)
+   end
+   -- }}}
+
+   -- {{{ Mouse bindings
+   root.buttons(awful.util.table.join(
+                   awful.button({ }, 3, function () mymainmenu:toggle() end),
+                   awful.button({ }, 4, awful.tag.viewnext),
+                   awful.button({ }, 5, awful.tag.viewprev)
+                                     ))
+   -- }}}
+
+   -- {{{ Key bindings
+   globalkeys = awful.util.table.join(
+      awful.key({ modkey,           }, "Left",   awful.tag.viewprev       ),
+      awful.key({ modkey,           }, "Right",  awful.tag.viewnext       ),
+      awful.key({ modkey,           }, "Escape", awful.tag.history.restore),
+
+      awful.key({ modkey,           }, "j",
+                function ()
+                   awful.client.focus.byidx( 1)
+                   if client.focus then client.focus:raise() end
+                end),
+      awful.key({ modkey,           }, "k",
+                function ()
+                   awful.client.focus.byidx(-1)
+                   if client.focus then client.focus:raise() end
+                end),
+      awful.key({ modkey,           }, "w", function () mymainmenu:show() end),
+
+      -- Layout manipulation
+      awful.key({ modkey, "Shift"   }, "j", function () awful.client.swap.byidx(  1)    end),
+      awful.key({ modkey, "Shift"   }, "k", function () awful.client.swap.byidx( -1)    end),
+      awful.key({ modkey, "Control" }, "j", function () awful.screen.focus_relative( 1) end),
+      awful.key({ modkey, "Control" }, "k", function () awful.screen.focus_relative(-1) end),
+      awful.key({ modkey,           }, "u", awful.client.urgent.jumpto),
+      awful.key({ modkey,           }, "Tab",
+                function ()
+                   awful.client.focus.history.previous()
+                   if client.focus then
+                      client.focus:raise()
+                   end
+                end),
+
+      -- Standard program
+      awful.key({ modkey,           }, "Return", function () awful.util.spawn(terminal) end),
+      awful.key({ modkey, "Control" }, "r", awesome.restart),
+      awful.key({ modkey, "Shift"   }, "q", awesome.quit),
+
+      awful.key({ modkey,           }, "l",     function () awful.tag.incmwfact( 0.05)    end),
+      awful.key({ modkey,           }, "h",     function () awful.tag.incmwfact(-0.05)    end),
+      awful.key({ modkey, "Shift"   }, "h",     function () awful.tag.incnmaster( 1)      end),
+      awful.key({ modkey, "Shift"   }, "l",     function () awful.tag.incnmaster(-1)      end),
+      awful.key({ modkey, "Control" }, "h",     function () awful.tag.incncol( 1)         end),
+      awful.key({ modkey, "Control" }, "l",     function () awful.tag.incncol(-1)         end),
+      awful.key({ modkey,           }, "space", function () awful.layout.inc(layouts,  1) end),
+      awful.key({ modkey, "Shift"   }, "space", function () awful.layout.inc(layouts, -1) end),
+
+      awful.key({ modkey, "Control" }, "n", awful.client.restore),
+
+      -- Prompt
+      awful.key({ modkey },            "r",     function () mypromptbox[mouse.screen]:run() end),
+
+      awful.key({ modkey }, "x",
+                function ()
+                   awful.prompt.run({ prompt = "Run Lua code: " },
+                                    mypromptbox[mouse.screen].widget,
+                                    awful.util.eval, nil,
+                                    awful.util.getdir("cache") .. "/history_eval")
+                end),
+      -- Menubar
+      awful.key({ modkey }, "p", function() menubar.show() end)
+                                     )
+
+   clientkeys = awful.util.table.join(
+      awful.key({ modkey,           }, "f",      function (c) c.fullscreen = not c.fullscreen  end),
+      awful.key({ modkey, "Shift"   }, "c",      function (c) c:kill()                         end),
+      awful.key({ modkey, "Control" }, "space",  awful.client.floating.toggle                     ),
+      awful.key({ modkey, "Control" }, "Return", function (c) c:swap(awful.client.getmaster()) end),
+      awful.key({ modkey,           }, "o",      awful.client.movetoscreen                        ),
+      awful.key({ modkey,           }, "t",      function (c) c.ontop = not c.ontop            end),
+      awful.key({ modkey,           }, "n",
+                function (c)
+                   -- The client currently has the input focus, so it cannot be
+                   -- minimized, since minimized clients can't have the focus.
+                   c.minimized = true
+                end),
+      awful.key({ modkey,           }, "m",
+                function (c)
+                   c.maximized_horizontal = not c.maximized_horizontal
+                   c.maximized_vertical   = not c.maximized_vertical
+                end)
+                                     )
+
+   -- Bind all key numbers to tags.
+   -- Be careful: we use keycodes to make it works on any keyboard layout.
+   -- This should map on the top row of your keyboard, usually 1 to 9.
+   for i = 1, 9 do
+      globalkeys = awful.util.table.join(globalkeys,
+                                         awful.key({ modkey }, "#" .. i + 9,
+                                                   function ()
+                                                      local screen = mouse.screen
+                                                      local tag = awful.tag.gettags(screen)[i]
+                                                      if tag then
+                                                         awful.tag.viewonly(tag)
+                                                      end
+                                                   end),
+                                         awful.key({ modkey, "Control" }, "#" .. i + 9,
+                                                   function ()
+                                                      local screen = mouse.screen
+                                                      local tag = awful.tag.gettags(screen)[i]
+                                                      if tag then
+                                                         awful.tag.viewtoggle(tag)
+                                                      end
+                                                   end),
+                                         awful.key({ modkey, "Shift" }, "#" .. i + 9,
+                                                   function ()
+                                                      local tag = awful.tag.gettags(client.focus.screen)[i]
+                                                      if client.focus and tag then
+                                                         awful.client.movetotag(tag)
+                                                      end
+                                                   end),
+                                         awful.key({ modkey, "Control", "Shift" }, "#" .. i + 9,
+                                                   function ()
+                                                      local tag = awful.tag.gettags(client.focus.screen)[i]
+                                                      if client.focus and tag then
+                                                         awful.client.toggletag(tag)
+                                                      end
+                                                   end))
+   end
+
+   clientbuttons = awful.util.table.join(
+      awful.button({ }, 1, function (c) client.focus = c; c:raise() end),
+      awful.button({ modkey }, 1, awful.mouse.client.move),
+      awful.button({ modkey }, 3, awful.mouse.client.resize))
+
+   -- Set keys
+   -- Add this line before root.keys(globalkeys).
+   musicwidget:append_global_keys()
+
+   root.keys(globalkeys)
+   -- }}}
+
+   -- {{{ Rules
+   awful.rules.rules = {
+      -- All clients will match this rule.
+      { rule = { },
+        properties = { border_width = beautiful.border_width,
+                       border_color = beautiful.border_normal,
+                       focus = awful.client.focus.filter,
+                       keys = clientkeys,
+                       buttons = clientbuttons } },
+      { rule = { class = "MPlayer" },
+        properties = { floating = true } },
+      { rule = { class = "pinentry" },
+        properties = { floating = true } },
+      { rule = { class = "gimp" },
+        properties = { floating = true } },
+      -- Set Firefox to always map on tags number 2 of screen 1.
+      -- { rule = { class = "Firefox" },
+      --   properties = { tag = tags[1][2] } },
+   }
+   -- }}}
+
+   -- {{{ Signals
+   -- Signal function to execute when a new client appears.
+   client.connect_signal("manage", function (c, startup)
+                            -- Enable sloppy focus
+                            c:connect_signal("mouse::enter", function(c)
+                                                if awful.layout.get(c.screen) ~= awful.layout.suit.magnifier
+                                                   and awful.client.focus.filter(c) then
+                                                client.focus = c
+                                                end
+                                                             end)
+
+                            if not startup then
+                               -- Set the windows at the slave,
+                               -- i.e. put it at the end of others instead of setting it master.
+                               -- awful.client.setslave(c)
+
+                               -- Put windows in a smart way, only if they does not set an initial position.
+                               if not c.size_hints.user_position and not c.size_hints.program_position then
+                                  awful.placement.no_overlap(c)
+                                  awful.placement.no_offscreen(c)
+                               end
+                            end
+
+                            local titlebars_enabled = false
+                            if titlebars_enabled and (c.type == "normal" or c.type == "dialog") then
+                               -- buttons for the titlebar
+                               local buttons = awful.util.table.join(
+                                  awful.button({ }, 1, function()
+                                                  client.focus = c
+                                                  c:raise()
+                                                  awful.mouse.client.move(c)
+                                                       end),
+                                  awful.button({ }, 3, function()
+                                                  client.focus = c
+                                                  c:raise()
+                                                  awful.mouse.client.resize(c)
+                                                       end)
+                                                                    )
+
+                               -- Widgets that are aligned to the left
+                               local left_layout = wibox.layout.fixed.horizontal()
+                               left_layout:add(awful.titlebar.widget.iconwidget(c))
+                               left_layout:buttons(buttons)
+
+                               -- Widgets that are aligned to the right
+                               local right_layout = wibox.layout.fixed.horizontal()
+                               right_layout:add(awful.titlebar.widget.floatingbutton(c))
+                               right_layout:add(awful.titlebar.widget.maximizedbutton(c))
+                               right_layout:add(awful.titlebar.widget.stickybutton(c))
+                               right_layout:add(awful.titlebar.widget.ontopbutton(c))
+                               right_layout:add(awful.titlebar.widget.closebutton(c))
+
+                               -- The title goes in the middle
+                               local middle_layout = wibox.layout.flex.horizontal()
+                               local title = awful.titlebar.widget.titlewidget(c)
+                               title:set_align("center")
+                               middle_layout:add(title)
+                               middle_layout:buttons(buttons)
+
+                               -- Now bring it all together
+                               local layout = wibox.layout.align.horizontal()
+                               layout:set_left(left_layout)
+                               layout:set_right(right_layout)
+                               layout:set_middle(middle_layout)
+
+                               awful.titlebar(c):set_widget(layout)
+                            end
+                                   end)
+
+   client.connect_signal("focus", function(c) c.border_color = beautiful.border_focus end)
+   client.connect_signal("unfocus", function(c) c.border_color = beautiful.border_normal end)
+   -- }}}
diff --git a/awesome/widgets/awesompd/utf8.lua b/awesome/widgets/awesompd/utf8.lua
new file mode 100644 (file)
index 0000000..0d2c139
--- /dev/null
@@ -0,0 +1,164 @@
+-- Provides UTF-8 aware string functions implemented in pure lua:
+-- * string.utf8len(s)
+-- * string.utf8sub(s, i, j)
+--
+-- All functions behave as their non UTF-8 aware counterparts with the exception
+-- that UTF-8 characters are used instead of bytes for all units.
+--
+-- Note: all validations had been removed due to awesome usage specifics.
+--[[
+Copyright (c) 2006-2007, Kyle Smith
+Modified by Alexander Yakushev, 2010-2013.
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+* Redistributions of source code must retain the above copyright notice,
+this list of conditions and the following disclaimer.
+* Redistributions in binary form must reproduce the above copyright
+notice, this list of conditions and the following disclaimer in the
+documentation and/or other materials provided with the distribution.
+* Neither the name of the author nor the names of its contributors may be
+used to endorse or promote products derived from this software without
+specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+--]]
+
+-- ABNF from RFC 3629
+--
+-- UTF8-octets = *( UTF8-char )
+-- UTF8-char = UTF8-1 / UTF8-2 / UTF8-3 / UTF8-4
+-- UTF8-1 = %x00-7F
+-- UTF8-2 = %xC2-DF UTF8-tail
+-- UTF8-3 = %xE0 %xA0-BF UTF8-tail / %xE1-EC 2( UTF8-tail ) /
+-- %xED %x80-9F UTF8-tail / %xEE-EF 2( UTF8-tail )
+-- UTF8-4 = %xF0 %x90-BF 2( UTF8-tail ) / %xF1-F3 3( UTF8-tail ) /
+-- %xF4 %x80-8F 2( UTF8-tail )
+-- UTF8-tail = %x80-BF
+--
+
+-- returns the number of bytes used by the UTF-8 character at byte i in s
+-- also doubles as a UTF-8 character validator
+
+local utf8 = {}
+
+function utf8.charbytes (s, i)
+   -- argument defaults
+   i = i or 1
+   local c = string.byte(s, i)
+   
+   -- determine bytes needed for character, based on RFC 3629
+   if c > 0 and c <= 127 then
+      -- UTF8-1
+      return 1
+   elseif c >= 194 and c <= 223 then
+      -- UTF8-2
+      local c2 = string.byte(s, i + 1)
+      return 2
+   elseif c >= 224 and c <= 239 then
+      -- UTF8-3
+      local c2 = s:byte(i + 1)
+      local c3 = s:byte(i + 2)
+      return 3
+   elseif c >= 240 and c <= 244 then
+      -- UTF8-4
+      local c2 = s:byte(i + 1)
+      local c3 = s:byte(i + 2)
+      local c4 = s:byte(i + 3)
+      return 4
+   end
+end
+
+-- returns the number of characters in a UTF-8 string
+function utf8.len (s)
+   local pos = 1
+   local bytes = string.len(s)
+   local len = 0
+   
+   while pos <= bytes and len ~= chars do
+      local c = string.byte(s,pos)
+      len = len + 1
+      
+      pos = pos + utf8.charbytes(s, pos)
+   end
+   
+   if chars ~= nil then
+      return pos - 1
+   end
+   
+   return len
+end
+
+-- functions identically to string.sub except that i and j are UTF-8 characters
+-- instead of bytes
+function utf8.sub (s, i, j)
+   j = j or -1
+
+   if i == nil then
+      return ""
+   end
+   
+   local pos = 1
+   local bytes = string.len(s)
+   local len = 0
+
+   -- only set l if i or j is negative
+   local l = (i >= 0 and j >= 0) or utf8.len(s)
+   local startChar = (i >= 0) and i or l + i + 1
+   local endChar = (j >= 0) and j or l + j + 1
+
+   -- can't have start before end!
+   if startChar > endChar then
+      return ""
+   end
+   
+   -- byte offsets to pass to string.sub
+   local startByte, endByte = 1, bytes
+   
+   while pos <= bytes do
+      len = len + 1
+      
+      if len == startChar then
+        startByte = pos
+      end
+      
+      pos = pos + utf8.charbytes(s, pos)
+      
+      if len == endChar then
+        endByte = pos - 1
+        break
+      end
+   end
+   
+   return string.sub(s, startByte, endByte)
+end
+
+-- replace UTF-8 characters based on a mapping table
+function utf8.replace (s, mapping)
+   local pos = 1
+   local bytes = string.len(s)
+   local charbytes
+   local newstr = ""
+
+   while pos <= bytes do
+      charbytes = utf8.charbytes(s, pos)
+      local c = string.sub(s, pos, pos + charbytes - 1)
+      newstr = newstr .. (mapping[c] or c)
+      pos = pos + charbytes
+   end
+
+   return newstr
+end
+
+return utf8