From dd30f585b4ec95426cb4f7348b4893c238250e1c Mon Sep 17 00:00:00 2001 From: Samir Benmendil Date: Mon, 13 Jan 2014 13:12:51 +0100 Subject: [PATCH] awesome: add awesompd widget + mpd bindings --- awesome/bindings.lua | 33 +- awesome/topbar.lua | 1 + awesome/widgets/awesompd.lua | 47 + awesome/widgets/awesompd/README.md | 24 + awesome/widgets/awesompd/asyncshell.lua | 71 + awesome/widgets/awesompd/awesompd.lua | 1219 +++++++++++++++++ awesome/widgets/awesompd/icons/check_icon.png | Bin 0 -> 241 bytes .../awesompd/icons/default_album_cover.png | Bin 0 -> 18940 bytes awesome/widgets/awesompd/icons/next_icon.png | Bin 0 -> 242 bytes awesome/widgets/awesompd/icons/pause_icon.png | Bin 0 -> 210 bytes awesome/widgets/awesompd/icons/play_icon.png | Bin 0 -> 234 bytes .../awesompd/icons/play_pause_icon.png | Bin 0 -> 248 bytes awesome/widgets/awesompd/icons/prev_icon.png | Bin 0 -> 240 bytes awesome/widgets/awesompd/icons/radio_icon.png | Bin 0 -> 342 bytes awesome/widgets/awesompd/icons/stop_icon.png | Bin 0 -> 210 bytes awesome/widgets/awesompd/jamendo.lua | 518 +++++++ awesome/widgets/awesompd/rcsample.lua | 508 +++++++ awesome/widgets/awesompd/utf8.lua | 164 +++ 18 files changed, 2564 insertions(+), 21 deletions(-) create mode 100644 awesome/widgets/awesompd.lua create mode 100644 awesome/widgets/awesompd/README.md create mode 100755 awesome/widgets/awesompd/asyncshell.lua create mode 100644 awesome/widgets/awesompd/awesompd.lua create mode 100644 awesome/widgets/awesompd/icons/check_icon.png create mode 100644 awesome/widgets/awesompd/icons/default_album_cover.png create mode 100644 awesome/widgets/awesompd/icons/next_icon.png create mode 100644 awesome/widgets/awesompd/icons/pause_icon.png create mode 100644 awesome/widgets/awesompd/icons/play_icon.png create mode 100644 awesome/widgets/awesompd/icons/play_pause_icon.png create mode 100644 awesome/widgets/awesompd/icons/prev_icon.png create mode 100644 awesome/widgets/awesompd/icons/radio_icon.png create mode 100644 awesome/widgets/awesompd/icons/stop_icon.png create mode 100644 awesome/widgets/awesompd/jamendo.lua create mode 100644 awesome/widgets/awesompd/rcsample.lua create mode 100644 awesome/widgets/awesompd/utf8.lua diff --git a/awesome/bindings.lua b/awesome/bindings.lua index 6a72d0b..1f68d7c 100644 --- a/awesome/bindings.lua +++ b/awesome/bindings.lua @@ -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 - --- }}} diff --git a/awesome/topbar.lua b/awesome/topbar.lua index 1bc3978..c5057c0 100644 --- a/awesome/topbar.lua +++ b/awesome/topbar.lua @@ -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 index 0000000..ea8ea2a --- /dev/null +++ b/awesome/widgets/awesompd.lua @@ -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 index 0000000..7a73137 --- /dev/null +++ b/awesome/widgets/awesompd/README.md @@ -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 index 0000000..c62284e --- /dev/null +++ b/awesome/widgets/awesompd/asyncshell.lua @@ -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 index 0000000..8494003 --- /dev/null +++ b/awesome/widgets/awesompd/awesompd.lua @@ -0,0 +1,1219 @@ +--------------------------------------------------------------------------- +-- @author Alexander Yakushev +-- @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["&"] = "&" +-- 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('%s%s%s', + 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("" .. title_text .. "") + 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 index 0000000000000000000000000000000000000000..dc5cba1287277f859cece13ee824cff04505493c GIT binary patch literal 241 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 literal 0 HcmV?d00001 diff --git a/awesome/widgets/awesompd/icons/next_icon.png b/awesome/widgets/awesompd/icons/next_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..918ff897e5f4e9a7496b40ecd0143891484d40da GIT binary patch literal 242 zcmeAS@N?(olHy`uVBq!ia0y~yU=RRd4mJh`2Kmqb6B!s77>k44ofy`glX=O&z`&N| z?e4+=20xv5*E29Ma29w(7Bet#3xhBt!>lD=GEJ{DFfhnw zhD4M&=jZ08=9Pf;I~S!UmZYXAlxLP?D7bt22BhevvNJF+czU`xhFF}wI{6?kg8>KA z#lPjtxH}Hsusb!mr8|RX(hZG`S3(`Gx>kk=@0hUd5XYjj1m|6k44ofy`glX=O&z`&N| z?e4+=20xv5*E29Ma29w(7Bet#3xhBt!>lo6U|^6e zag8W(&d<$F%`0JWE=o--Nlj5G&n(GMaQE~L2yf&QXJBB^^>lFzu{fRl=l_3uX4Qty zMkfbh=6}wHfsdLmb1!D&v1E?e&|&x};IxIsUIqrXT6S5Pv*ss3&hd2hb6Mw<&;$U{ CDL3r^ literal 0 HcmV?d00001 diff --git a/awesome/widgets/awesompd/icons/play_icon.png b/awesome/widgets/awesompd/icons/play_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..83230d13214f288943c4de91d96d8b74a64251ee GIT binary patch literal 234 zcmeAS@N?(olHy`uVBq!ia0y~yU=RRd4mJh`2Kmqb6B!s77>k44ofy`glX=O&z`&N| z?e4+=20xv5*E29Ma29w(7Bet#3xhBt!>l`Ki;O^-g5Z=fq&cMLn>gnPbVsSe8&;S4S%&HBY zjZO}w%>SGXkLoGhQgDh>$Pn0=*l<}Ph*|KkLRWHQtIOA(CsGGI)^SUm{K}-fGx31M b7b9VYZy$M;7#>yJ2RYNz)z4*}Q$iB}E7wGx literal 0 HcmV?d00001 diff --git a/awesome/widgets/awesompd/icons/play_pause_icon.png b/awesome/widgets/awesompd/icons/play_pause_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..e54beb6a509af66ffbd88fd3dc8f38fca5076864 GIT binary patch literal 248 zcmeAS@N?(olHy`uVBq!ia0y~yU=RRd4mJh`2Kmqb6B!s77>k44ofy`glX=O&z`&N| z?e4+=20xv5*E29Ma29w(7Bet#3xhBt!>lb5%;X$Ia`BIgT3js$$8P9I|%QopD=M&Dk44ofy`glX=O&z`&N| z?e4+=20xv5*E29Ma29w(7Bet#3xhBt!>l< zXQPutDDyvOL#qSYZYNk-cvBX+7~3&-eB}yb?zrZ&IU_|Sh-W=_hZ)O!@eZSw00{w; dmcke5Y7BLIg+#-dd3J&<^K|udS?83{1OPslKd}G+ literal 0 HcmV?d00001 diff --git a/awesome/widgets/awesompd/icons/radio_icon.png b/awesome/widgets/awesompd/icons/radio_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..ffdf065b18858f0544eba1abcf47f9f9723cd9cb GIT binary patch literal 342 zcmeAS@N?(olHy`uVBq!ia0y~yU=RRd4mJh`2Kmqb6B!s77>k44ofy`glX=O&z`&N| z?e4+=20xv5*E29Ma29w(7Bet#3xhBt!>lwc6x=<11Hv2m#2FYEHhQ`^hFF|FI@yuykb#Ixe7Cft z*YuC!Bs zY8gNO5aWxU@Av#A->uQ~ODq$9&3`;%I?seHB~xFpsXdu`zka`8!EP>7aimx`r4|$zopr0E-rJk44ofy`glX=O&z`&N| z?e4+=20xv5*E29Ma29w(7Bet#3xhBt!>l_mV|L&!kaIj;{an^LB{Ts5 DIrKcM literal 0 HcmV?d00001 diff --git a/awesome/widgets/awesompd/jamendo.lua b/awesome/widgets/awesompd/jamendo.lua new file mode 100644 index 0000000..3851e0a --- /dev/null +++ b/awesome/widgets/awesompd/jamendo.lua @@ -0,0 +1,518 @@ +--------------------------------------------------------------------------- +-- @author Alexander Yakushev +-- @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¶m2=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 index 0000000..18a7a23 --- /dev/null +++ b/awesome/widgets/awesompd/rcsample.lua @@ -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 index 0000000..0d2c139 --- /dev/null +++ b/awesome/widgets/awesompd/utf8.lua @@ -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 -- 2.49.0