Initial Commit

This commit is contained in:
2021-04-26 11:20:41 +03:00
commit 5a99cb9e82
125 changed files with 6148 additions and 0 deletions

124
widget/battery-d.lua Normal file
View File

@@ -0,0 +1,124 @@
-------------------------------------------------
-- Battery Widget for Awesome Window Manager
-- Shows the battery status using the ACPI tool
-- More details could be found here:
-- https://github.com/streetturtle/awesome-wm-widgets/tree/master/battery-widget
-- @author Pavel Makhov
-- @copyright 2017 Pavel Makhov
-------------------------------------------------
local awful = require('awful')
local watch = require('awful.widget.watch')
local wibox = require('wibox')
local beautiful = require('beautiful')
local dpi = require('beautiful').xresources.apply_dpi
-- acpi sample outputs
-- Battery 0: Discharging, 75%, 01:51:38 remaining
-- Battery 0: Charging, 53%, 00:57:43 until charged
local percentage = wibox.widget.textbox()
local battery_icon = wibox.widget.textbox()
battery_icon.font = beautiful.icon_font
local battery_popup = awful.tooltip({
objects = {percentage},
mode = 'outside',
align = 'left',
preferred_positions = {'right', 'left', 'top', 'bottom'}
})
watch('acpi -i', 10, function(_, stdout)
local battery_info = {}
local capacities = {}
for s in stdout:gmatch('[^\r\n]+') do
local status, charge_str, time = string.match(s, '.+: (%a+), (%d?%d?%d)%%,?.*')
if status ~= nil then
table.insert(battery_info, {
status = status,
charge = tonumber(charge_str)
})
else
local cap_str = string.match(s, '.+:.+last full capacity (%d+)')
table.insert(capacities, tonumber(cap_str))
end
end
local capacity = 0
for _, cap in ipairs(capacities) do
capacity = capacity + cap
end
local charge = 0
local status
for i, batt in ipairs(battery_info) do
if batt.charge >= charge then
status = batt.status -- use most charged battery status
-- this is arbitrary, and maybe another metric should be used
end
charge = charge + batt.charge * capacities[i]
end
charge = charge / capacity
battery_popup.text = string.gsub(stdout, '\n$', '')
percentage.text = math.floor(charge)
if status == 'Charging' then
battery_icon.text = ''
if math.floor(charge) <= 20 then
battery_icon.text = ''
elseif math.floor(charge) <= 30 then
battery_icon.text = ''
elseif math.floor(charge) <= 40 then
battery_icon.text = ''
elseif math.floor(charge) <= 60 then
battery_icon.text = ''
elseif math.floor(charge) <= 80 then
battery_icon.text = ''
elseif math.floor(charge) <= 90 then
--battery_icon.text = ''
battery_icon.text = '90'
elseif math.floor(charge) <= 100 then
--battery_icon.text = ''
battery_icon.text = '90'
end
elseif status == 'Full' then
battery_icon.text = ''
else
if math.floor(charge) <= 10 then
battery_icon.text = ''
elseif math.floor(charge) <= 20 then
battery_icon.text = ''
elseif math.floor(charge) <= 30 then
battery_icon.text = ''
elseif math.floor(charge) <= 40 then
battery_icon.text = ''
elseif math.floor(charge) <= 50 then
battery_icon.text = ''
elseif math.floor(charge) <= 60 then
battery_icon.text = ''
elseif math.floor(charge) <= 60 then
battery_icon.text = ''
elseif math.floor(charge) <= 80 then
battery_icon.text = ''
elseif math.floor(charge) <= 90 then
battery_icon.text = ''
battery_icon.text = '90'
elseif math.floor(charge) <= 100 then
battery_icon.text = ''
battery_icon.text = '90'
end
end
collectgarbage('collect')
end)
return wibox.widget {
wibox.widget{
battery_icon,
fg = beautiful.accent.hue_300,
widget = wibox.container.background
},
percentage,
spacing = dpi(2),
layout = wibox.layout.fixed.horizontal
}

191
widget/battery.lua Normal file
View File

@@ -0,0 +1,191 @@
-------------------------------------------------
-- Battery Widget for Awesome Window Manager
-- Shows the battery status using the ACPI tool
-- More details could be found here:
-- https://github.com/streetturtle/awesome-wm-widgets/tree/master/battery-widget
-- @author Pavel Makhov
-- @copyright 2017 Pavel Makhov
-------------------------------------------------
local awful = require("awful")
local naughty = require("naughty")
local watch = require("awful.widget.watch")
local wibox = require("wibox")
local gfs = require("gears.filesystem")
local dpi = require('beautiful').xresources.apply_dpi
-- acpi sample outputs
-- Battery 0: Discharging, 75%, 01:51:38 remaining
-- Battery 0: Charging, 53%, 00:57:43 until charged
local HOME = os.getenv("HOME")
local battery_widget = {}
local function worker(args)
local args = args or {}
local bat_num = '1'
local font = args.font or 'Play 8'
local path_to_icons = args.path_to_icons or "/usr/share/icons/Arc/status/symbolic/"
local show_current_level = args.show_current_level or false
local margin_left = args.margin_left or 0
local margin_right = args.margin_right or 0
local display_notification = args.display_notification or false
local position = args.notification_position or "top_right"
local timeout = args.timeout or 10
local warning_msg_title = args.warning_msg_title or 'Huston, we have a problem'
local warning_msg_text = args.warning_msg_text or 'Battery is dying'
local warning_msg_position = args.warning_msg_position or 'bottom_right'
local warning_msg_icon = args.warning_msg_icon or HOME .. '/.config/awesome/awesome-wm-widgets/batteryarc-widget/spaceman.jpg'
local enable_battery_warning = args.enable_battery_warning
if enable_battery_warning == nil then
enable_battery_warning = true
end
local bat_expr = 'Battery ' .. bat_num
if not gfs.dir_readable(path_to_icons) then
naughty.notify{
title = "Battery Widget",
text = "Folder with icons doesn't exist: " .. path_to_icons,
preset = naughty.config.presets.critical
}
end
local icon_widget = wibox.widget {
{
id = "icon",
widget = wibox.widget.imagebox,
resize = false
},
layout = wibox.container.margin(_, 0, 0, 3)
}
local level_widget = wibox.widget {
font = font,
widget = wibox.widget.textbox
}
battery_widget = wibox.widget {
icon_widget,
level_widget,
layout = wibox.layout.fixed.horizontal,
}
-- Popup with battery info
-- One way of creating a pop-up notification - naughty.notify
local notification
local function show_battery_status(batteryType)
awful.spawn.easy_async('bash -c "acpi | grep \'' .. bat_expr .. '\'"',
function(stdout, _, _, _)
naughty.destroy(notification)
notification = naughty.notify{
text = stdout,
title = "Battery status",
icon = path_to_icons .. batteryType .. ".svg",
icon_size = dpi(16),
position = position,
timeout = 5, hover_timeout = 0.5,
width = 200,
screen = mouse.screen
}
end
)
end
-- Alternative to naughty.notify - tooltip. You can compare both and choose the preferred one
--battery_popup = awful.tooltip({objects = {battery_widget}})
-- To use colors from beautiful theme put
-- following lines in rc.lua before require("battery"):
-- beautiful.tooltip_fg = beautiful.fg_normal
-- beautiful.tooltip_bg = beautiful.bg_normal
local function show_battery_warning()
naughty.notify {
icon = warning_msg_icon,
icon_size = 100,
text = warning_msg_text,
title = warning_msg_title,
timeout = 25, -- show the warning for a longer time
hover_timeout = 0.5,
position = warning_msg_position,
bg = "#F06060",
fg = "#EEE9EF",
width = 300,
screen = mouse.screen
}
end
local last_battery_check = os.time()
local batteryType = "battery-good-symbolic"
watch('bash -c "acpi -i | grep \'' .. bat_expr .. '\'"', timeout,
function(widget, stdout, stderr, exitreason, exitcode)
local battery_info = {}
local capacities = {}
for s in stdout:gmatch("[^\r\n]+") do
local status, charge_str, time = string.match(s, '.+: (%a+), (%d?%d?%d)%%,?(.*)')
if status ~= nil then
table.insert(battery_info, {status = status, charge = tonumber(charge_str)})
else
local cap_str = string.match(s, '.+:.+last full capacity (%d+)')
table.insert(capacities, tonumber(cap_str))
end
end
local capacity = 0
for i, cap in ipairs(capacities) do
capacity = capacity + cap
end
local charge = 0
local status
for i, batt in ipairs(battery_info) do
if batt.charge >= charge then
status = batt.status -- use most charged battery status
-- this is arbitrary, and maybe another metric should be used
end
charge = charge + batt.charge * capacities[i]
end
charge = charge / capacity
if show_current_level then
level_widget.text = string.format('%d%%', charge)
end
if (charge >= 0 and charge < 15) then
batteryType = "battery-empty%s-symbolic"
if enable_battery_warning and status ~= 'Charging' and os.difftime(os.time(), last_battery_check) > 300 then
-- if 5 minutes have elapsed since the last warning
last_battery_check = os.time()
show_battery_warning()
end
elseif (charge >= 15 and charge < 40) then batteryType = "battery-caution%s-symbolic"
elseif (charge >= 40 and charge < 60) then batteryType = "battery-low%s-symbolic"
elseif (charge >= 60 and charge < 80) then batteryType = "battery-good%s-symbolic"
elseif (charge >= 80 and charge <= 100) then batteryType = "battery-full%s-symbolic"
end
if status == 'Charging' then
batteryType = string.format(batteryType, '-charging')
else
batteryType = string.format(batteryType, '')
end
widget.icon:set_image(path_to_icons .. batteryType .. ".svg")
-- Update popup text
-- battery_popup.text = string.gsub(stdout, "\n$", "")
end,
icon_widget)
if display_notification then
battery_widget:connect_signal("mouse::enter", function() show_battery_status(batteryType) end)
battery_widget:connect_signal("mouse::leave", function() naughty.destroy(notification) end)
end
return wibox.container.margin(battery_widget, margin_left, margin_right)
end
return setmetatable(battery_widget, { __call = function(_, ...) return worker(...) end })

View File

@@ -0,0 +1,49 @@
local wibox = require('wibox')
local mat_list_item = require('widget.material.list-item')
local mat_slider = require('widget.material.slider')
local mat_icon_button = require('widget.material.icon-button')
local clickable_container = require('widget.material.clickable-container')
local icons = require('theme.icons')
local watch = require('awful.widget.watch')
local spawn = require('awful.spawn')
local slider =
wibox.widget {
read_only = false,
widget = mat_slider
}
slider:connect_signal(
'property::value',
function()
spawn('xbacklight -set ' .. math.max(slider.value, 5))
end
)
watch(
[[bash -c "xbacklight -get"]],
1,
function(widget, stdout, stderr, exitreason, exitcode)
local brightness = string.match(stdout, '(%d+)')
slider:set_value(tonumber(brightness))
collectgarbage('collect')
end
)
local icon =
wibox.widget {
image = icons.brightness,
widget = wibox.widget.imagebox
}
local button = mat_icon_button(icon)
local brightness_setting =
wibox.widget {
button,
slider,
widget = mat_list_item
}
return brightness_setting

View File

@@ -0,0 +1,53 @@
# Calendar Widget
Calendar widget for Awesome WM - slightly improved version of the `wibox.widget.calendar`.
## Features
- mouse support: scroll up - shows next month, scroll down - previous
- themes:
| Name | Screenshot |
|---|---|
|nord (default) | ![nord_theme](./nord.png) |
| outrun | ![outrun_theme](./outrun.png) |
| light | ![outrun_theme](./light.png) |
| dark | ![outrun_theme](./dark.png) |
- setup widget placement
top center - in case you clock is centered:
![calendar_top](./calendar_top.png)
top right - for default awesome config:
![calendar_top_right](./calendar_top_right.png)
bottom right - in case your wibar at the bottom:
![calendar_bottom_right](./calendar_bottom_right.png)
## How to use
This widget needs an 'anchor' - another widget which triggers visibility of the calendar. Default `mytextclock` is the perfect candidate!
Just after mytextclock is instantiated, create the widget and add the mouse listener to it.
```lua
local calendar_widget = require("awesome-wm-widgets.calendar-widget.calendar")
-- ...
-- Create a textclock widget
mytextclock = wibox.widget.textclock()
-- default
local cw = calendar_widget()
-- or customized
local cw = calendar_widget({
theme = 'outrun',
placement = 'bottom_right'
})
mytextclock:connect_signal("button::press",
function(_, _, _, button)
if button == 1 then cw.toggle() end
end)
```

View File

@@ -0,0 +1,235 @@
-------------------------------------------------
-- Calendar Widget for Awesome Window Manager
-- Shows the current month and supports scroll up/down to switch month
-- More details could be found here:
-- https://github.com/streetturtle/awesome-wm-widgets/tree/master/calendar-widget
-- @author Pavel Makhov
-- @copyright 2019 Pavel Makhov
-------------------------------------------------
local awful = require("awful")
local beautiful = require("beautiful")
local wibox = require("wibox")
local gears = require("gears")
local naughty = require("naughty")
local calendar_widget = {}
local function worker(args)
local calendar_themes = {
nord = {
bg = '#2E3440',
fg = '#D8DEE9',
focus_date_bg = '#88C0D0',
focus_date_fg = '#000000',
weekend_day_bg = '#3B4252',
weekday_fg = '#88C0D0',
header_fg = '#E5E9F0',
border = '#4C566A'
},
outrun = {
bg = '#0d0221',
fg = '#D8DEE9',
focus_date_bg = '#650d89',
focus_date_fg = '#2de6e2',
weekend_day_bg = '#261447',
weekday_fg = '#2de6e2',
header_fg = '#f6019d',
border = '#261447'
},
dark = {
bg = '#000000',
fg = '#ffffff',
focus_date_bg = '#ffffff',
focus_date_fg = '#000000',
weekend_day_bg = '#444444',
weekday_fg = '#ffffff',
header_fg = '#ffffff',
border = '#333333'
},
light = {
bg = '#ffffff',
fg = '#000000',
focus_date_bg = '#000000',
focus_date_fg = '#ffffff',
weekend_day_bg = '#AAAAAA',
weekday_fg = '#000000',
header_fg = '#000000',
border = '#CCCCCC'
},
monokai = {
bg = '#272822',
fg = '#F8F8F2',
focus_date_bg = '#AE81FF',
focus_date_fg = '#ffffff',
weekend_day_bg = '#75715E',
weekday_fg = '#FD971F',
header_fg = '#F92672',
border = '#75715E'
}
}
local args = args or {}
if args.theme ~= nil and calendar_themes[args.theme] == nil then
naughty.notify({
preset = naughty.config.presets.critical,
title = 'Calendar Widget',
text = 'Theme "' .. args.theme .. '" not found, fallback to default'})
args.theme = 'nord'
end
local theme = args.theme or 'nord'
local placement = args.placement or 'top'
local styles = {}
local function rounded_shape(size)
return function(cr, width, height)
gears.shape.rounded_rect(cr, width, height, size)
end
end
styles.month = {
padding = 4,
bg_color = calendar_themes[theme].bg,
border_width = 0,
}
styles.normal = {
markup = function(t) return t end,
shape = rounded_shape(4)
}
styles.focus = {
fg_color = calendar_themes[theme].focus_date_fg,
bg_color = calendar_themes[theme].focus_date_bg,
markup = function(t) return '<b>' .. t .. '</b>' end,
shape = rounded_shape(4)
}
styles.header = {
fg_color = calendar_themes[theme].header_fg,
bg_color = calendar_themes[theme].bg,
markup = function(t) return '<b>' .. t .. '</b>' end
}
styles.weekday = {
fg_color = calendar_themes[theme].weekday_fg,
bg_color = calendar_themes[theme].bg,
markup = function(t) return '<b>' .. t .. '</b>' end,
}
local function decorate_cell(widget, flag, date)
if flag == 'monthheader' and not styles.monthheader then
flag = 'header'
end
-- highlight only today's day
if flag == 'focus' then
local today = os.date('*t')
if today.month ~= date.month then
flag = 'normal'
end
end
local props = styles[flag] or {}
if props.markup and widget.get_text and widget.set_markup then
widget:set_markup(props.markup(widget:get_text()))
end
-- Change bg color for weekends
local d = { year = date.year, month = (date.month or 1), day = (date.day or 1) }
local weekday = tonumber(os.date('%w', os.time(d)))
local default_bg = (weekday == 0 or weekday == 6) and calendar_themes[theme].weekend_day_bg or calendar_themes[theme].bg
local ret = wibox.widget {
{
{
widget,
halign = 'center',
widget = wibox.container.place
},
margins = (props.padding or 2) + (props.border_width or 0),
widget = wibox.container.margin
},
shape = props.shape,
shape_border_color = props.border_color or '#000000',
shape_border_width = props.border_width or 0,
fg = props.fg_color or calendar_themes[theme].fg,
bg = props.bg_color or default_bg,
widget = wibox.container.background
}
return ret
end
local cal = wibox.widget {
date = os.date('*t'),
font = beautiful.get_font(),
fn_embed = decorate_cell,
long_weekdays = true,
widget = wibox.widget.calendar.month
}
local popup = awful.popup {
ontop = true,
visible = false,
shape = gears.shape.rounded_rect,
offset = { y = 5 },
border_width = 1,
border_color = calendar_themes[theme].border,
widget = cal
}
popup:buttons(
awful.util.table.join(
awful.button({}, 4, function()
local a = cal:get_date()
a.month = a.month + 1
cal:set_date(nil)
cal:set_date(a)
popup:set_widget(cal)
end),
awful.button({}, 5, function()
local a = cal:get_date()
a.month = a.month - 1
cal:set_date(nil)
cal:set_date(a)
popup:set_widget(cal)
end)
)
)
function calendar_widget.toggle()
if popup.visible then
-- to faster render the calendar refresh it and just hide
cal:set_date(nil) -- the new date is not set without removing the old one
cal:set_date(os.date('*t'))
popup:set_widget(nil) -- just in case
popup:set_widget(cal)
popup.visible = not popup.visible
else
if placement == 'top' then
awful.placement.top(popup, { margins = { top = 30 }, parent = awful.screen.focused() })
elseif placement == 'top_right' then
awful.placement.top_right(popup, { margins = { top = 30, right = 10}, parent = awful.screen.focused() })
elseif placement == 'bottom_right' then
awful.placement.bottom_right(popup, { margins = { bottom = 30, right = 10}, parent = awful.screen.focused() })
else
awful.placement.top(popup, { margins = { top = 30 }, parent = awful.screen.focused() })
end
popup.visible = true
end
end
return calendar_widget
end
return setmetatable(calendar_widget, { __call = function(_, ...)
return worker(...)
end })

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

31
widget/calendar.lua Normal file
View File

@@ -0,0 +1,31 @@
local awful = require('awful')
local wibox = require('wibox')
local beautiful = require('beautiful')
local dpi = require('beautiful').xresources.apply_dpi
local month_calendar = awful.widget.calendar_popup.month({
start_sunday = true,
style_month = {
border_width = dpi(0),
bg_color = beautiful.primary.hue_100,
padding = dpi(20)
},
style_header = {
border_width = 0,
fg_color = beautiful.accent.hue_400
},
style_weekday = {
border_width = 0
},
style_normal = {
border_width = 0
},
style_focus = {
border_width = dpi(0),
border_color = beautiful.fg_normal,
fg_color = beautiful.accent.hue_200,
bg_color = beautiful.primary.hue_100
}
})
return month_calendar

70
widget/calendar/init.lua Normal file
View File

@@ -0,0 +1,70 @@
local awful = require('awful')
local gears = require('gears')
local wibox = require('wibox')
local beautiful = require('beautiful')
local icons = require('theme.icons')
local clickable_container = require('widget.material.clickable-container')
local dpi = require('beautiful').xresources.apply_dpi
local styles = {}
local function rounded_shape(size, partial)
if partial then
return function(cr, width, height)
gears.shape.rectangle(cr, width + 5, height, 11)
end
else
return function(cr, width, height)
gears.shape.rectangle(cr, width, height, size)
end
end
end
styles.month = {padding = 5, bg_color = '#555555', shape = rounded_shape(10)}
styles.normal = {shape = rounded_shape(5)}
styles.focus = {
fg_color = beautiful.primary.hue_400, -- Current day Color
markup = function(t) return '<b>' .. t .. '</b>' end,
shape = rounded_shape(5, true)
}
styles.header = {
fg_color = beautiful.primary.hue_200, -- Month Name Color
markup = function(t) return '<b>' .. t .. '</b>' end,
shape = rounded_shape(10)
}
styles.weekday = {
fg_color = beautiful.background.hue_50, -- Day Color
markup = function(t) return '<b>' .. t .. '</b>' end,
shape = rounded_shape(5)
}
local function decorate_cell(widget, flag, date)
if flag == 'monthheader' and not styles.monthheader then flag = 'header' end
local props = styles[flag] or {}
if props.markup and widget.get_text and widget.set_markup then
widget:set_markup(props.markup(widget:get_text()))
end
local d = {
year = date.year,
month = (date.month or 1),
day = (date.day or 1)
}
local weekday = tonumber(os.date('%w', os.time(d)))
local ret = wibox.widget {
{
widget,
margins = (props.padding or 2) + (props.border_width or 0),
widget = wibox.container.margin
},
fg = props.fg_color or '#999999',
widget = wibox.container.background
}
return ret
end
local cal = wibox.widget {
date = os.date('*t'),
font = 'Roboto 10',
fn_embed = decorate_cell,
start_sunday = true,
widget = wibox.widget.calendar.month
}
return cal

5
widget/clock.lua Normal file
View File

@@ -0,0 +1,5 @@
local wibox = require('wibox')
local beautiful = require('beautiful')
local clock = wibox.widget.textclock('<span font="' .. beautiful.font .. '">%d/%m %H:%M</span>')
return clock

27
widget/cpu.lua Normal file
View File

@@ -0,0 +1,27 @@
local wibox = require('wibox')
local beautiful = require('beautiful')
local dpi = require('beautiful').xresources.apply_dpi
local watch = require('awful.widget.watch')
local cpu = wibox.widget.textbox()
local total_prev = 0
local idle_prev = 0
watch([[bash -c "cat /proc/stat | grep '^cpu '"]], 1, function(_, stdout)
local user, nice, system, idle, iowait, irq, softirq, steal, guest, guest_nice =
stdout:match('(%d+)%s(%d+)%s(%d+)%s(%d+)%s(%d+)%s(%d+)%s(%d+)%s(%d+)%s(%d+)%s(%d+)%s')
local total = user + nice + system + idle + iowait + irq + softirq + steal
local diff_idle = idle - idle_prev
local diff_total = total - total_prev
local diff_usage = (1000 * (diff_total - diff_idle) / diff_total + 5) / 10
cpu.text = math.floor(diff_usage) .. '%'
total_prev = total
idle_prev = idle
collectgarbage('collect')
end)
return cpu

50
widget/cpu/cpu-meter.lua Normal file
View File

@@ -0,0 +1,50 @@
local wibox = require('wibox')
local mat_list_item = require('widget.material.list-item')
local mat_slider = require('widget.material.slider')
local mat_icon = require('widget.material.icon')
local icons = require('theme.icons')
local watch = require('awful.widget.watch')
local dpi = require('beautiful').xresources.apply_dpi
local total_prev = 0
local idle_prev = 0
local slider =
wibox.widget {
read_only = true,
widget = mat_slider
}
watch(
[[bash -c "cat /proc/stat | grep '^cpu '"]],
1,
function(_, stdout)
local user, nice, system, idle, iowait, irq, softirq, steal, guest, guest_nice =
stdout:match('(%d+)%s(%d+)%s(%d+)%s(%d+)%s(%d+)%s(%d+)%s(%d+)%s(%d+)%s(%d+)%s(%d+)%s')
local total = user + nice + system + idle + iowait + irq + softirq + steal
local diff_idle = idle - idle_prev
local diff_total = total - total_prev
local diff_usage = (1000 * (diff_total - diff_idle) / diff_total + 5) / 10
slider:set_value(diff_usage)
total_prev = total
idle_prev = idle
collectgarbage('collect')
end
)
local cpu_meter =
wibox.widget {
wibox.widget {
icon = icons.chart,
size = dpi(24),
widget = mat_icon
},
slider,
widget = mat_list_item
}
return cpu_meter

View File

@@ -0,0 +1,36 @@
local wibox = require('wibox')
local mat_list_item = require('widget.material.list-item')
local mat_slider = require('widget.material.slider')
local mat_icon = require('widget.material.icon')
local icons = require('theme.icons')
local watch = require('awful.widget.watch')
local dpi = require('beautiful').xresources.apply_dpi
local slider =
wibox.widget {
read_only = true,
widget = mat_slider
}
watch(
[[bash -c "df -h /home|grep '^/' | awk '{print $5}'"]],
10,
function(_, stdout)
local space_consumed = stdout:match('(%d+)')
slider:set_value(tonumber(space_consumed))
collectgarbage('collect')
end
)
local harddrive_meter =
wibox.widget {
wibox.widget {
icon = icons.harddisk,
size = dpi(24),
widget = mat_icon
},
slider,
widget = mat_list_item
}
return harddrive_meter

View File

@@ -0,0 +1,43 @@
local wibox = require('wibox')
function build(widget, label)
local container = wibox.widget {
-- widget,
wibox.widget {
text = label,
widget = wibox.widget.textbox,
},
widget = wibox.container.background
}
local old_cursor, old_wibox
container:connect_signal('mouse::enter', function()
container.bg = '#ffffff11'
-- Hm, no idea how to get the wibox from this signal's arguments...
local w = _G.mouse.current_wibox
if w then
old_cursor, old_wibox = w.cursor, w
w.cursor = 'hand1'
end
end)
container:connect_signal('mouse::leave', function()
container.bg = '#ffffff00'
if old_wibox then
old_wibox.cursor = old_cursor
old_wibox = nil
end
end)
container:connect_signal('button::press', function()
container.bg = '#ffffff22'
end)
container:connect_signal('button::release', function()
container.bg = '#ffffff11'
end)
return container
end
return build

View File

@@ -0,0 +1,14 @@
local wibox = require('wibox')
local gears = require('gears')
local clickable_container = require('widget.material.clickable-container')
local dpi = require('beautiful').xresources.apply_dpi
function build(imagebox, args)
return wibox.widget {
imagebox,
shape = gears.shape.circle,
widget = clickable_container
}
end
return build

80
widget/material/icon.lua Normal file
View File

@@ -0,0 +1,80 @@
-- Default widget requirements
local base = require('wibox.widget.base')
local gtable = require('gears.table')
local setmetatable = setmetatable
-- Commons requirements
local wibox = require('wibox')
-- Local declarations
local mat_list_item = {mt = {}}
function mat_list_item:layout(_, width, height)
local layout = {}
-- Add divider if present
if self._private.icon then
table.insert(
layout,
base.place_widget_at(
self._private.imagebox,
width / 2 - self._private.size / 2,
height / 2 - self._private.size / 2,
self._private.size,
self._private.size
)
)
end
return layout
end
function mat_list_item:fit(_, width, height)
local min = math.min(width, height)
return min, min
end
function mat_list_item:set_icon(icon)
self._private.icon = icon
self._private.imagebox.image = icon
end
function mat_list_item:get_icon()
return self._private.icon
end
function mat_list_item:set_size(size)
self._private.size = size
self:emit_signal('widget::layout_changed')
end
function mat_list_item:get_size()
return self._private.size
end
local function new(icon, size)
local ret =
base.make_widget(
nil,
nil,
{
enable_properties = true
}
)
gtable.crush(ret, mat_list_item, true)
ret._private.icon = icon
ret._private.imagebox = wibox.widget.imagebox(icon)
ret._private.size = size
return ret
end
function mat_list_item.mt:__call(...)
return new(...)
end
--@DOC_widget_COMMON@
--@DOC_object_COMMON@
return setmetatable(mat_list_item, mat_list_item.mt)

View File

@@ -0,0 +1,186 @@
-- Default widget requirements
local base = require('wibox.widget.base')
local gtable = require('gears.table')
local setmetatable = setmetatable
local dpi = require('beautiful').xresources.apply_dpi
-- Commons requirements
local wibox = require('wibox')
local clickable_container = require('widget.material.clickable-container')
-- Local declarations
local mat_list_item = {
mt = {}
}
function mat_list_item:build_separator()
self._private.separator = wibox.widget {
orientation = 'horizontal',
forced_height = 1,
opacity = 0.08,
widget = wibox.widget.separator
}
self:emit_signal('widget::layout_changed')
end
function mat_list_item:build_clickable_container()
self._private.clickable_container = wibox.widget {
wibox.widget {
widget = wibox.widget.textbox
},
widget = clickable_container
}
self:emit_signal('widget::layout_changed')
end
function mat_list_item:layout(_, width, height)
local content_width = width - dpi(32)
local content_x = dpi(dpi(16))
local layout = {}
-- Add divider if present
if self._private.divider then
table.insert(layout, base.place_widget_at(self._private.separator, 0, 0, width, 1))
end
-- Add clickable_container if clickable
if self._private.clickable then
table.insert(layout, base.place_widget_at(self._private.clickable_container, 0, 0, width, height))
end
if self._private.prefix then
content_x = content_x + dpi(54)
content_width = content_width - dpi(54)
table.insert(layout, base.place_widget_at(self._private.prefix, dpi(16), 0, dpi(48), height))
end
if self._private.suffix then
content_width = content_width - dpi(54)
table.insert(layout, base.place_widget_at(self._private.suffix, width - dpi(40), dpi(12), width, height))
end
table.insert(layout, base.place_widget_at(self._private.content, content_x, 0, content_width, height))
return layout
end
function mat_list_item:fit(_, width)
return width, dpi(48)
end
---- Properties ----
-- Property clickable
function mat_list_item:set_clickable(value)
if self._private.clickable ~= value then
self._private.clickable = value
self:emit_signal('property::clickable')
self:emit_signal('widget::layout_changed')
if self._private.clickable and not self._private.clickable_container then
self:build_clickable_container()
end
end
end
function mat_list_item:get_clickable()
return self._private.clickable
end
-- Property divider
function mat_list_item:set_divider(value)
if self._private.divider ~= value then
self._private.divider = value
self:emit_signal('property::divider')
self:emit_signal('widget::layout_changed')
if self._private.divider and not self._private.separator then
self:build_separator()
end
end
end
function mat_list_item:get_divider()
return self._private.divider
end
function mat_list_item:set_prefix(widget)
if widget then
base.check_widget(widget)
end
self._private.prefix = widget
self:emit_signal('widget::layout_changed')
end
function mat_list_item:get_prefix()
return self._private.prefix
end
function mat_list_item:set_suffix(widget)
if widget then
base.check_widget(widget)
end
self._private.suffix = widget
self:emit_signal('widget::layout_changed')
end
function mat_list_item:get_suffix()
return self._private.suffix
end
--- The widget who will be the content.
-- @property content
-- @tparam widget widget The widget
function mat_list_item:set_content(widget)
if widget then
base.check_widget(widget)
end
self._private.content = widget
self:emit_signal('widget::layout_changed')
end
function mat_list_item:get_content()
return self._private.content
end
-- Get the number of children element
-- @treturn table The children
function mat_list_item:get_children()
return {self._private.widget}
end
-- Replace the layout children
-- This layout only accept one children, all others will be ignored
-- @tparam table children A table composed of valid widgets
function mat_list_item:set_children(children)
if not children[2] then
self:set_content(children[1])
else
self:set_prefix(children[1])
self:set_content(children[2])
end
if children[3] then
self:set_suffix(children[3])
end
end
local function new(widget)
local ret = base.make_widget(nil, nil, {
enable_properties = true
})
gtable.crush(ret, mat_list_item, true)
ret._private.content = widget
return ret
end
function mat_list_item.mt:__call(...)
return new(...)
end
-- @DOC_widget_COMMON@
-- @DOC_object_COMMON@
return setmetatable(mat_list_item, mat_list_item.mt)

117
widget/material/slider.lua Normal file
View File

@@ -0,0 +1,117 @@
-- Default widget requirements
local base = require('wibox.widget.base')
local gtable = require('gears.table')
local setmetatable = setmetatable
local dpi = require('beautiful').xresources.apply_dpi
-- Commons requirements
local wibox = require('wibox')
local gears = require('gears')
local beautiful = require('beautiful')
local mat_colors = require('theme.mat-colors')
-- Local declarations
local mat_slider = {
mt = {}
}
local properties = {
read_only = false
}
function mat_slider:set_value(value)
if self._private.value ~= value then
self._private.value = value
self._private.progress_bar:set_value(self._private.value)
self._private.slider:set_value(self._private.value)
self:emit_signal('property::value')
-- self:emit_signal('widget::layout_changed')
end
end
function mat_slider:get_value(value)
return self._private.value
end
function mat_slider:set_read_only(value)
if self._private.read_only ~= value then
self._private.read_only = value
self:emit_signal('property::read_only')
self:emit_signal('widget::layout_changed')
end
end
function mat_slider:get_read_only(value)
return self._private.read_only
end
function mat_slider:layout(_, width, height)
local layout = {}
table.insert(layout, base.place_widget_at(self._private.progress_bar, 0, dpi(21), width, height - dpi(42)))
if (not self._private.read_only) then
table.insert(layout, base.place_widget_at(self._private.slider, 0, dpi(6), width, height - dpi(12)))
end
return layout
end
function mat_slider:draw(_, cr, width, height)
if (self._private.read_only) then
self._private.slider.forced_height = 0
end
end
function mat_slider:fit(_, width, height)
return width, height
end
local function new(args)
local ret = base.make_widget(nil, nil, {
enable_properties = true
})
gtable.crush(ret._private, args or {})
gtable.crush(ret, mat_slider, true)
ret._private.progress_bar = wibox.widget {
max_value = 100,
value = 25,
forced_height = dpi(6),
paddings = 0,
shape = gears.shape.rounded_rect,
background_color = beautiful.primary.hue_100,
color = beautiful.accent.hue_400,
widget = wibox.widget.progressbar
}
ret._private.slider = wibox.widget {
forced_height = dpi(8),
bar_shape = gears.shape.rounded_rect,
bar_height = 0,
bar_color = beautiful.accent.hue_500,
handle_color = beautiful.accent.hue_300,
handle_shape = gears.shape.circle,
handle_border_color = '#00000012',
handle_border_width = dpi(3),
value = 25,
widget = wibox.widget.slider
}
ret._private.slider:connect_signal('property::value', function()
ret:set_value(ret._private.slider.value)
end)
ret._private.read_only = false
return ret
end
function mat_slider.mt:__call(...)
return new(...)
end
-- @DOC_widget_COMMON@
-- @DOC_object_COMMON@
return setmetatable(mat_slider, mat_slider.mt)

21
widget/memory.lua Normal file
View File

@@ -0,0 +1,21 @@
local wibox = require('wibox')
local beautiful = require('beautiful')
local watch = require('awful.widget.watch')
local memory = wibox.widget.textbox()
memory.font = beautiful.font
function round(exact, quantum)
local quant,frac = math.modf(exact/quantum)
return quantum * (quant + (frac > 0.5 and 1 or 0))
end
watch('bash -c "free | grep -z Mem.*Swap.*"', 1, function(_, stdout)
local total, used, free, shared, buff_cache, available, total_swap, used_swap, free_swap =
stdout:match('(%d+)%s*(%d+)%s*(%d+)%s*(%d+)%s*(%d+)%s*(%d+)%s*Swap:%s*(%d+)%s*(%d+)%s*(%d+)')
memory.text = round((used / 1048576), 0.01) .. 'GB'
collectgarbage('collect')
end)
return memory

37
widget/ram/ram-meter.lua Normal file
View File

@@ -0,0 +1,37 @@
local wibox = require('wibox')
local mat_list_item = require('widget.material.list-item')
local mat_slider = require('widget.material.slider')
local mat_icon = require('widget.material.icon')
local icons = require('theme.icons')
local watch = require('awful.widget.watch')
local dpi = require('beautiful').xresources.apply_dpi
local slider =
wibox.widget {
read_only = true,
widget = mat_slider
}
watch(
'bash -c "free | grep -z Mem.*Swap.*"',
1,
function(_, stdout)
local total, used, free, shared, buff_cache, available, total_swap, used_swap, free_swap =
stdout:match('(%d+)%s*(%d+)%s*(%d+)%s*(%d+)%s*(%d+)%s*(%d+)%s*Swap:%s*(%d+)%s*(%d+)%s*(%d+)')
slider:set_value(used / total * 100)
collectgarbage('collect')
end
)
local ram_meter =
wibox.widget {
wibox.widget {
icon = icons.memory,
size = dpi(24),
widget = mat_icon
},
slider,
widget = mat_list_item
}
return ram_meter

12
widget/storage.lua Normal file
View File

@@ -0,0 +1,12 @@
local wibox = require('wibox')
local watch = require('awful.widget.watch')
local beautiful = require('beautiful')
local storage = wibox.widget.textbox()
storage.font = beautiful.font
watch('bash -c "df -h $HOME | awk \'/[0-9]/ {print $2-$3}\'"', 30, function(_, stdout)
storage.text = stdout
end)
return storage

133
widget/tag-list.lua Normal file
View File

@@ -0,0 +1,133 @@
local awful = require('awful')
local wibox = require('wibox')
local dpi = require('beautiful').xresources.apply_dpi
local capi = {
button = _G.button
}
local clickable_container = require('widget.material.clickable-container')
local modkey = require('configuration.keys.mod').modKey
--- Common method to create buttons.
-- @tab buttons
-- @param object
-- @treturn table
local function create_buttons(buttons, object)
if buttons then
local btns = {}
for _, b in ipairs(buttons) do
-- Create a proxy button object: it will receive the real
-- press and release events, and will propagate them to the
-- button object the user provided, but with the object as
-- argument.
local btn = capi.button {
modifiers = b.modifiers,
button = b.button
}
btn:connect_signal('press', function()
b:emit_signal('press', object)
end)
btn:connect_signal('release', function()
b:emit_signal('release', object)
end)
btns[#btns + 1] = btn
end
return btns
end
end
local function list_update(w, buttons, label, data, objects)
-- update the widgets, creating them if needed
w:reset()
for i, o in ipairs(objects) do
local cache = data[o]
local ib, tb, bgb, tbm, ibm, l, bg_clickable
if cache then
ib = cache.ib
tb = cache.tb
bgb = cache.bgb
tbm = cache.tbm
ibm = cache.ibm
else
ib = wibox.widget.imagebox()
tb = wibox.widget.textbox()
bgb = wibox.container.background()
tbm = wibox.container.margin(tb, dpi(6), dpi(6), dpi(4), dpi(4))
ibm = wibox.container.margin(ib, dpi(8), dpi(8), dpi(9), dpi(9))
l = wibox.layout.fixed.horizontal()
bg_clickable = clickable_container()
-- All of this is added in a fixed widget
l:fill_space(true)
-- l:add(ibm)
l:add(tbm)
bg_clickable:set_widget(l)
-- And all of this gets a background
bgb:set_widget(bg_clickable)
bgb:buttons(create_buttons(buttons, o))
data[o] = {
ib = ib,
tb = tb,
bgb = bgb,
tbm = tbm,
ibm = ibm
}
end
local text, bg, bg_image, icon, args = label(o, tb)
args = args or {}
if text == nil or text == '' then
tbm:set_margins(0)
else
if not tb:set_markup_silently(text) then
tb:set_markup('<i>&lt;Invalid text&gt;</i>')
end
end
bgb:set_bg(bg)
if type(bg_image) == 'function' then
bg_image = bg_image(tb, o, nil, objects, i)
end
bgb:set_bgimage(bg_image)
if icon then
ib.image = icon
else
ibm:set_margins(0)
end
bgb.shape = args.shape
w:add(bgb)
end
end
local TagList = function(s)
return awful.widget.taglist(s, awful.widget.taglist.filter.all,
awful.util.table.join(awful.button({}, 1, function(t)
t:view_only()
_G._splash_to_current_tag()
end), awful.button({modkey}, 1, function(t)
if _G.client.focus then
_G.client.focus:move_to_tag(t)
t:view_only()
end
_G._splash_to_current_tag()
end), awful.button({}, 3, function()
awful.tag.viewtoggle()
_G._splash_to_current_tag()
end), awful.button({modkey}, 3, function(t)
if _G.client.focus then
_G.client.focus:toggle_tag(t)
end
_G._splash_to_current_tag()
end), awful.button({}, 4, function(t)
awful.tag.viewprev(t.screen)
_G._splash_to_current_tag()
end), awful.button({}, 5, function(t)
awful.tag.viewnext(t.screen)
_G._splash_to_current_tag()
end)), {}, list_update, wibox.layout.fixed.horizontal())
end
return TagList

147
widget/task-list.lua Normal file
View File

@@ -0,0 +1,147 @@
local awful = require('awful')
local wibox = require('wibox')
local dpi = require('beautiful').xresources.apply_dpi
local capi = {
button = _G.button
}
local gears = require('gears')
local clickable_container = require('widget.material.clickable-container')
local tasklist_mode = 'text'
local beautiful = require('beautiful')
--- Common method to create buttons.
-- @tab buttons
-- @param object
-- @treturn table
local function create_buttons(buttons, object)
if buttons then
local btns = {}
for _, b in ipairs(buttons) do
-- Create a proxy button object: it will receive the real
-- press and release events, and will propagate them to the
-- button object the user provided, but with the object as
-- argument.
local btn = capi.button {
modifiers = b.modifiers,
button = b.button
}
btn:connect_signal('press', function()
b:emit_signal('press', object)
end)
btn:connect_signal('release', function()
b:emit_signal('release', object)
end)
btns[#btns + 1] = btn
end
return btns
end
end
local function list_update(w, buttons, label, data, objects)
-- update the widgets, creating them if needed
w:reset()
for i, o in ipairs(objects) do
local cache = data[o]
local ib, tb, bgb, tbm, ibm, l, ll, bg_clickable
if cache then
ib = cache.ib
tb = cache.tb
bgb = cache.bgb
tbm = cache.tbm
ibm = cache.ibm
else
ib = wibox.widget.imagebox()
tb = wibox.widget.textbox()
bg_clickable = clickable_container()
bgb = wibox.container.background()
tbm = wibox.container.margin(tb, dpi(4), dpi(4), dpi(1), dpi(1))
ibm = wibox.container.margin(ib, dpi(1), dpi(1), dpi(1), dpi(1))
l = wibox.layout.fixed.horizontal()
ll = wibox.layout.flex.horizontal()
-- All of this is added in a fixed widget
l:fill_space(true)
l:add(ibm)
l:add(tbm)
ll:add(l)
bg_clickable:set_widget(ll)
-- And all of this gets a background
bgb:set_widget(bg_clickable)
l:buttons(create_buttons(buttons, o))
data[o] = {
ib = ib,
tb = tb,
bgb = bgb,
tbm = tbm,
ibm = ibm
}
end
local text, bg, bg_image, icon, args = label(o, tb)
args = args or {}
-- The text might be invalid, so use pcall.
if tasklist_mode == 'icon' then
text = nil
elseif tasklist_mode == 'text' then
icon = nil
end
if text == nil or text == '' then
tbm:set_margins(0)
else
if not tb:set_markup_silently(text) then
tb:set_markup('<i>&lt;Invalid text&gt;</i>')
end
end
bgb:set_bg(bg)
if type(bg_image) == 'function' then
bg_image = bg_image(tb, o, nil, objects, i)
end
bgb:set_bgimage(bg_image)
if icon then
ib.image = icon
ib.resize = true
else
ibm:set_margins(0)
end
bgb.shape = args.shape
bgb.shape_border_width = args.shape_border_width
bgb.shape_border_color = args.shape_border_color
tb.align = 'center'
w:add(bgb)
end
end
local tasklist_buttons = awful.util.table.join(awful.button({}, 1, function(c)
if c == _G.client.focus then
c.minimized = true
else
-- Without this, the following
-- :isvisible() makes no sense
c.minimized = false
if not c:isvisible() and c.first_tag then
c.first_tag:view_only()
end
-- This will also un-minimize
-- the client, if needed
_G.client.focus = c
c:raise()
end
end), awful.button({}, 4, function()
awful.client.focus.byidx(1)
end), awful.button({}, 5, function()
awful.client.focus.byidx(-1)
end), awful.button({}, 2, function(c)
c.kill(c)
end))
local TaskList = function(s)
return awful.widget.tasklist(s, awful.widget.tasklist.filter.currenttags, tasklist_buttons, nil, list_update)
end
return TaskList

View File

@@ -0,0 +1,37 @@
local wibox = require('wibox')
local mat_list_item = require('widget.material.list-item')
local mat_slider = require('widget.material.slider')
local mat_icon = require('widget.material.icon')
local icons = require('theme.icons')
local watch = require('awful.widget.watch')
local dpi = require('beautiful').xresources.apply_dpi
local slider =
wibox.widget {
read_only = true,
widget = mat_slider
}
local max_temp = 80
watch(
'bash -c "cat /sys/class/thermal/thermal_zone0/temp"',
1,
function(_, stdout)
local temp = stdout:match('(%d+)')
slider:set_value((temp / 1000) / max_temp * 100)
collectgarbage('collect')
end
)
local temperature_meter =
wibox.widget {
wibox.widget {
icon = icons.thermometer,
size = dpi(24),
widget = mat_icon
},
slider,
widget = mat_list_item
}
return temperature_meter

12
widget/temprature.lua Normal file
View File

@@ -0,0 +1,12 @@
local wibox = require('wibox')
local watch = require('awful.widget.watch')
local beautiful = require('beautiful')
local temprature = wibox.widget.textbox()
temprature.font = beautiful.font
watch('bash -c "sensors | awk \'/Core 0/ {print substr($3, 2) }\'"', 30, function(_, stdout)
temprature.text = stdout
end)
return temprature

50
widget/volume.lua Normal file
View File

@@ -0,0 +1,50 @@
local awful = require("awful")
local wibox = require('wibox')
local mat_list_item = require('widget.material.list-item')
local dpi = require('beautiful').xresources.apply_dpi
local watch = require('awful.widget.watch')
local beautiful = require('beautiful')
local volume_icon = wibox.widget.textbox()
volume_icon.font = beautiful.icon_font
local volume_widget = wibox.widget.textbox()
volume_widget.align = 'center'
volume_widget.valign = 'center'
volume_widget.font = beautiful.font
local volume
function update_volume()
awful.spawn.easy_async_with_shell("bash -c 'amixer -D pulse sget Master'", function(stdout)
volume = string.match(stdout, '(%d?%d?%d)%%')
awful.spawn.easy_async_with_shell("bash -c 'pacmd list-sinks | awk '/muted/ { print $2 }''", function(muted)
muted = string.gsub(muted, "%s+", "")
if muted == 'muted:no' and (volume > '50' or volume == '100') then
volume_icon.text = ''
elseif muted == 'muted:no' and volume <= '50' and volume > '0' then
volume_icon.text = '奔'
elseif muted == 'muted:yes' then
volume_icon.text = ''
elseif volume == '0' then
volume_icon.text = ''
end
volume_widget.text = volume
end)
collectgarbage('collect')
end)
end
watch('bash -c', 3, function(_, stdout)
update_volume()
end)
return wibox.widget {
wibox.widget{
volume_icon,
fg = beautiful.accent.hue_100,
widget = wibox.container.background
},
volume_widget,
spacing = dpi(2),
layout = wibox.layout.fixed.horizontal
}

View File

@@ -0,0 +1,28 @@
local wibox = require('wibox')
local mat_list_item = require('widget.material.list-item')
local mat_slider = require('widget.material.slider')
local mat_icon_button = require('widget.material.icon-button')
local icons = require('theme.icons')
local watch = require('awful.widget.watch')
local spawn = require('awful.spawn')
local slider = wibox.widget {read_only = false, widget = mat_slider}
slider:connect_signal('property::value', function()
spawn('amixer -D pulse sset Master ' .. slider.value .. '%')
end)
watch([[bash -c "amixer -D pulse sget Master"]], 1, function(_, stdout)
local mute = string.match(stdout, '%[(o%D%D?)%]')
local volume = string.match(stdout, '(%d?%d?%d)%%')
slider:set_value(tonumber(volume))
collectgarbage('collect')
end)
local icon = wibox.widget {image = icons.volume, widget = wibox.widget.imagebox}
local button = mat_icon_button(icon)
local volume_setting = wibox.widget {button, slider, widget = mat_list_item}
return volume_setting

560
widget/weather.lua Normal file
View File

@@ -0,0 +1,560 @@
-------------------------------------------------
-- Weather Widget based on the OpenWeatherMap
-- https://openweathermap.org/
--
-- @author Pavel Makhov
-- @copyright 2020 Pavel Makhov
-------------------------------------------------
local awful = require("awful")
local watch = require("awful.widget.watch")
local json = require("json")
local naughty = require("naughty")
local wibox = require("wibox")
local gears = require("gears")
local beautiful = require("beautiful")
local HOME_DIR = os.getenv("HOME")
local WIDGET_DIR = HOME_DIR .. '/.config/awesome/awesome-wm-widgets/weather-widget'
local GET_FORECAST_CMD = [[bash -c "curl -s --show-error -X GET '%s'"]]
local function show_warning(message)
naughty.notify {
preset = naughty.config.presets.critical,
title = 'Weather Widget',
text = message
}
end
local weather_widget = {}
local warning_shown = false
local tooltip = awful.tooltip {
mode = 'outside',
preferred_positions = {'bottom'}
}
local weather_popup = awful.popup {
ontop = true,
visible = false,
shape = gears.shape.rounded_rect,
border_width = 1,
border_color = beautiful.bg_focus,
maximum_width = 400,
offset = {y = 5},
widget = {}
}
--- Maps openWeatherMap icon name to file name w/o extension
local icon_map = {
["01d"] = "clear-sky",
["02d"] = "few-clouds",
["03d"] = "scattered-clouds",
["04d"] = "broken-clouds",
["09d"] = "shower-rain",
["10d"] = "rain",
["11d"] = "thunderstorm",
["13d"] = "snow",
["50d"] = "mist",
["01n"] = "clear-sky-night",
["02n"] = "few-clouds-night",
["03n"] = "scattered-clouds-night",
["04n"] = "broken-clouds-night",
["09n"] = "shower-rain-night",
["10n"] = "rain-night",
["11n"] = "thunderstorm-night",
["13n"] = "snow-night",
["50n"] = "mist-night"
}
--- Return wind direction as a string
local function to_direction(degrees)
-- Ref: https://www.campbellsci.eu/blog/convert-wind-directions
if degrees == nil then return "Unknown dir" end
local directions = {
"N", "NNE", "NE", "ENE", "E", "ESE", "SE", "SSE", "S", "SSW", "SW",
"WSW", "W", "WNW", "NW", "NNW", "N"
}
return directions[math.floor((degrees % 360) / 22.5) + 1]
end
--- Convert degrees Celsius to Fahrenheit
local function celsius_to_fahrenheit(c) return c * 9 / 5 + 32 end
-- Convert degrees Fahrenheit to Celsius
local function fahrenheit_to_celsius(f) return (f - 32) * 5 / 9 end
local function gen_temperature_str(temp, fmt_str, show_other_units, units)
local temp_str = string.format(fmt_str, temp)
local s = temp_str .. '°' .. (units == 'metric' and 'C' or 'F')
if (show_other_units) then
local temp_conv, units_conv
if (units == 'metric') then
temp_conv = celsius_to_fahrenheit(temp)
units_conv = 'F'
else
temp_conv = fahrenheit_to_celsius(temp)
units_conv = 'C'
end
local temp_conv_str = string.format(fmt_str, temp_conv)
s = s .. ' ' .. '(' .. temp_conv_str .. '°' .. units_conv .. ')'
end
return s
end
local function uvi_index_color(uvi)
local color
if uvi >= 0 and uvi < 3 then color = '#A3BE8C'
elseif uvi >= 3 and uvi < 6 then color = '#EBCB8B'
elseif uvi >= 6 and uvi < 8 then color = '#D08770'
elseif uvi >= 8 and uvi < 11 then color = '#BF616A'
elseif uvi >= 11 then color = '#B48EAD'
end
return '<span weight="bold" foreground="' .. color .. '">' .. uvi .. '</span>'
end
local function worker(user_args)
local args = user_args or {}
--- Validate required parameters
if args.coordinates == nil or args.api_key == nil then
show_warning('Required parameters are not set: ' ..
(args.coordinates == nil and '<b>coordinates</b>' or '') ..
(args.api_key == nil and ', <b>api_key</b> ' or ''))
return
end
local coordinates = args.coordinates
local api_key = args.api_key
local font_name = args.font_name or beautiful.font:gsub("%s%d+$", "")
local units = args.units or 'metric'
local time_format_12h = args.time_format_12h
local both_units_widget = args.both_units_widget or false
local show_hourly_forecast = args.show_hourly_forecast
local show_daily_forecast = args.show_daily_forecast
local icon_pack_name = args.icons or 'weather-underground-icons'
local icons_extension = args.icons_extension or '.png'
local timeout = args.timeout or 120
local ICONS_DIR = WIDGET_DIR .. '/icons/' .. icon_pack_name .. '/'
local owm_one_cal_api =
('https://api.openweathermap.org/data/2.5/onecall' ..
'?lat=' .. coordinates[1] .. '&lon=' .. coordinates[2] .. '&appid=' .. api_key ..
'&units=' .. units .. '&exclude=minutely' ..
(show_hourly_forecast == false and ',hourly' or '') ..
(show_daily_forecast == false and ',daily' or ''))
weather_widget = wibox.widget {
{
{
{
{
id = 'icon',
resize = true,
widget = wibox.widget.imagebox
},
valign = 'center',
widget = wibox.container.place,
},
{
id = 'txt',
widget = wibox.widget.textbox
},
layout = wibox.layout.fixed.horizontal,
},
margins = 4,
layout = wibox.container.margin
},
shape = function(cr, width, height)
gears.shape.rounded_rect(cr, width, height, 4)
end,
widget = wibox.container.background,
set_image = function(self, path)
self:get_children_by_id('icon')[1].image = path
end,
set_text = function(self, text)
self:get_children_by_id('txt')[1].text = text
end,
is_ok = function(self, is_ok)
if is_ok then
self:get_children_by_id('icon')[1]:set_opacity(1)
self:get_children_by_id('icon')[1]:emit_signal('widget:redraw_needed')
else
self:get_children_by_id('icon')[1]:set_opacity(0.2)
self:get_children_by_id('icon')[1]:emit_signal('widget:redraw_needed')
end
end
}
local current_weather_widget = wibox.widget {
{
{
{
id = 'icon',
resize = true,
forced_width = 128,
forced_height = 128,
widget = wibox.widget.imagebox
},
align = 'center',
widget = wibox.container.place
},
{
id = 'description',
font = font_name .. ' 10',
align = 'center',
widget = wibox.widget.textbox
},
forced_width = 128,
layout = wibox.layout.align.vertical
},
{
{
{
id = 'temp',
font = font_name .. ' 36',
widget = wibox.widget.textbox
},
{
id = 'feels_like_temp',
align = 'center',
font = font_name .. ' 9',
widget = wibox.widget.textbox
},
layout = wibox.layout.fixed.vertical
},
{
{
id = 'wind',
font = font_name .. ' 9',
widget = wibox.widget.textbox
},
{
id = 'humidity',
font = font_name .. ' 9',
widget = wibox.widget.textbox
},
{
id = 'uv',
font = font_name .. ' 9',
widget = wibox.widget.textbox
},
expand = 'inside',
layout = wibox.layout.align.vertical
},
spacing = 16,
forced_width = 150,
layout = wibox.layout.fixed.vertical
},
forced_width = 300,
layout = wibox.layout.flex.horizontal,
update = function(self, weather)
self:get_children_by_id('icon')[1]:set_image(
ICONS_DIR .. icon_map[weather.weather[1].icon] .. icons_extension)
self:get_children_by_id('temp')[1]:set_text(gen_temperature_str(weather.temp, '%.0f', false, units))
self:get_children_by_id('feels_like_temp')[1]:set_text(
'Feels like ' .. gen_temperature_str(weather.feels_like, '%.0f', false, units))
self:get_children_by_id('description')[1]:set_text(weather.weather[1].description)
self:get_children_by_id('wind')[1]:set_markup(
'Wind: <b>' .. weather.wind_speed .. 'm/s (' .. to_direction(weather.wind_deg) .. ')</b>')
self:get_children_by_id('humidity')[1]:set_markup('Humidity: <b>' .. weather.humidity .. '%</b>')
self:get_children_by_id('uv')[1]:set_markup('UV: ' .. uvi_index_color(weather.uvi))
end
}
local daily_forecast_widget = {
forced_width = 300,
layout = wibox.layout.flex.horizontal,
update = function(self, forecast, timezone_offset)
local count = #self
for i = 0, count do self[i]=nil end
for i, day in ipairs(forecast) do
if i > 5 then break end
local day_forecast = wibox.widget {
{
text = os.date('%a', tonumber(day.dt) + tonumber(timezone_offset)),
align = 'center',
font = font_name .. ' 9',
widget = wibox.widget.textbox
},
{
{
{
image = ICONS_DIR .. icon_map[day.weather[1].icon] .. icons_extension,
resize = true,
forced_width = 48,
forced_height = 48,
widget = wibox.widget.imagebox
},
align = 'center',
layout = wibox.container.place
},
{
text = day.weather[1].description,
font = font_name .. ' 8',
align = 'center',
forced_height = 50,
widget = wibox.widget.textbox
},
layout = wibox.layout.fixed.vertical
},
{
{
text = gen_temperature_str(day.temp.day, '%.0f', false, units),
align = 'center',
font = font_name .. ' 9',
widget = wibox.widget.textbox
},
{
text = gen_temperature_str(day.temp.night, '%.0f', false, units),
align = 'center',
font = font_name .. ' 9',
widget = wibox.widget.textbox
},
layout = wibox.layout.fixed.vertical
},
spacing = 8,
layout = wibox.layout.fixed.vertical
}
table.insert(self, day_forecast)
end
end
}
local hourly_forecast_graph = wibox.widget {
step_width = 12,
color = '#EBCB8B',
background_color = beautiful.bg_normal,
forced_height = 100,
forced_width = 300,
widget = wibox.widget.graph,
set_max_value = function(self, new_max_value)
self.max_value = new_max_value
end,
set_min_value = function(self, new_min_value)
self.min_value = new_min_value
end
}
local hourly_forecast_negative_graph = wibox.widget {
step_width = 12,
color = '#5E81AC',
background_color = beautiful.bg_normal,
forced_height = 100,
forced_width = 300,
widget = wibox.widget.graph,
set_max_value = function(self, new_max_value)
self.max_value = new_max_value
end,
set_min_value = function(self, new_min_value)
self.min_value = new_min_value
end
}
local hourly_forecast_widget = {
layout = wibox.layout.fixed.vertical,
update = function(self, hourly)
local hours_below = {
id = 'hours',
forced_width = 300,
layout = wibox.layout.flex.horizontal
}
local temp_below = {
id = 'temp',
forced_width = 300,
layout = wibox.layout.flex.horizontal
}
local max_temp = -1000
local min_temp = 1000
local values = {}
for i, hour in ipairs(hourly) do
if i > 25 then break end
values[i] = hour.temp
if max_temp < hour.temp then max_temp = hour.temp end
if min_temp > hour.temp then min_temp = hour.temp end
if (i - 1) % 5 == 0 then
table.insert(hours_below, wibox.widget {
text = os.date(time_format_12h and '%I%p' or '%H:00', tonumber(hour.dt)),
align = 'center',
font = font_name .. ' 9',
widget = wibox.widget.textbox
})
table.insert(temp_below, wibox.widget {
markup = '<span foreground="'
.. (tonumber(hour.temp) > 0 and '#2E3440' or '#ECEFF4') .. '">'
.. string.format('%.0f', hour.temp) .. '°' .. '</span>',
align = 'center',
font = font_name .. ' 9',
widget = wibox.widget.textbox
})
end
end
hourly_forecast_graph:set_max_value(math.max(max_temp, math.abs(min_temp)))
hourly_forecast_graph:set_min_value(min_temp > 0 and min_temp * 0.7 or 0) -- move graph a bit up
hourly_forecast_negative_graph:set_max_value(math.abs(min_temp))
hourly_forecast_negative_graph:set_min_value(max_temp < 0 and math.abs(max_temp) * 0.7 or 0)
for _, value in ipairs(values) do
if value >= 0 then
hourly_forecast_graph:add_value(value)
hourly_forecast_negative_graph:add_value(0)
else
hourly_forecast_graph:add_value(0)
hourly_forecast_negative_graph:add_value(math.abs(value))
end
end
local count = #self
for i = 0, count do self[i]=nil end
-- all temperatures are positive
if min_temp > 0 then
table.insert(self, wibox.widget{
{
hourly_forecast_graph,
reflection = {horizontal = true},
widget = wibox.container.mirror
},
{
temp_below,
valign = 'bottom',
widget = wibox.container.place
},
id = 'graph',
layout = wibox.layout.stack
})
table.insert(self, hours_below)
-- all temperatures are negative
elseif max_temp < 0 then
table.insert(self, hours_below)
table.insert(self, wibox.widget{
{
hourly_forecast_negative_graph,
reflection = {horizontal = true, vertical = true},
widget = wibox.container.mirror
},
{
temp_below,
valign = 'top',
widget = wibox.container.place
},
id = 'graph',
layout = wibox.layout.stack
})
-- there are both negative and positive temperatures
else
table.insert(self, wibox.widget{
{
hourly_forecast_graph,
reflection = {horizontal = true},
widget = wibox.container.mirror
},
{
temp_below,
valign = 'bottom',
widget = wibox.container.place
},
id = 'graph',
layout = wibox.layout.stack
})
table.insert(self, wibox.widget{
{
hourly_forecast_negative_graph,
reflection = {horizontal = true, vertical = true},
widget = wibox.container.mirror
},
{
hours_below,
valign = 'top',
widget = wibox.container.place
},
id = 'graph',
layout = wibox.layout.stack
})
end
end
}
local function update_widget(widget, stdout, stderr)
if stderr ~= '' then
if not warning_shown then
if (stderr ~= 'curl: (52) Empty reply from server'
and stderr ~= 'curl: (28) Failed to connect to api.openweathermap.org port 443: Connection timed out'
and stderr:find('^curl: %(18%) transfer closed with %d+ bytes remaining to read$') ~= nil
) then
show_warning(stderr)
end
warning_shown = true
widget:is_ok(false)
tooltip:add_to_object(widget)
widget:connect_signal('mouse::enter', function() tooltip.text = stderr end)
end
return
end
warning_shown = false
tooltip:remove_from_object(widget)
widget:is_ok(true)
local result = json.decode(stdout)
widget:set_image(ICONS_DIR .. icon_map[result.current.weather[1].icon] .. icons_extension)
widget:set_text(gen_temperature_str(result.current.temp, '%.0f', both_units_widget, units))
current_weather_widget:update(result.current)
local final_widget = {
current_weather_widget,
spacing = 16,
layout = wibox.layout.fixed.vertical
}
if show_hourly_forecast then
hourly_forecast_widget:update(result.hourly)
table.insert(final_widget, hourly_forecast_widget)
end
if show_daily_forecast then
daily_forecast_widget:update(result.daily, result.timezone_offset)
table.insert(final_widget, daily_forecast_widget)
end
weather_popup:setup({
{
final_widget,
margins = 10,
widget = wibox.container.margin
},
bg = beautiful.bg_normal,
widget = wibox.container.background
})
end
weather_widget:buttons(gears.table.join(awful.button({}, 1, function()
if weather_popup.visible then
weather_widget:set_bg('#00000000')
weather_popup.visible = not weather_popup.visible
else
weather_widget:set_bg(beautiful.bg_focus)
weather_popup:move_next_to(mouse.current_widget_geometry)
end
end)))
watch(
string.format(GET_FORECAST_CMD, owm_one_cal_api),
timeout, -- API limit is 1k req/day; day has 1440 min; every 2 min is good
update_widget, weather_widget
)
return weather_widget
end
return setmetatable(weather_widget, {__call = function(_, ...) return worker(...) end})