diff --git a/drawing/FileSystem.lua b/drawing/FileSystem.lua new file mode 100644 index 0000000..f1ae53d --- /dev/null +++ b/drawing/FileSystem.lua @@ -0,0 +1,77 @@ +local Widget = require 'Widget' +local Text = require 'Text' +local Line = require 'Line' +local TextColumn = require 'TextColumn' +local CompoundBar = require 'CompoundBar' +local util = require 'util' +local Patterns = require 'Patterns' + +local __string_match = string.match + +local _FS_PATHS_ = {'/', '/boot', '/var', '/home', '/mnt/data', '/usr/local/opt'} +local _MODULE_Y_ = 165 +local _SPACING_ = 20 +local _BAR_PAD_ = 100 + +local FS_NUM = #_FS_PATHS_ + +local header = Widget.Header{ + x = _G_INIT_DATA_.RIGHT_X, + y = _MODULE_Y_, + width = _G_INIT_DATA_.SECTION_WIDTH, + header = 'FILE SYSTEMS' +} + +local labels = Widget.TextColumn{ + x = _G_INIT_DATA_.RIGHT_X, + y = header.bottom_y, + spacing = _SPACING_, + 'root', + 'boot', + 'var', + 'home', + 'data', + 'lopt' +} + +local conky_used_perc = {} + +for i, v in pairs(_FS_PATHS_) do + conky_used_perc[i] = '${fs_used_perc '..v..'}' +end + +local bars = Widget.CompoundBar{ + x = _G_INIT_DATA_.RIGHT_X + _BAR_PAD_, + y = header.bottom_y, + length = _G_INIT_DATA_.SECTION_WIDTH - _BAR_PAD_, + spacing = _SPACING_, + num_bars = FS_NUM, + critical_limit = '>0.8' +} + +Widget = nil +Patterns = nil + +_SPACING_ = nil +_BAR_PAD_ = nil +_FS_PATHS_ = nil + +local update = function(cr) + for i = 1, FS_NUM do + local percent = util.conky_numeric(conky_used_perc[i]) + CompoundBar.set(bars, i, percent * 0.01) + end +end + +local draw = function(cr, current_interface, trigger) + if trigger == 0 then update(cr) end + + if current_interface == 0 then + Text.draw(header.text, cr) + Line.draw(header.underline, cr) + TextColumn.draw(labels, cr) + CompoundBar.draw(bars, cr) + end +end + +return draw diff --git a/drawing/Graphics.lua b/drawing/Graphics.lua new file mode 100644 index 0000000..48267a9 --- /dev/null +++ b/drawing/Graphics.lua @@ -0,0 +1,333 @@ +local Widget = require 'Widget' +local CriticalText = require 'CriticalText' +local Text = require 'Text' +local TextColumn = require 'TextColumn' +local Line = require 'Line' +local LabelPlot = require 'LabelPlot' +local util = require 'util' +local Patterns = require 'Patterns' + +local __tonumber = tonumber +local __string_find = string.find +local __string_match = string.match + +local _MODULE_Y_ = 145 +local _SEPARATOR_SPACING_ = 20 +local _TEXT_SPACING_ = 20 +local _PLOT_SEC_BREAK_ = 20 +local _PLOT_HEIGHT_ = 56 + +local header = Widget.Header{ + x = _G_INIT_DATA_.LEFT_X, + y = _MODULE_Y_, + width = _G_INIT_DATA_.SECTION_WIDTH, + header = 'NVIDIA GRAPHICS' +} + +local _RIGHT_X_ = _G_INIT_DATA_.LEFT_X + _G_INIT_DATA_.SECTION_WIDTH + +local status = { + label = Widget.Text{ + x = _G_INIT_DATA_.LEFT_X, + y = header.bottom_y, + text = 'Status' + }, + value = Widget.Text{ + x = _RIGHT_X_, + y = header.bottom_y, + x_align = 'right', + text_color = Patterns.BLUE, + text = '' + } +} + +local _SEP_Y_1_ = header.bottom_y + _SEPARATOR_SPACING_ + +local separator1 = Widget.Line{ + p1 = {x = _G_INIT_DATA_.LEFT_X, y = _SEP_Y_1_}, + p2 = {x = _RIGHT_X_, y = _SEP_Y_1_} +} + +local _INTERNAL_TEMP_Y_ = _SEP_Y_1_ + _SEPARATOR_SPACING_ + +local internal_temp = { + label = Widget.Text{ + x = _G_INIT_DATA_.LEFT_X, + y = _INTERNAL_TEMP_Y_, + text = 'Internal Temperature' + }, + value = Widget.CriticalText{ + x = _RIGHT_X_, + y = _INTERNAL_TEMP_Y_, + x_align = 'right', + text_color = Patterns.BLUE, + text = '' + } +} + +local _PCI_UTIL_Y_ = _INTERNAL_TEMP_Y_ + _TEXT_SPACING_ + +local pci_util = { + label = Widget.Text{ + x = _G_INIT_DATA_.LEFT_X, + y = _PCI_UTIL_Y_, + text = 'PCI Utilization' + }, + value = Widget.Text{ + x = _RIGHT_X_, + y = _PCI_UTIL_Y_, + x_align = 'right', + text_color = Patterns.BLUE, + text = '' + } +} + +local _SEP_Y_2_ = _PCI_UTIL_Y_ + _SEPARATOR_SPACING_ + +local separator2 = Widget.Line{ + p1 = {x = _G_INIT_DATA_.LEFT_X, y = _SEP_Y_2_}, + p2 = {x = _RIGHT_X_, y = _SEP_Y_2_} +} + +local _CLOCK_SPEED_Y_ = _SEP_Y_2_ + _SEPARATOR_SPACING_ + +local clock_speed = { + labels = Widget.TextColumn{ + x = _G_INIT_DATA_.LEFT_X, + y = _CLOCK_SPEED_Y_, + spacing = _TEXT_SPACING_, + 'GPU Clock Speed', + 'Memory Clock Speed' + }, + values = Widget.TextColumn{ + x = _G_INIT_DATA_.LEFT_X + _G_INIT_DATA_.SECTION_WIDTH, + y = _CLOCK_SPEED_Y_, + spacing = _TEXT_SPACING_, + x_align = 'right', + text_color = Patterns.BLUE, + num_rows = 2 + } +} + +local _SEP_Y_3_ = _CLOCK_SPEED_Y_ + _TEXT_SPACING_ * 2 + +local separator3 = Widget.Line{ + p1 = {x = _G_INIT_DATA_.LEFT_X, y = _SEP_Y_3_}, + p2 = {x = _RIGHT_X_, y = _SEP_Y_3_} +} + +local _GPU_UTIL_Y_ = _SEP_Y_3_ + _SEPARATOR_SPACING_ + +local gpu_util = { + label = Widget.Text{ + x = _G_INIT_DATA_.LEFT_X, + y = _GPU_UTIL_Y_, + text = 'GPU Utilization' + }, + value = Widget.Text{ + x = _RIGHT_X_, + y = _GPU_UTIL_Y_, + x_align = 'right', + text_color = Patterns.BLUE, + text = '' + }, + plot = Widget.LabelPlot{ + x = _G_INIT_DATA_.LEFT_X, + y = _GPU_UTIL_Y_ + _PLOT_SEC_BREAK_, + width = _G_INIT_DATA_.SECTION_WIDTH, + height = _PLOT_HEIGHT_ + } +} + +local _MEM_UTIL_Y_ = _GPU_UTIL_Y_ + _PLOT_HEIGHT_ + _PLOT_SEC_BREAK_ * 2 + +local mem_util = { + label = Widget.Text{ + x = _G_INIT_DATA_.LEFT_X, + y = _MEM_UTIL_Y_, + text = 'Memory Utilization' + }, + value = Widget.Text{ + x = _RIGHT_X_, + y = _MEM_UTIL_Y_, + x_align = 'right', + text_color = Patterns.BLUE, + text = '' + }, + plot = Widget.LabelPlot{ + x = _G_INIT_DATA_.LEFT_X, + y = _MEM_UTIL_Y_ + _PLOT_SEC_BREAK_, + width = _G_INIT_DATA_.SECTION_WIDTH, + height = _PLOT_HEIGHT_ + } +} + +local _VID_UTIL_Y_ = _MEM_UTIL_Y_ + _PLOT_HEIGHT_ + _PLOT_SEC_BREAK_ * 2 + +local vid_util = { + label = Widget.Text{ + x = _G_INIT_DATA_.LEFT_X, + y = _VID_UTIL_Y_, + text = 'Video Utilization' + }, + value = Widget.Text{ + x = _RIGHT_X_, + y = _VID_UTIL_Y_, + x_align = 'right', + text_color = Patterns.BLUE, + text = '' + }, + plot = Widget.LabelPlot{ + x = _G_INIT_DATA_.LEFT_X, + y = _VID_UTIL_Y_ + _PLOT_SEC_BREAK_, + width = _G_INIT_DATA_.SECTION_WIDTH, + height = _PLOT_HEIGHT_ + } +} + +--[[ +vars to process the nv settings glob + +glob will be of the form: + + + + , + graphics=, memory=, video=, PCIe= +--]] +local NV_QUERY = 'optirun nvidia-settings -c :8 -t'.. + ' -q UsedDedicatedGPUMemory'.. + ' -q TotalDedicatedGPUMemory'.. + ' -q ThermalSensorReading'.. + ' -q [gpu:0]/GPUCurrentClockFreqs'.. + ' -q [gpu:0]/GPUUtilization' + +local NV_REGEX = '(%d+)\n'.. + '(%d+)\n'.. + '(%d+)\n'.. + '(%d+),(%d+)\n'.. + 'graphics=(%d+), memory=%d+, video=(%d+), PCIe=(%d+)\n' + +local NA = 'N/A' + +local nvidia_off = function(cr) + CriticalText.set(internal_temp.value, cr, NA, 1) + Text.set(pci_util.value, cr, NA) + + TextColumn.set(clock_speed.values, cr, 1, NA) + TextColumn.set(clock_speed.values, cr, 2, NA) + + Text.set(gpu_util.value, cr, NA) + Text.set(mem_util.value, cr, NA) + Text.set(vid_util.value, cr, NA) + + LabelPlot.update(gpu_util.plot, 0) + LabelPlot.update(mem_util.plot, 0) + LabelPlot.update(vid_util.plot, 0) +end + +local update = function(cr) + -- check if bbswitch is on + if util.read_file('/proc/acpi/bbswitch', '.+%s+(%u+)') == 'ON' then + + -- bbswitch might be on, but only because conky is constantly querying + -- it and there appears to be some lag between closing all optirun + -- processes and flipping bbswitch off. If bbswitch is on and there are + -- no optirun processes, we call this "Mixed." In this case we don't + -- check anything (to allow bbswitch to actually switch off) and set all + -- values to N/A and 0. + if __string_find(util.execute_cmd('ps -A -o comm'), 'optirun') == nil then + Text.set(status.value, cr, 'Mixed') + nvidia_off(cr) + else + Text.set(status.value, cr, 'On') + local nvidia_settings_glob = util.execute_cmd(NV_QUERY) + + local used_memory, total_memory, temp_reading, gpu_frequency, + memory_frequency, gpu_utilization, vid_utilization, + pci_utilization = __string_match(nvidia_settings_glob, NV_REGEX) + + local is_critical = 1 + if __tonumber(temp_reading) > 80 then is_critical = 0 end + + CriticalText.set(internal_temp.value, cr, temp_reading..'°C', is_critical) + Text.set(pci_util.value, cr, pci_utilization..'%') + + TextColumn.set(clock_speed.values, cr, 1, gpu_frequency..' Mhz') + TextColumn.set(clock_speed.values, cr, 2, memory_frequency..' Mhz') + + local percent_used_memory = used_memory / total_memory + + Text.set(gpu_util.value, cr, gpu_utilization..'%') + Text.set(mem_util.value, cr, util.round(percent_used_memory * 100)..'%') + Text.set(vid_util.value, cr, vid_utilization..'%') + + LabelPlot.update(gpu_util.plot, gpu_utilization * 0.01) + LabelPlot.update(mem_util.plot, percent_used_memory) + LabelPlot.update(vid_util.plot, vid_utilization * 0.01) + end + else + Text.set(status.value, cr, 'Off') + nvidia_off(cr) + end +end + +Widget = nil +Patterns = nil +_MODULE_Y_ = nil +_SEPARATOR_SPACING_ = nil +_TEXT_SPACING_ = nil +_PLOT_SEC_BREAK_ = nil +_PLOT_HEIGHT_ = nil +_RIGHT_X_ = nil +_SEP_Y_1_ = nil +_SEP_Y_2_ = nil +_SEP_Y_3_ = nil +_INTERNAL_TEMP_Y_ = nil +_PCI_UTIL_Y_ = nil +_CLOCK_SPEED_Y_ = nil +_GPU_UTIL_Y_ = nil +_MEM_UTIL_Y_ = nil +_VID_UTIL_Y_ = nil + +local draw = function(cr, current_interface) + update(cr) + + if current_interface == 0 then + Text.draw(header.text, cr) + Line.draw(header.underline, cr) + + Text.draw(status.label, cr) + Text.draw(status.value, cr) + + Line.draw(separator1, cr) + + Text.draw(internal_temp.label, cr) + Text.draw(internal_temp.value, cr) + + Text.draw(pci_util.label, cr) + Text.draw(pci_util.value, cr) + + Line.draw(separator2, cr) + + TextColumn.draw(clock_speed.labels, cr) + TextColumn.draw(clock_speed.values, cr) + + Line.draw(separator3, cr) + + Text.draw(gpu_util.label, cr) + Text.draw(gpu_util.value, cr) + LabelPlot.draw(gpu_util.plot, cr) + + Text.draw(mem_util.label, cr) + Text.draw(mem_util.value, cr) + LabelPlot.draw(mem_util.plot, cr) + + Text.draw(vid_util.label, cr) + Text.draw(vid_util.value, cr) + LabelPlot.draw(vid_util.plot, cr) + end +end + +return draw + diff --git a/drawing/Memory.lua b/drawing/Memory.lua new file mode 100644 index 0000000..84af5b2 --- /dev/null +++ b/drawing/Memory.lua @@ -0,0 +1,226 @@ +local Widget = require 'Widget' +local Arc = require 'Arc' +local Dial = require 'Dial' +local CriticalText = require 'CriticalText' +local Text = require 'Text' +local TextColumn = require 'TextColumn' +local Line = require 'Line' +local LabelPlot = require 'LabelPlot' +local Table = require 'Table' +local util = require 'util' +local Patterns = require 'Patterns' + +local __string_match = string.match +local __cairo_path_destroy = cairo_path_destroy + +local _MODULE_Y_ = 712 +local _DIAL_THICKNESS_ = 8 +local _DIAL_SPACING_ = 1 +local _TEXT_Y_OFFSET_ = 7 +local _TEXT_LEFT_X_OFFSET_ = 30 +local _TEXT_SPACING_ = 20 +local _PLOT_SECTION_BREAK_ = 30 +local _PLOT_HEIGHT_ = 56 +local _TABLE_SECTION_BREAK_ = 20 +local _TABLE_HEIGHT_ = 114 + +local MEM_TOTAL_KB = tonumber(util.read_file('/proc/meminfo', '^MemTotal:%s+(%d+)')) + +local MEMINFO_REGEX = '\nMemFree:%s+(%d+).+'.. + '\nBuffers:%s+(%d+).+'.. + '\nCached:%s+(%d+).+'.. + '\nSwapTotal:%s+(%d+).+'.. + '\nSwapFree:%s+(%d+).+'.. + '\nSReclaimable:%s+(%d+)' + +local NUM_ROWS = 5 +local TABLE_CONKY = {{}, {}, {}} + +for r = 1, NUM_ROWS do + TABLE_CONKY[1][r] = '${top_mem name '..r..'}' + TABLE_CONKY[2][r] = '${top_mem pid '..r..'}' + TABLE_CONKY[3][r] = '${top_mem mem '..r..'}' +end + +local header = Widget.Header{ + x = _G_INIT_DATA_.RIGHT_X, + y = _MODULE_Y_, + width = _G_INIT_DATA_.SECTION_WIDTH, + header = 'MEMORY' +} + +local DIAL_RADIUS = 32 +local DIAL_THETA_0 = math.rad(90) +local DIAL_THETA_1 = math.rad(360) +local DIAL_X = _G_INIT_DATA_.RIGHT_X + DIAL_RADIUS + _DIAL_THICKNESS_ / 2 +local DIAL_Y = header.bottom_y + DIAL_RADIUS + _DIAL_THICKNESS_ / 2 + +local dial = Widget.Dial{ + x = DIAL_X, + y = DIAL_Y, + radius = DIAL_RADIUS, + thickness = _DIAL_THICKNESS_, + critical_limit = '>0.8' +} +local cache_arc = Widget.Arc{ + x = DIAL_X, + y = DIAL_Y, + radius = DIAL_RADIUS, + thickness = _DIAL_THICKNESS_, + arc_pattern = Patterns.PURPLE_ROUNDED +} + +local total_used = Widget.CriticalText{ + x = DIAL_X, + y = DIAL_Y, + x_align = 'center', + y_align = 'center', + append_end = '%' +} + +local inner_ring = Widget.Arc{ + x = DIAL_X, + y = DIAL_Y, + radius = DIAL_RADIUS - _DIAL_THICKNESS_ / 2 - 2, + theta0 = 0, + theta1 = 360 +} + +local _LINE_1_Y_ = header.bottom_y + _TEXT_Y_OFFSET_ +local _TEXT_LEFT_X_ = _G_INIT_DATA_.RIGHT_X + DIAL_RADIUS * 2 + _TEXT_LEFT_X_OFFSET_ +local _RIGHT_X_ = _G_INIT_DATA_.RIGHT_X + _G_INIT_DATA_.SECTION_WIDTH + +local swap= { + label = Widget.Text{ + x = _TEXT_LEFT_X_, + y = _LINE_1_Y_, + spacing = _TEXT_SPACING_, + text = 'Swap Usage' + }, + percent = Widget.CriticalText{ + x = _RIGHT_X_, + y = _LINE_1_Y_, + x_align = 'right', + append_end = ' %', + }, +} + +local cache = { + labels = Widget.TextColumn{ + x = _TEXT_LEFT_X_, + y = _LINE_1_Y_ + _TEXT_SPACING_, + spacing = _TEXT_SPACING_, + 'Page Cache', + 'Buffers', + 'Kernel Slab' + }, + percents = Widget.TextColumn{ + x = _RIGHT_X_, + y = _LINE_1_Y_ + _TEXT_SPACING_, + x_align = 'right', + append_end = ' %', + text_color = Patterns.PURPLE, + '', + '', + '' + }, +} + +local _PLOT_Y_ = _PLOT_SECTION_BREAK_ + header.bottom_y + DIAL_RADIUS * 2 + +local plot = Widget.LabelPlot{ + x = _G_INIT_DATA_.RIGHT_X, + y = _PLOT_Y_, + width = _G_INIT_DATA_.SECTION_WIDTH, + height = _PLOT_HEIGHT_ +} + +local tbl = Widget.Table{ + x = _G_INIT_DATA_.RIGHT_X, + y = _PLOT_Y_ + _PLOT_HEIGHT_ + _TABLE_SECTION_BREAK_, + width = _G_INIT_DATA_.SECTION_WIDTH, + height = _TABLE_HEIGHT_, + 'Name', + 'PID', + 'Mem (%)' +} + +local update = function(cr) + -- see source for the 'free' command (sysinfo.c) for formulas + + local memfree_kb, buffers_kb, cached_kb, swap_total_kb, swap_free_kb, + slab_reclaimable_kb = __string_match(util.read_file('/proc/meminfo'), MEMINFO_REGEX) + + local used_percent = (MEM_TOTAL_KB - memfree_kb - cached_kb - buffers_kb - slab_reclaimable_kb) / MEM_TOTAL_KB + + Dial.set(dial, used_percent) + CriticalText.set(total_used, cr, util.round(used_percent * 100)) + + local cache_theta = (DIAL_THETA_0 - DIAL_THETA_1) / MEM_TOTAL_KB * memfree_kb + DIAL_THETA_1 + __cairo_path_destroy(cache_arc.path) + cache_arc.path = Arc.create_path(cr, DIAL_X, DIAL_Y, DIAL_RADIUS, dial.dial_angle, cache_theta) + + CriticalText.set(swap.percent, cr, util.precision_round_to_string( + (swap_total_kb - swap_free_kb) / swap_total_kb * 100)) + + local _percents = cache.percents + + TextColumn.set(_percents, cr, 1, util.precision_round_to_string( + cached_kb / MEM_TOTAL_KB * 100)) + + TextColumn.set(_percents, cr, 2, util.precision_round_to_string( + buffers_kb / MEM_TOTAL_KB * 100)) + + TextColumn.set(_percents, cr, 3, util.precision_round_to_string( + slab_reclaimable_kb / MEM_TOTAL_KB * 100)) + + LabelPlot.update(plot, used_percent) + + for c = 1, 3 do + local column = TABLE_CONKY[c] + for r = 1, NUM_ROWS do + Table.set(tbl, cr, c, r, util.conky(column[r], '(%S+)')) + end + end +end + +Widget = nil +Patterns = nil +_MODULE_Y_ = nil +_DIAL_THICKNESS_ = nil +_DIAL_SPACING_ = nil +_TEXT_Y_OFFSET_ = nil +_TEXT_LEFT_X_OFFSET_ = nil +_TEXT_SPACING_ = nil +_PLOT_SECTION_BREAK_ = nil +_PLOT_HEIGHT_ = nil +_TABLE_SECTION_BREAK_ = nil +_TABLE_HEIGHT_ = nil +_LINE_1_Y_ = nil +_TEXT_LEFT_X_ = nil +_RIGHT_X_ = nil +_PLOT_Y_ = nil + +local draw = function(cr, current_interface) + update(cr) + + if current_interface == 0 then + Text.draw(header.text, cr) + Line.draw(header.underline, cr) + Dial.draw(dial, cr) + Arc.draw(cache_arc, cr) + Arc.draw(inner_ring, cr) + CriticalText.draw(total_used, cr) + + Text.draw(swap.label, cr) + CriticalText.draw(swap.percent, cr) + TextColumn.draw(cache.labels, cr) + TextColumn.draw(cache.percents, cr) + + LabelPlot.draw(plot, cr) + + Table.draw(tbl, cr) + end +end + +return draw diff --git a/drawing/Network.lua b/drawing/Network.lua new file mode 100644 index 0000000..5e802e0 --- /dev/null +++ b/drawing/Network.lua @@ -0,0 +1,154 @@ +local Widget = require 'Widget' +local Text = require 'Text' +local Line = require 'Line' +local ScalePlot = require 'ScalePlot' +local util = require 'util' +local Patterns = require 'Patterns' + +local __string_gmatch = string.gmatch + +local _PLOT_SEC_BREAK_ = 20 +local _PLOT_HEIGHT_ = 56 + +local network_label_function = function(bytes) + local new_unit = util.get_unit(bytes) + + local converted = util.convert_bytes(bytes, 'B', new_unit) + local precision = 0 + if converted < 10 then precision = 1 end + + return util.round_to_string(converted, precision)..' '..new_unit..'/s' +end + +local header = Widget.Header{ + x = _G_INIT_DATA_.CENTER_RIGHT_X, + y = _G_INIT_DATA_.TOP_Y, + width = _G_INIT_DATA_.SECTION_WIDTH, + header = 'NETWORK' +} + +local _RIGHT_X_ = _G_INIT_DATA_.CENTER_RIGHT_X + _G_INIT_DATA_.SECTION_WIDTH + +local dnload = { + label = Widget.Text{ + x = _G_INIT_DATA_.CENTER_RIGHT_X, + y = header.bottom_y, + text = 'Download', + }, + speed = Widget.Text{ + x = _RIGHT_X_, + y = header.bottom_y, + x_align = 'right', + text_color = Patterns.BLUE + }, + plot = Widget.ScalePlot{ + x = _G_INIT_DATA_.CENTER_RIGHT_X, + y = header.bottom_y + _PLOT_SEC_BREAK_, + width = _G_INIT_DATA_.SECTION_WIDTH, + height = _PLOT_HEIGHT_, + y_label_func = network_label_function + } +} + +local _UPLOAD_Y_ = header.bottom_y + _PLOT_HEIGHT_ + _PLOT_SEC_BREAK_ * 2 + +local upload = { + label = Widget.Text{ + x = _G_INIT_DATA_.CENTER_RIGHT_X, + y = _UPLOAD_Y_, + text = 'Upload', + }, + speed = Widget.Text{ + x = _RIGHT_X_, + y = _UPLOAD_Y_, + x_align = 'right', + text_color = Patterns.BLUE + }, + plot = Widget.ScalePlot{ + x = _G_INIT_DATA_.CENTER_RIGHT_X, + y = _UPLOAD_Y_ + _PLOT_SEC_BREAK_, + width = _G_INIT_DATA_.SECTION_WIDTH, + height = _PLOT_HEIGHT_, + y_label_func = network_label_function + } +} + +local interface_counters_tbl = {} + +local update = function(cr, update_frequency) + local dspeed, uspeed = 0, 0 + + local rx_delta, tx_delta + + -- iterate through the route file and filter on interfaces that are gateways (flag = 0003) + local iterator = __string_gmatch(util.read_file('/proc/net/route'), + '(%w+)%s+%w+%s+%w+%s+0003%s+') + + for interface in iterator do + local interface_counters = interface_counters_tbl[interface] + + if not interface_counters then + local rx_path = '/sys/class/net/'..interface..'/statistics/rx_bytes' + local tx_path = '/sys/class/net/'..interface..'/statistics/tx_bytes' + + interface_counters = { + rx_path = rx_path, + tx_path = tx_path, + prev_rx_byte_cnt = util.read_file(rx_path, nil, '*n'), + prev_tx_byte_cnt = util.read_file(tx_path, nil, '*n'), + } + interface_counters_tbl[interface] = interface_counters + end + + local rx_byte_cnt = util.read_file(interface_counters.rx_path, nil, '*n') + local tx_byte_cnt = util.read_file(interface_counters.tx_path, nil, '*n') + + rx_delta = rx_byte_cnt - interface_counters.prev_rx_byte_cnt + tx_delta = tx_byte_cnt - interface_counters.prev_tx_byte_cnt + + interface_counters.prev_rx_byte_cnt = rx_byte_cnt + interface_counters.prev_tx_byte_cnt = tx_byte_cnt + + -- mask overflow + if rx_delta > 0 then dspeed = dspeed + rx_delta * update_frequency end + if tx_delta > 0 then uspeed = uspeed + tx_delta * update_frequency end + end + + local dspeed_unit = util.get_unit(dspeed) + local uspeed_unit = util.get_unit(uspeed) + + dnload.speed.append_end = ' '..dspeed_unit..'/s' + upload.speed.append_end = ' '..uspeed_unit..'/s' + + Text.set(dnload.speed, cr, util.precision_convert_bytes(dspeed, 'B', dspeed_unit, 3)) + Text.set(upload.speed, cr, util.precision_convert_bytes(uspeed, 'B', uspeed_unit, 3)) + + ScalePlot.update(dnload.plot, cr, dspeed) + ScalePlot.update(upload.plot, cr, uspeed) +end + +Widget = nil +Patterns = nil +_PLOT_SEC_BREAK_ = nil +_PLOT_HEIGHT_ = nil +_RIGHT_X_ = nil +_UPLOAD_Y_ = nil + +local draw = function(cr, current_interface, update_frequency) + update(cr, update_frequency) + + if current_interface == 0 then + Text.draw(header.text, cr) + Line.draw(header.underline, cr) + + Text.draw(dnload.label, cr) + Text.draw(dnload.speed, cr) + ScalePlot.draw(dnload.plot, cr) + + Text.draw(upload.label, cr) + Text.draw(upload.speed, cr) + ScalePlot.draw(upload.plot, cr) + end +end + +return draw diff --git a/drawing/Pacman.lua b/drawing/Pacman.lua new file mode 100644 index 0000000..326909b --- /dev/null +++ b/drawing/Pacman.lua @@ -0,0 +1,65 @@ +local Widget = require 'Widget' +local Text = require 'Text' +local Line = require 'Line' +local TextColumn = require 'TextColumn' +local util = require 'util' +local Patterns = require 'Patterns' + +local PACMAN_TABLE = { + 'pacman -Qq', + 'pacman -Qeq', + 'pacman -Quq', + 'pacman -Qdtq', + 'pacman -Qmq' +} + +local _TEXT_SPACING_ = 20 + +local header = Widget.Header{ + x = _G_INIT_DATA_.RIGHT_X, + y = _G_INIT_DATA_.TOP_Y, + width = _G_INIT_DATA_.SECTION_WIDTH, + header = 'PACMAN' +} + +local labels = Widget.TextColumn{ + x = _G_INIT_DATA_.RIGHT_X, + y = header.bottom_y, + spacing = _TEXT_SPACING_, + 'Total', + 'Explicit', + 'Outdated', + 'Orphaned', + 'Local' +} +local info = Widget.TextColumn{ + x = _G_INIT_DATA_.RIGHT_X + _G_INIT_DATA_.SECTION_WIDTH, + y = header.bottom_y, + spacing = _TEXT_SPACING_, + x_align = 'right', + text_color = Patterns.BLUE, + num_rows = 5 +} + +Widget = nil +Patterns = nil +_TEXT_SPACING_ = nil + +local update = function(cr) + for i, cmd in pairs(PACMAN_TABLE) do + TextColumn.set(info, cr, i, util.line_count(util.execute_cmd(cmd))) + end +end + +local draw = function(cr, current_interface, log_is_changed) + if log_is_changed then update(cr) end + + if current_interface == 0 then + Text.draw(header.text, cr) + Line.draw(header.underline, cr) + TextColumn.draw(labels, cr) + TextColumn.draw(info, cr) + end +end + +return draw diff --git a/drawing/Panel.lua b/drawing/Panel.lua new file mode 100644 index 0000000..3f9d8a9 --- /dev/null +++ b/drawing/Panel.lua @@ -0,0 +1,31 @@ +local Widget = require 'Widget' +local FillRect = require 'FillRect' + +local left = Widget.Panel{ + x = _G_INIT_DATA_.LEFT_X - _G_INIT_DATA_.PANEL_MARGIN_X, + y = _G_INIT_DATA_.TOP_Y - _G_INIT_DATA_.PANEL_MARGIN_Y, + width = _G_INIT_DATA_.SECTION_WIDTH + _G_INIT_DATA_.PANEL_MARGIN_X * 2, + height = _G_INIT_DATA_.SIDE_HEIGHT + _G_INIT_DATA_.PANEL_MARGIN_Y * 2, +} +local center = Widget.Panel{ + x = _G_INIT_DATA_.CENTER_LEFT_X - _G_INIT_DATA_.PANEL_MARGIN_X, + y = _G_INIT_DATA_.TOP_Y - _G_INIT_DATA_.PANEL_MARGIN_Y, + width = _G_INIT_DATA_.CENTER_WIDTH + _G_INIT_DATA_.PANEL_MARGIN_Y * 2 + _G_INIT_DATA_.CENTER_PAD, + height = _G_INIT_DATA_.CENTER_HEIGHT + _G_INIT_DATA_.PANEL_MARGIN_Y * 2, +} +local right = Widget.Panel{ + x = _G_INIT_DATA_.RIGHT_X - _G_INIT_DATA_.PANEL_MARGIN_X, + y = _G_INIT_DATA_.TOP_Y - _G_INIT_DATA_.PANEL_MARGIN_Y, + width = _G_INIT_DATA_.SECTION_WIDTH + _G_INIT_DATA_.PANEL_MARGIN_X * 2, + height = _G_INIT_DATA_.SIDE_HEIGHT + _G_INIT_DATA_.PANEL_MARGIN_Y * 2, +} + +Widget = nil + +local draw = function(cr) + FillRect.draw(left, cr) + FillRect.draw(center, cr) + FillRect.draw(right, cr) +end + +return draw diff --git a/drawing/Power.lua b/drawing/Power.lua new file mode 100644 index 0000000..8e03f53 --- /dev/null +++ b/drawing/Power.lua @@ -0,0 +1,221 @@ +local Widget = require 'Widget' +local Text = require 'Text' +local TextColumn = require 'TextColumn' +local Line = require 'Line' +local ScalePlot = require 'ScalePlot' +local util = require 'util' +local Patterns = require 'Patterns' + +local _MODULE_Y_ = 328 +local _SEPARATOR_SPACING_ = 20 +local _TEXT_SPACING_ = 20 +local _PLOT_SEC_BREAK_ = 20 +local _PLOT_HEIGHT_ = 56 + +local power_label_function = function(watts) return watts..' W' end + +local calculate_power = function(cr, prev_cnt, cnt, update_frequency) + if cnt > prev_cnt then + return (cnt - prev_cnt) * update_frequency * 0.000001 + else + return 0 + end +end + +local header = Widget.Header{ + x = _G_INIT_DATA_.RIGHT_X, + y = _MODULE_Y_, + width = _G_INIT_DATA_.SECTION_WIDTH, + header = 'POWER' +} + +local _RIGHT_X_ = _G_INIT_DATA_.RIGHT_X + _G_INIT_DATA_.SECTION_WIDTH + +local pp01 = { + labels = Widget.TextColumn{ + x = _G_INIT_DATA_.RIGHT_X, + y = header.bottom_y, + spacing = _TEXT_SPACING_, + 'Core', + 'iGPU' + }, + values = Widget.TextColumn{ + x = _RIGHT_X_, + y = header.bottom_y, + spacing = _TEXT_SPACING_, + x_align = 'right', + text_color = Patterns.BLUE, + append_end = ' W', + num_rows = 2 + } +} + +local _SEP_Y_ = header.bottom_y + _TEXT_SPACING_ + _SEPARATOR_SPACING_ + +local separator = Widget.Line{ + p1 = {x = _G_INIT_DATA_.RIGHT_X, y = _SEP_Y_}, + p2 = {x = _RIGHT_X_, y = _SEP_Y_} +} + +local _PKG0_Y_ = _SEP_Y_ + _SEPARATOR_SPACING_ + +local pkg0 = { + label = Widget.Text{ + x = _G_INIT_DATA_.RIGHT_X, + y = _PKG0_Y_, + text = 'PKG 0' + }, + value = Widget.Text{ + x = _RIGHT_X_, + y = _PKG0_Y_, + x_align = 'right', + text_color = Patterns.BLUE, + text = '', + append_end = ' W' + }, + plot = Widget.ScalePlot{ + x = _G_INIT_DATA_.RIGHT_X, + y = _PKG0_Y_ + _PLOT_SEC_BREAK_, + width = _G_INIT_DATA_.SECTION_WIDTH, + height = _PLOT_HEIGHT_, + y_label_func = power_label_function, + } +} + +local _DRAM_Y_ = _PKG0_Y_ + _PLOT_SEC_BREAK_ * 2 + _PLOT_HEIGHT_ + +local dram = { + label = Widget.Text{ + x = _G_INIT_DATA_.RIGHT_X, + y = _DRAM_Y_, + text = 'DRAM' + }, + value = Widget.Text{ + x = _RIGHT_X_, + y = _DRAM_Y_, + x_align = 'right', + text_color = Patterns.BLUE, + text = '', + append_end = ' W' + }, + plot = Widget.ScalePlot{ + x = _G_INIT_DATA_.RIGHT_X, + y = _DRAM_Y_ + _PLOT_SEC_BREAK_, + width = _G_INIT_DATA_.SECTION_WIDTH, + height = _PLOT_HEIGHT_, + y_label_func = power_label_function, + } +} + +local _BATTERY_DRAW_Y_ = _DRAM_Y_ + _PLOT_SEC_BREAK_ * 2 + _PLOT_HEIGHT_ + +local battery_draw = { + label = Widget.Text{ + x = _G_INIT_DATA_.RIGHT_X, + y = _BATTERY_DRAW_Y_, + spacing = _TEXT_SPACING_, + text = 'Battery Draw' + }, + value = Widget.CriticalText{ + x = _RIGHT_X_, + y = _BATTERY_DRAW_Y_, + x_align = 'right', + }, + plot = Widget.ScalePlot{ + x = _G_INIT_DATA_.RIGHT_X, + y = _BATTERY_DRAW_Y_ + _PLOT_SEC_BREAK_, + width = _G_INIT_DATA_.SECTION_WIDTH, + height = _PLOT_HEIGHT_, + y_label_func = power_label_function, + } +} + +local PKG0_PATH = '/sys/class/powercap/intel-rapl:0/energy_uj' +local CORE_PATH = '/sys/class/powercap/intel-rapl:0:0/energy_uj' +local IGPU_PATH = '/sys/class/powercap/intel-rapl:0:1/energy_uj' +local DRAM_PATH = '/sys/class/powercap/intel-rapl:0:2/energy_uj' + +local prev_pkg0_uj_cnt = util.read_file(PKG0_PATH, nil, '*n') +local prev_core_uj_cnt = util.read_file(CORE_PATH, nil, '*n') +local prev_igpu_uj_cnt = util.read_file(IGPU_PATH, nil, '*n') +local prev_dram_uj_cnt = util.read_file(DRAM_PATH, nil, '*n') + +local update = function(cr, update_frequency, is_using_ac) + local pkg0_uj_cnt = util.read_file(PKG0_PATH, nil, '*n') + local core_uj_cnt = util.read_file(CORE_PATH, nil, '*n') + local igpu_uj_cnt = util.read_file(IGPU_PATH, nil, '*n') + local dram_uj_cnt = util.read_file(DRAM_PATH, nil, '*n') + + TextColumn.set(pp01.values, cr, 1, util.precision_round_to_string( + calculate_power(cr, prev_core_uj_cnt, core_uj_cnt, update_frequency), 3)) + + TextColumn.set(pp01.values, cr, 2, util.precision_round_to_string( + calculate_power(cr, prev_igpu_uj_cnt, igpu_uj_cnt, update_frequency), 3)) + + local pkg0_power = calculate_power(cr, prev_pkg0_uj_cnt, pkg0_uj_cnt, update_frequency) + local dram_power = calculate_power(cr, prev_dram_uj_cnt, dram_uj_cnt, update_frequency) + + Text.set(pkg0.value, cr, util.precision_round_to_string(pkg0_power, 3)) + ScalePlot.update(pkg0.plot, cr, pkg0_power) + + Text.set(dram.value, cr, util.precision_round_to_string(dram_power, 3)) + ScalePlot.update(dram.plot, cr, dram_power) + + prev_pkg0_uj_cnt = pkg0_uj_cnt + prev_core_uj_cnt = core_uj_cnt + prev_igpu_uj_cnt = igpu_uj_cnt + prev_dram_uj_cnt = dram_uj_cnt + + if is_using_ac then + Text.set(battery_draw.value, cr, 'A/C') + ScalePlot.update(battery_draw.plot, cr, 0) + else + local current = util.read_file('/sys/class/power_supply/BAT0/current_now', nil, '*n') + local voltage = util.read_file('/sys/class/power_supply/BAT0/voltage_now', nil, '*n') + local power = current * voltage * 0.000000000001 + + Text.set(battery_draw.value, cr, util.precision_round_to_string(power, 3)..' W') + ScalePlot.update(battery_draw.plot, cr, power) + end +end + +Widget = nil +Patterns = nil +_MODULE_Y_ = nil +_SEPARATOR_SPACING_ = nil +_TEXT_SPACING_ = nil +_PLOT_SEC_BREAK_ = nil +_PLOT_HEIGHT_ = nil +_RIGHT_X_ = nil +_SEP_Y_ = nil +_PKG0_Y_ = nil +_DRAM_Y_ = nil +_BATTERY_DRAW_Y_ = nil + +local draw = function(cr, current_interface, update_frequency, is_using_ac) + update(cr, update_frequency, is_using_ac) + + if current_interface == 0 then + Text.draw(header.text, cr) + Line.draw(header.underline, cr) + + TextColumn.draw(pp01.labels, cr) + TextColumn.draw(pp01.values, cr) + + Line.draw(separator, cr) + + Text.draw(pkg0.label, cr) + Text.draw(pkg0.value, cr) + ScalePlot.draw(pkg0.plot, cr) + + Text.draw(dram.label, cr) + Text.draw(dram.value, cr) + ScalePlot.draw(dram.plot, cr) + + Text.draw(battery_draw.label, cr) + Text.draw(battery_draw.value, cr) + ScalePlot.draw(battery_draw.plot, cr) + end +end + +return draw diff --git a/drawing/Processor.lua b/drawing/Processor.lua new file mode 100644 index 0000000..eb07764 --- /dev/null +++ b/drawing/Processor.lua @@ -0,0 +1,248 @@ +local Widget = require 'Widget' +local Arc = require 'Arc' +local CompoundDial = require 'CompoundDial' +local CriticalText = require 'CriticalText' +local Text = require 'Text' +local Line = require 'Line' +local LabelPlot = require 'LabelPlot' +local Table = require 'Table' +local util = require 'util' +local Patterns = require 'Patterns' + +local CORETEMP_PATH = '/sys/devices/platform/coretemp.0/hwmon/hwmon%i/%s' + +local NUM_PHYSICAL_CORES = 4 +local NUM_THREADS_PER_CORE = 2 + +local NUM_ROWS = 5 +local TABLE_CONKY = {{}, {}, {}} + +for r = 1, NUM_ROWS do + TABLE_CONKY[1][r] = '${top name '..r..'}' + TABLE_CONKY[2][r] = '${top pid '..r..'}' + TABLE_CONKY[3][r] = '${top cpu '..r..'}' +end + +local _MODULE_Y_ = 636 +local _DIAL_INNER_RADIUS_ = 30 +local _DIAL_OUTER_RADIUS_ = 42 +local _DIAL_SPACING_ = 1 +local _TEXT_Y_OFFSET_ = 15 +local _SEPARATOR_SPACING_ = 20 +local _PLOT_SECTION_BREAK_ = 23 +local _PLOT_HEIGHT_ = 56 +local _TABLE_SECTION_BREAK_ = 20 +local _TABLE_HEIGHT_ = 114 + +local _create_core_ = function(cores, id, x, y) + local conky_threads = {} + + for c = 0, NUM_PHYSICAL_CORES * NUM_THREADS_PER_CORE - 1 do + if util.read_file('/sys/devices/system/cpu/cpu'..c..'/topology/core_id', nil, '*n') == id then + table.insert(conky_threads, '${cpu cpu'..c..'}') + end + end + + local hwmon_index = -1 + while util.read_file(string.format(CORETEMP_PATH, hwmon_index, 'name'), nil, '*l') ~= 'coretemp' do + hwmon_index = hwmon_index + 1 + end + + cores[id +1] = { + dials = Widget.CompoundDial{ + x = x, + y = y, + inner_radius = _DIAL_INNER_RADIUS_, + outer_radius = _DIAL_OUTER_RADIUS_, + spacing = _DIAL_SPACING_, + num_dials = NUM_THREADS_PER_CORE, + critical_limit = '>0.8' + }, + inner_ring = Widget.Arc{ + x = x, + y = y, + radius = _DIAL_INNER_RADIUS_ - 2, + theta0 = 0, + theta1 = 360 + }, + coretemp_text = Widget.CriticalText{ + x = x, + y = y, + x_align = 'center', + y_align = 'center', + append_end = '°C', + critical_limit = '>90' + }, + coretemp_path = string.format(CORETEMP_PATH, hwmon_index, 'temp'..(id + 2)..'_input'), + conky_threads = conky_threads + } +end + +local header = Widget.Header{ + x = _G_INIT_DATA_.LEFT_X, + y = _MODULE_Y_, + width = _G_INIT_DATA_.SECTION_WIDTH, + header = "PROCESSOR" +} + +--we assume that this cpu has 4 physical cores with 2 logical each +local cores = {} + +for c = 0, NUM_PHYSICAL_CORES - 1 do + local dial_x = _G_INIT_DATA_.LEFT_X + _DIAL_OUTER_RADIUS_ + + (_G_INIT_DATA_.SECTION_WIDTH - 2 * _DIAL_OUTER_RADIUS_) * c / 3 + local dial_y = header.bottom_y + _DIAL_OUTER_RADIUS_ + _create_core_(cores, c, dial_x, dial_y) +end + +local _RIGHT_X_ = _G_INIT_DATA_.LEFT_X + _G_INIT_DATA_.SECTION_WIDTH + +local _PROCESS_Y_ = header.bottom_y + _DIAL_OUTER_RADIUS_ * 2 + _PLOT_SECTION_BREAK_ + +local process = { + labels = Widget.Text{ + x = _G_INIT_DATA_.LEFT_X, + y = _PROCESS_Y_, + text = 'R | S | D | T | Z' + }, + values = Widget.Text{ + x = _RIGHT_X_, + y = _PROCESS_Y_, + x_align = 'right', + text_color = Patterns.BLUE, + text = '' + } +} + +local _SEP_Y_ = _PROCESS_Y_ + _SEPARATOR_SPACING_ + +local separator = Widget.Line{ + p1 = {x = _G_INIT_DATA_.LEFT_X, y = _SEP_Y_}, + p2 = {x = _RIGHT_X_, y = _SEP_Y_} +} + +local _LOAD_Y_ = _SEP_Y_ + _SEPARATOR_SPACING_ + +local total_load = { + label = Widget.Text{ + x = _G_INIT_DATA_.LEFT_X, + y = _LOAD_Y_, + text = 'Total Load' + }, + value = Widget.CriticalText{ + x = _RIGHT_X_, + y = _LOAD_Y_, + x_align = 'right', + append_end = '%', + critical_limit = '>80' + } +} + +local _PLOT_Y_ = _LOAD_Y_ + _PLOT_SECTION_BREAK_ + +local plot = Widget.LabelPlot{ + x = _G_INIT_DATA_.LEFT_X, + y = _PLOT_Y_, + width = _G_INIT_DATA_.SECTION_WIDTH, + height = _PLOT_HEIGHT_ +} + +local tbl = Widget.Table{ + x = _G_INIT_DATA_.LEFT_X, + y = _PLOT_Y_ + _PLOT_HEIGHT_ + _TABLE_SECTION_BREAK_, + width = _G_INIT_DATA_.SECTION_WIDTH, + height = _TABLE_HEIGHT_, + num_rows = NUM_ROWS, + 'Name', + 'PID', + 'CPU (%)' +} + +local update = function(cr) + local conky = util.conky + local char_count = util.char_count + + local sum = 0 + for c = 1, NUM_PHYSICAL_CORES do + local core = cores[c] + + local conky_threads = core.conky_threads + for t = 1, NUM_THREADS_PER_CORE do + local percent = util.conky_numeric(conky_threads[t]) * 0.01 + CompoundDial.set(core.dials, t, percent) + sum = sum + percent + end + + CriticalText.set(core.coretemp_text, cr, util.round(0.001 * util.read_file(core.coretemp_path, nil, '*n'))) + end + + local process_glob = util.execute_cmd('ps -A -o s') + + --subtract one from running b/c ps will always be "running" + Text.set(process.values, cr, (char_count(process_glob, 'R') - 1)..' | '.. + char_count(process_glob, 'S')..' | '.. + char_count(process_glob, 'D')..' | '.. + char_count(process_glob, 'T')..' | '.. + char_count(process_glob, 'Z')) + + local load_percent = util.round(sum / NUM_PHYSICAL_CORES / NUM_THREADS_PER_CORE, 2) + CriticalText.set(total_load.value, cr, load_percent * 100) + + LabelPlot.update(plot, load_percent) + + for c = 1, 3 do + local column = TABLE_CONKY[c] + for r = 1, NUM_ROWS do + Table.set(tbl, cr, c, r, conky(column[r], '(%S+)')) + end + end +end + +Widget = nil +Patterns = nil +_MODULE_Y_ = nil +_DIAL_INNER_RADIUS_ = nil +_DIAL_OUTER_RADIUS_ = nil +_DIAL_SPACING_ = nil +_TEXT_Y_OFFSET_ = nil +_SEPARATOR_SPACING_ = nil +_PLOT_SECTION_BREAK_ = nil +_PLOT_HEIGHT_ = nil +_TABLE_SECTION_BREAK_ = nil +_TABLE_HEIGHT_ = nil +_create_core_ = nil +_LOAD_Y_ = nil +_RIGHT_X_ = nil +_SEP_Y_ = nil +_PROCESS_Y_ = nil +_PLOT_Y_ = nil + +local draw = function(cr, current_interface) + update(cr) + + if current_interface == 0 then + Text.draw(header.text, cr) + Line.draw(header.underline, cr) + + for c = 1, NUM_PHYSICAL_CORES do + local core = cores[c] + CompoundDial.draw(core.dials, cr) + Arc.draw(core.inner_ring, cr) + CriticalText.draw(core.coretemp_text, cr) + end + + Text.draw(process.labels, cr) + Text.draw(process.values, cr) + + Line.draw(separator, cr) + + Text.draw(total_load.label, cr) + CriticalText.draw(total_load.value, cr) + + LabelPlot.draw(plot, cr) + + Table.draw(tbl, cr) + end +end + +return draw diff --git a/drawing/ReadWrite.lua b/drawing/ReadWrite.lua new file mode 100644 index 0000000..b2f379f --- /dev/null +++ b/drawing/ReadWrite.lua @@ -0,0 +1,141 @@ +local Widget = require 'Widget' +local Text = require 'Text' +local Line = require 'Line' +local ScalePlot = require 'ScalePlot' +local util = require 'util' +local Patterns = require 'Patterns' + +local __tonumber = tonumber +local __string_match = string.match + +local _PLOT_SEC_BREAK_ = 20 +local _PLOT_HEIGHT_ = 56 + +local BLOCK_SIZE_BYTES = 512 +local STAT_FILE = '/sys/block/sda/stat' + +-- fields 3 and 7 (sectors read and written) +local RW_REGEX = '%s+%d+%s+%d+%s+(%d+)%s+%d+%s+%d+%s+%d+%s+(%d+)' + +local read_stat_file = function() + local bytes_r, bytes_w = __string_match(util.read_file(STAT_FILE), RW_REGEX) + return __tonumber(bytes_r) * BLOCK_SIZE_BYTES, __tonumber(bytes_w) * BLOCK_SIZE_BYTES +end + +local update_stat = function(cr, stat, byte_cnt, update_frequency) + local delta_bytes = byte_cnt - stat.prev_byte_cnt + stat.prev_byte_cnt = byte_cnt + + if delta_bytes > 0 then + local bps = delta_bytes * update_frequency + local unit = util.get_unit(bps) + stat.rate.append_end = ' '..unit..'/s' + Text.set(stat.rate, cr, util.precision_convert_bytes(bps, 'B', unit, 3)) + ScalePlot.update(stat.plot, cr, bps) + else + stat.rate.append_end = ' B/s' + Text.set(stat.rate, cr, '0.00') + ScalePlot.update(stat.plot, cr, 0) + end +end + +local io_label_function = function(bytes) + local new_unit = util.get_unit(bytes) + + local converted = util.convert_bytes(bytes, 'B', new_unit) + local precision = 0 + if converted < 10 then precision = 1 end + + return util.round_to_string(converted, precision)..' '..new_unit..'/s' +end + +local header = Widget.Header{ + x = _G_INIT_DATA_.CENTER_LEFT_X, + y = _G_INIT_DATA_.TOP_Y, + width = _G_INIT_DATA_.SECTION_WIDTH, + header = 'INPUT / OUTPUT' +} + +local _RIGHT_X_ = _G_INIT_DATA_.CENTER_LEFT_X + _G_INIT_DATA_.SECTION_WIDTH + +local reads = { + label = Widget.Text{ + x = _G_INIT_DATA_.CENTER_LEFT_X, + y = header.bottom_y, + text = 'Reads', + }, + rate = Widget.Text{ + x = _RIGHT_X_, + y = header.bottom_y, + x_align = 'right', + append_end=' B/s', + text_color = Patterns.BLUE + }, + plot = Widget.ScalePlot{ + x = _G_INIT_DATA_.CENTER_LEFT_X, + y = header.bottom_y + _PLOT_SEC_BREAK_, + width = _G_INIT_DATA_.SECTION_WIDTH, + height = _PLOT_HEIGHT_, + y_label_func = io_label_function, + } +} + +local _WRITE_Y_ = header.bottom_y + _PLOT_HEIGHT_ + _PLOT_SEC_BREAK_ * 2 + +local writes = { + label = Widget.Text{ + x = _G_INIT_DATA_.CENTER_LEFT_X, + y = _WRITE_Y_, + text = 'Writes', + }, + rate = Widget.Text{ + x = _RIGHT_X_, + y = _WRITE_Y_, + x_align = 'right', + append_end =' B/s', + text_color = Patterns.BLUE + }, + plot = Widget.ScalePlot{ + x = _G_INIT_DATA_.CENTER_LEFT_X, + y = _WRITE_Y_ + _PLOT_SEC_BREAK_, + width = _G_INIT_DATA_.SECTION_WIDTH, + height = _PLOT_HEIGHT_, + y_label_func = io_label_function, + } +} + +Widget = nil +Patterns = nil +_PLOT_SEC_BREAK_ = nil +_PLOT_HEIGHT_ = nil +_RIGHT_X_ = nil +_WRITE_Y_ = nil + +reads.byte_cnt = 0 +writes.byte_cnt = 0 +reads.prev_byte_cnt, writes.prev_byte_cnt = read_stat_file() + +local update = function(cr, update_frequency) + local read_byte_cnt, write_byte_cnt = read_stat_file() + update_stat(cr, reads, read_byte_cnt, update_frequency) + update_stat(cr, writes, write_byte_cnt, update_frequency) +end + +local draw = function(cr, current_interface, update_frequency) + update(cr, update_frequency) + + if current_interface == 0 then + Text.draw(header.text, cr) + Line.draw(header.underline, cr) + + Text.draw(reads.label, cr) + Text.draw(reads.rate, cr) + ScalePlot.draw(reads.plot, cr) + + Text.draw(writes.label, cr) + Text.draw(writes.rate, cr) + ScalePlot.draw(writes.plot, cr) + end +end + +return draw diff --git a/drawing/System.lua b/drawing/System.lua new file mode 100644 index 0000000..bfece0d --- /dev/null +++ b/drawing/System.lua @@ -0,0 +1,67 @@ +local Widget = require 'Widget' +local Text = require 'Text' +local Line = require 'Line' +local TextColumn = require 'TextColumn' +local util = require 'util' +local Patterns = require 'Patterns' + +local __string_match = string.match + +local _TEXT_SPACING_ = 20 + +local extract_date = function(cmd) + local yyyy, mm_dd = __string_match(util.execute_cmd(cmd), '%[(%d-)%-(%d-%-%d-)%s') + return mm_dd..'-'..yyyy +end + +local header = Widget.Header{ + x = _G_INIT_DATA_.LEFT_X, + y = _G_INIT_DATA_.TOP_Y, + width = _G_INIT_DATA_.SECTION_WIDTH, + header = 'SYSTEM' +} + +local labels = Widget.TextColumn{ + x = _G_INIT_DATA_.LEFT_X, + y = header.bottom_y, + spacing = _TEXT_SPACING_, + 'Kernel', + 'Uptime', + 'Last Upgrade', + 'Last Sync' +} +local info = Widget.TextColumn{ + x = _G_INIT_DATA_.LEFT_X + _G_INIT_DATA_.SECTION_WIDTH, + y = header.bottom_y, + spacing = _TEXT_SPACING_, + x_align = 'right', + text_color = Patterns.BLUE, + util.conky('$kernel'), + '', + '', + '' +} + +Widget = nil +Patterns = nil +_TEXT_SPACING_ = nil + +local draw = function(cr, current_interface, log_is_changed) + TextColumn.set(info, cr, 2, util.conky('$uptime')) + + if log_is_changed then + TextColumn.set(info, cr, 3, extract_date("sed -n ".. + "'/ starting full system upgrade/p' /var/log/pacman.log | tail -1")) + TextColumn.set(info, cr, 4, extract_date("sed -n ".. + "'/ synchronizing package lists/p' /var/log/pacman.log | tail -1")) + end + + if current_interface == 0 then + Text.draw(header.text, cr) + Line.draw(header.underline, cr) + TextColumn.draw(labels, cr) + TextColumn.draw(info, cr) + end +end + +return draw diff --git a/drawing/Weather.lua b/drawing/Weather.lua new file mode 100644 index 0000000..3e04592 --- /dev/null +++ b/drawing/Weather.lua @@ -0,0 +1,482 @@ +local Widget = require 'Widget' +local Text = require 'Text' +local Line = require 'Line' +local TextColumn = require 'TextColumn' +local ScaledImage = require 'ScaledImage' +local util = require 'util' +local json = require 'json' +local Patterns = require 'Patterns' + +local __string_match = string.match +local __string_sub = string.sub +local __string_upper = string.upper +local __os_execute = os.execute + +local TIME_FORMAT = '%-I:%M %p' +local DATE_FORMAT = '%A' + +local NUM_ROWS = 8 +local WEATHER_UPDATE_INTERVAL = 900 + +local WEATHER_JSON_PATH = '/tmp/weather.json' +local ICON_DIR_PATH = _G_INIT_DATA_.ABS_PATH .. '/images/weather/' +local RECENTLY_UPDATED_PATH = '/tmp/weather_recently_updated' +local NA = 'N/A' +local NA_IMAGE_PATH = ICON_DIR_PATH .. 'na.png' + +local _SPACING_ = 20 +local _HEADER_PAD_ = 20 +local _ICON_SIDE_LENGTH_ = 75 +local _TEMP_SECTION_WIDTH_ = 220 +local _SECTION_HEIGHT_ = _HEADER_PAD_ + _ICON_SIDE_LENGTH_ + 30 + +local create_side_rows = function(side_rows_x, side_rows_y, side_rows_tbl) + for i = 1, NUM_ROWS do + side_rows_tbl[i] = {} + local current_row = side_rows_tbl[i] + local current_row_y = side_rows_y + (i - 1) * _SECTION_HEIGHT_ + + current_row.desc = Widget.Text{ + x = side_rows_x, + y = current_row_y, + text_color = Patterns.BLUE, + } + + current_row.period = Widget.Text{ + x = side_rows_x + _G_INIT_DATA_.SECTION_WIDTH, + y = current_row_y, + x_align = 'right', + text_color = Patterns.BLUE + } + + current_row.icon = Widget.ScaledImage{ + x = side_rows_x, + y = current_row_y + _HEADER_PAD_, + width = _ICON_SIDE_LENGTH_, + height = _ICON_SIDE_LENGTH_ + } + + current_row.temp1 = Widget.Text{ + x = side_rows_x + _ICON_SIDE_LENGTH_ + _TEMP_SECTION_WIDTH_ / 2, + y = current_row_y + _HEADER_PAD_ + 25, + x_align = 'center', + font_size = 28, + text_color = Patterns.BLUE + } + + current_row.temp2 = Widget.Text{ + x = side_rows_x + _ICON_SIDE_LENGTH_ + _TEMP_SECTION_WIDTH_ / 2, + y = current_row_y + _HEADER_PAD_ + 55, + x_align = 'center', + font_size = 11 + } + + current_row.label_column = Widget.TextColumn{ + x = side_rows_x + _ICON_SIDE_LENGTH_ + _TEMP_SECTION_WIDTH_, + y = current_row_y + _HEADER_PAD_ + 15, + spacing = _SPACING_, + 'H', + 'P', + 'W' + } + + current_row.info_column = Widget.TextColumn{ + x = side_rows_x + _G_INIT_DATA_.SECTION_WIDTH, + y = current_row_y + _HEADER_PAD_ + 15, + spacing = _SPACING_, + x_align = 'right', + text_color = Patterns.BLUE, + num_rows = 3 + } + + if i < NUM_ROWS then + current_row.separator = Widget.Line{ + p1 = { + x = side_rows_x, + y = current_row_y + _SECTION_HEIGHT_ - 18 + }, + p2 = { + x = side_rows_x + _G_INIT_DATA_.SECTION_WIDTH, + y = current_row_y + _SECTION_HEIGHT_ - 18 + }, + line_pattern = Patterns.MID_GREY + } + end + end +end + +-- LEFT +local left = { + header = Widget.Header{ + x = _G_INIT_DATA_.LEFT_X, + y = _G_INIT_DATA_.TOP_Y, + width = _G_INIT_DATA_.SECTION_WIDTH, + header = 'HOURLY FORECAST' + }, + hours = {} +} + +create_side_rows(_G_INIT_DATA_.LEFT_X, left.header.bottom_y, left.hours) + +-- CENTER +local center = {} + +center.header = Widget.Header{ + x = _G_INIT_DATA_.CENTER_LEFT_X, + y = _G_INIT_DATA_.TOP_Y, + width = _G_INIT_DATA_.CENTER_WIDTH, + header = 'CURRENT CONDITIONS' +} + +center.current_desc = Widget.Text{ + x = _G_INIT_DATA_.CENTER_LEFT_X, + y = center.header.bottom_y + 8, + text_color = Patterns.BLUE, + font_size = 24 +} + +local _CENTER_X_1_ = _G_INIT_DATA_.CENTER_LEFT_X + _G_INIT_DATA_.SECTION_WIDTH * 0.25 +local _CENTER_ICON_WIDTH_ = 120 + +center.icon = Widget.ScaledImage{ + x = _CENTER_X_1_ - _CENTER_ICON_WIDTH_ / 2, + y = center.header.bottom_y + 105 - _CENTER_ICON_WIDTH_ / 2, + width = _CENTER_ICON_WIDTH_, + height = _CENTER_ICON_WIDTH_ +} + +local _CENTER_X_2_ = _G_INIT_DATA_.CENTER_LEFT_X + _G_INIT_DATA_.SECTION_WIDTH * 0.70 +local _INFO_Y_ = center.header.bottom_y + 70 + +center.current_temp = Widget.Text{ + x = _CENTER_X_2_, + y = _INFO_Y_, + x_align = 'center', + font_size = 48, + text_color = Patterns.BLUE +} + +center.obs_time = Widget.Text{ + x = _CENTER_X_2_, + y = _INFO_Y_ + 42, + x_align = 'center', + font_size = 12, +} + +center.place = Widget.Text{ + x = _CENTER_X_2_, + y = _INFO_Y_ + 66, + x_align = 'center', + font_size = 12, +} + +local _COLUMN_PADDING_ = 15 +local _CENTER_SPACING_ = _SPACING_ + 7 + +center.label_column_1 = Widget.TextColumn{ + x = _G_INIT_DATA_.CENTER_RIGHT_X, + y = center.header.bottom_y, + spacing = _CENTER_SPACING_, + font_size = 14, + 'Feels Like', + 'Dewpoint', + 'Humidity', + 'Sky Coverage', + 'Visibility', + 'Ceiling', + 'Precipitation' +} + +center.info_column_1 = Widget.TextColumn{ + x = _G_INIT_DATA_.CENTER_RIGHT_X + (_G_INIT_DATA_.SECTION_WIDTH - _COLUMN_PADDING_) / 2, + y = center.header.bottom_y, + x_align = 'right', + text_color = Patterns.BLUE, + spacing = _CENTER_SPACING_, + font_size = 14, + num_rows = 7 +} + +center.label_column_2 = Widget.TextColumn{ + x = _G_INIT_DATA_.CENTER_RIGHT_X + (_G_INIT_DATA_.SECTION_WIDTH + _COLUMN_PADDING_) / 2, + y = center.header.bottom_y, + spacing = _CENTER_SPACING_, + font_size = 14, + 'WindSpd', + 'WindGust', + 'WindDir', + 'Pressure', + 'Sunrise', + 'Sunset', + 'Light Rate' +} + +center.info_column_2 = Widget.TextColumn{ + x = _G_INIT_DATA_.CENTER_RIGHT_X + _G_INIT_DATA_.SECTION_WIDTH, + y = center.header.bottom_y, + x_align = 'right', + text_color = Patterns.BLUE, + spacing = _CENTER_SPACING_, + font_size = 14, + num_rows = 7 +} + +-- RIGHT +local right = { + header = Widget.Header{ + x = _G_INIT_DATA_.RIGHT_X, + y = _G_INIT_DATA_.TOP_Y, + width = _G_INIT_DATA_.SECTION_WIDTH, + header = '8 DAY FORECAST' + }, + days = {} +} + +create_side_rows(_G_INIT_DATA_.RIGHT_X, right.header.bottom_y, right.days) + +Widget = nil +Patterns = nil + +_SPACING_ = nil +_HEADER_PAD_ = nil +_ICON_SIDE_LENGTH_ = nil +_TEMP_SECTION_WIDTH_ = nil +_SECTION_HEIGHT_ = nil +_CENTER_X_1_ = nil +_CENTER_ICON_WIDTH_ = nil +_CENTER_X_2_ = nil +_INFO_Y_ = nil +_COLUMN_PADDING_ = nil +_CENTER_SPACING_ = nil + +local populate_row = function(current_section, cr, desc, period, icon_path, + temp1, temp2, humidity, pop, wind) + if desc then + Text.set(current_section.desc, cr, Text.trim_to_length(desc, 20)) + else + Text.set(current_section.desc, cr, NA) + end + + Text.set(current_section.period, cr, period or NA) + + ScaledImage.set(current_section.icon, icon_path or NA_IMAGE_PATH) + + Text.set(current_section.temp1, cr, temp1 or NA) + Text.set(current_section.temp2, cr, temp2 or NA) + + TextColumn.set(current_section.info_column, cr, 1, humidity or NA) + TextColumn.set(current_section.info_column, cr, 2, pop or NA) + TextColumn.set(current_section.info_column, cr, 3, wind or NA) +end + +local populate_center = function(center_section, cr, desc, icon_path, temp, + obs_time, place, feels_like, dewpoint, humidity, coverage, visibility, ceiling, + precip, wind_spd, wind_gust_spd, wind_dir, pressure, sunrise, sunset, light) + + if desc then + Text.set(center_section.current_desc, cr, Text.trim_to_length(desc, 20)) + else + Text.set(center_section.current_desc, cr, NA) + end + + ScaledImage.set(center_section.icon, icon_path or NA_IMAGE_PATH) + + Text.set(center_section.current_temp, cr, temp or NA) + Text.set(center_section.obs_time, cr, obs_time or NA) + Text.set(center_section.place, cr, place or NA) + + local info_column_1 = center_section.info_column_1 + + TextColumn.set(info_column_1, cr, 1, feels_like or NA) + TextColumn.set(info_column_1, cr, 2, dewpoint or NA) + TextColumn.set(info_column_1, cr, 3, humidity or NA) + TextColumn.set(info_column_1, cr, 4, coverage or NA) + TextColumn.set(info_column_1, cr, 5, visibility or NA) + TextColumn.set(info_column_1, cr, 6, ceiling or NA) + TextColumn.set(info_column_1, cr, 7, precip or NA) + + local info_column_2 = center_section.info_column_2 + + TextColumn.set(info_column_2, cr, 1, wind_spd or NA) + TextColumn.set(info_column_2, cr, 2, wind_gust_spd or NA) + TextColumn.set(info_column_2, cr, 3, wind_dir or NA) + TextColumn.set(info_column_2, cr, 4, pressure or NA) + TextColumn.set(info_column_2, cr, 5, sunrise or NA) + TextColumn.set(info_column_2, cr, 6, sunset or NA) + TextColumn.set(info_column_2, cr, 7, light or NA) +end + +local update_interface = function(cr) + local file = util.read_file(WEATHER_JSON_PATH) + local data = (file ~= '') and json.decode(file) + + if data then + data = data.response.responses + + if data[1].success == false then + for i = 1, NUM_ROWS do populate_row(left.hours[i], cr) end + + populate_center(center, cr, nil, nil, nil, nil, 'Invalid Location') + + for i = 1, NUM_ROWS do populate_row(right.days[i], cr) end + else + -- LEFT + local hourly = data[2].response[1].periods + + for i = 1, NUM_ROWS do + local hour_data = hourly[i] + + populate_row( + left.hours[i], + cr, + hour_data.weatherPrimary, + hour_data.timestamp and util.convert_unix_time(hour_data.timestamp, TIME_FORMAT), + hour_data.icon and ICON_DIR_PATH..hour_data.icon, + hour_data.avgTempF and hour_data.avgTempF..'°F', + hour_data.feelslikeF and 'Feels like '..hour_data.feelslikeF..'°F', + hour_data.humidity and hour_data.humidity..' %', + hour_data.pop and hour_data.pop..' %', + hour_data.windSpeedMPH and hour_data.windSpeedMPH..' mph' + ) + end + + -- CENTER + local current_data = data[1].response + local ob = current_data.ob + + local place + if current_data.place then + place = current_data.place.name + if place then place = util.capitalize_each_word(__string_match(place, '([%w%s]+)/?')) end + + local state = current_data.place.state + if state == '' then state = nil end + + if place and state then + place = place..', '..__string_upper(state) + elseif place then + local country = current_data.place.country + if country then place = place..', '..__string_upper(country) end + end + end + + populate_center( + center, + cr, + ob.weather, + ob.icon and ICON_DIR_PATH..ob.icon, + ob.tempF and ob.tempF..'°F', + ob.timestamp and util.convert_unix_time(ob.timestamp, TIME_FORMAT), + place, + ob.feelslikeF and ob.feelslikeF..'°F', + ob.dewpointF and ob.dewpointF..'°F', + ob.humidity and ob.humidity..' %', + ob.sky and ob.sky..' %', + ob.visibilityMI and ob.visibilityMI..' mi', + ob.ceilingFT and ob.ceilingFT..' ft', + ob.precipIN and ob.precipIN..' in', + ob.windSpeedMPH and ob.windSpeedMPH..' mph', + ob.windGustMPH and ob.windGustMPH..' mph', + ob.windDirDEG and ob.windDirDEG..' deg', + ob.pressureMB and ob.pressureMB..' mbar', + ob.sunrise and util.convert_unix_time(ob.sunrise, TIME_FORMAT), + ob.sunset and util.convert_unix_time(ob.sunset, TIME_FORMAT), + ob.light and ob.light..' %' + ) + + -- RIGHT + local daily = data[3].response[1].periods + + for i = 1, NUM_ROWS do + local day_data = daily[i] + + populate_row( + right.days[i], + cr, + day_data.weatherPrimary, + day_data.timestamp and __string_sub(util.convert_unix_time( + day_data.timestamp, DATE_FORMAT), 1, 3), + day_data.icon and ICON_DIR_PATH..day_data.icon, + day_data.maxTempF and day_data.maxTempF..'°F', + day_data.minTempF and 'Low of '..day_data.minTempF..'°F', + day_data.humidity and day_data.humidity..' %', + day_data.pop and day_data.pop..' %', + day_data.windSpeedMPH and day_data.windSpeedMPH..' mph' + ) + end + end + else + for i = 1, NUM_ROWS do + populate_row(left.hours[i], cr) + populate_row(right.days[i], cr) + end + + populate_center(center, cr) + end +end + +local draw_sections = function(section_group, cr) + for i = 1, NUM_ROWS do + local section = section_group[i] + + if i < NUM_ROWS then Line.draw(section.separator, cr) end + + Text.draw(section.desc, cr) + Text.draw(section.period, cr) + ScaledImage.draw(section.icon) + Text.draw(section.temp1, cr) + Text.draw(section.temp2, cr) + TextColumn.draw(section.label_column, cr) + TextColumn.draw(section.info_column, cr) + end +end + +__os_execute('get_weather.sh') + +local update_cycle = WEATHER_UPDATE_INTERVAL + +local draw = function(cr, interface, interface_is_changed) + if update_cycle == 0 then __os_execute('get_weather.sh') end + + local json_is_recently_updated = (util.read_file(RECENTLY_UPDATED_PATH, nil, '*l') == 'true') + + if json_is_recently_updated then + update_cycle = WEATHER_UPDATE_INTERVAL + util.write_file(RECENTLY_UPDATED_PATH, 'false') + end + + if json_is_recently_updated or interface_is_changed then update_interface(cr) end + + update_cycle = update_cycle - 1 + + if interface == 1 then + -- LEFT + Text.draw(left.header.text, cr) + Line.draw(left.header.underline, cr) + + draw_sections(left.hours, cr) + + -- CENTER + Text.draw(center.header.text, cr) + Line.draw(center.header.underline, cr) + + Text.draw(center.current_desc, cr) + ScaledImage.draw(center.icon) + Text.draw(center.current_temp, cr) + Text.draw(center.obs_time, cr) + Text.draw(center.place, cr) + + TextColumn.draw(center.label_column_1, cr) + TextColumn.draw(center.info_column_1, cr) + TextColumn.draw(center.label_column_2, cr) + TextColumn.draw(center.info_column_2, cr) + + -- RIGHT + Text.draw(right.header.text, cr) + Line.draw(right.header.underline, cr) + + draw_sections(right.days, cr) + end +end + +return draw diff --git a/schema/Patterns.lua b/schema/Patterns.lua new file mode 100644 index 0000000..e209d03 --- /dev/null +++ b/schema/Patterns.lua @@ -0,0 +1,68 @@ +local M = {} + +local Color = require 'Color' + +local WHITE = 0xffffffff + +local GREY1 = 0xeeeeeeff +local GREY2 = 0xbfbfbfff +local GREY3 = 0xd6d6d6ff +local GREY4 = 0x888888ff +local GREY5 = 0x565656ff +local GREY6 = 0x2f2f2fb2 +local BLACK = 0x000000ff + +local BLUE1 = 0x99CEFFff +local BLUE2 = 0xBFE1FFff +local BLUE3 = 0x316BA6ff + +local RED1 = 0xFF3333ff +local RED2 = 0xFF8282ff +local RED3 = 0xFFB8B8ff + +local PURPLE1 = 0xeecfffff +local PURPLE2 = 0xcb91ffff +local PURPLE3 = 0x9523ffff + +M.WHITE = Color.init{hex_rgba = WHITE} + +M.LIGHT_GREY = Color.init{hex_rgba = GREY1} +M.MID_GREY = Color.init{hex_rgba = GREY3} +M.DARK_GREY = Color.init{hex_rgba = GREY4} + +M.BLUE = Color.init{hex_rgba = BLUE2} +M.RED = Color.init{hex_rgba = RED2} +M.PURPLE = Color.init{hex_rgba = PURPLE2} + +M.GREY_ROUNDED = Color.Gradient{ + Color.ColorStop{hex_rgba = GREY5, stop = 0.0}, + Color.ColorStop{hex_rgba = GREY2, stop = 0.5}, + Color.ColorStop{hex_rgba = GREY5, stop = 1.0} +} + +M.BLUE_ROUNDED = Color.Gradient{ + Color.ColorStop{hex_rgba = BLUE3, stop = 0.0}, + Color.ColorStop{hex_rgba = BLUE1, stop = 0.5}, + Color.ColorStop{hex_rgba = BLUE3, stop = 1.0} +} + +M.RED_ROUNDED = Color.Gradient{ + Color.ColorStop{hex_rgba = RED1, stop = 0.0}, + Color.ColorStop{hex_rgba = RED3, stop = 0.5}, + Color.ColorStop{hex_rgba = RED1, stop = 1.0} +} + +M.PURPLE_ROUNDED = Color.Gradient{ + Color.ColorStop{hex_rgba = PURPLE3, stop = 0.0}, + Color.ColorStop{hex_rgba = PURPLE1, stop = 0.5}, + Color.ColorStop{hex_rgba = PURPLE3, stop = 1.0} +} + +M.TRANSPARENT_BLACK = Color.init{hex_rgba = BLACK, force_alpha = 0.7} + +M.TRANSPARENT_BLUE = Color.Gradient{ + Color.ColorStop{hex_rgba = BLUE3, stop = 0.0, force_alpha = 0.2}, + Color.ColorStop{hex_rgba = BLUE1, stop = 1.0, force_alpha = 1.0} +} + +return M