961 lines
35 KiB
Python
961 lines
35 KiB
Python
import math
|
|
import os
|
|
import re
|
|
import subprocess
|
|
|
|
import cairocffi
|
|
import iwlib
|
|
import netifaces
|
|
import psutil
|
|
from libqtile import bar, qtile
|
|
from libqtile.log_utils import logger
|
|
from libqtile.widget import base
|
|
from libqtile.widget.base import ORIENTATION_HORIZONTAL
|
|
from libqtile.widget.battery import default_icon_path, load_battery, BatteryState
|
|
from libqtile.widget.wlan import get_status
|
|
from libqtile.widget.groupbox import GroupBox
|
|
from libqtile.command.base import expose_command
|
|
|
|
from kuro.utils.general import notify, BUTTON_LEFT, BUTTON_MIDDLE, BUTTON_RIGHT, BUTTON_DOWN, BUTTON_UP, BUTTON_MUTE, \
|
|
call_process
|
|
|
|
|
|
class DualPaneTextboxBase(base._Widget):
|
|
"""
|
|
Base class for widgets that are two boxes next to each other both containing text.
|
|
"""
|
|
orientations = ORIENTATION_HORIZONTAL
|
|
defaults = [
|
|
("font", "sans", "Default font"),
|
|
("fontsize_left", None, "Font size of left text field. Calculated if None."),
|
|
("fontsize_right", None, "Font size of right text field. Calculated if None."),
|
|
("padding", None, "Padding. Calculated if None."),
|
|
("padding_between", None, "Padding between left and right. Calculated if None."),
|
|
("foreground", "ffffff", "Foreground colour"),
|
|
("fontshadow", None, "font shadow color, default is None(no shadow)"),
|
|
("markup", False, "Whether or not to use pango markup"),
|
|
]
|
|
changed = False
|
|
|
|
def __init__(self, text_left=" ", text_right=" ", width=bar.CALCULATED, **config):
|
|
self.layout_left = None
|
|
self.layout_right = None
|
|
base._Widget.__init__(self, width, **config)
|
|
self._text_left = None
|
|
self._text_right = None
|
|
self.text_left = text_left
|
|
self.text_right = text_right
|
|
self.changed = False
|
|
self.add_defaults(DualPaneTextboxBase.defaults)
|
|
|
|
@property
|
|
def text_left(self):
|
|
return self._text_left
|
|
|
|
@text_left.setter
|
|
def text_left(self, value):
|
|
assert value is None or isinstance(value, str)
|
|
if value != self._text_left:
|
|
self.changed = True
|
|
self._text_left = value
|
|
if self.layout_left:
|
|
self.layout_left.text = value
|
|
@property
|
|
def text_right(self):
|
|
return self._text_right
|
|
|
|
@text_right.setter
|
|
def text_right(self, value):
|
|
assert value is None or isinstance(value, str)
|
|
if value != self._text_right:
|
|
self.changed = True
|
|
self._text_right = value
|
|
if self.layout_right:
|
|
self.layout_right.text = value
|
|
|
|
@property
|
|
def foreground(self):
|
|
return self._foreground
|
|
|
|
@foreground.setter
|
|
def foreground(self, fg):
|
|
self._foreground = fg
|
|
if self.layout_left:
|
|
self.layout_left.colour = fg
|
|
if self.layout_right:
|
|
self.layout_right.colour = fg
|
|
|
|
@property
|
|
def font(self):
|
|
return self._font
|
|
|
|
@font.setter
|
|
def font(self, value):
|
|
self._font = value
|
|
if self.layout_left:
|
|
self.layout_left.font = value
|
|
if self.layout_right:
|
|
self.layout_right.font = value
|
|
|
|
@property
|
|
def font_left(self):
|
|
return self._font_left
|
|
|
|
@font_left.setter
|
|
def font_left(self, value):
|
|
self._font_left = value
|
|
if self.layout_left:
|
|
self.layout_left.font = value
|
|
|
|
@property
|
|
def font_right(self):
|
|
return self._font_right
|
|
|
|
@font_right.setter
|
|
def font_right(self, value):
|
|
self._font_right = value
|
|
if self.layout_right:
|
|
self.layout_right.font = value
|
|
|
|
@property
|
|
def fontshadow(self):
|
|
return self._fontshadow
|
|
|
|
@fontshadow.setter
|
|
def fontshadow(self, value):
|
|
self._fontshadow = value
|
|
if self.layout_left:
|
|
self.layout_left.font_shadow = value
|
|
if self.layout_right:
|
|
self.layout_right.font_shadow = value
|
|
|
|
@property
|
|
def actual_padding(self):
|
|
if self.padding is None:
|
|
return min(self.fontsize_left, self.fontsize_right) / 2
|
|
else:
|
|
return self.padding
|
|
|
|
@property
|
|
def actual_padding_between(self):
|
|
if self.padding_between is None:
|
|
return max(self.fontsize_left, self.fontsize_right) / 4
|
|
else:
|
|
return self.padding_between
|
|
|
|
def _configure(self, qtile, bar):
|
|
base._Widget._configure(self, qtile, bar)
|
|
if self.fontsize_left is None:
|
|
self.fontsize_left = self.bar.height - self.bar.height / 5
|
|
if self.fontsize_right is None:
|
|
self.fontsize_right = self.bar.height - self.bar.height / 5
|
|
self.layout_left = self.drawer.textlayout(
|
|
self.text_left,
|
|
self.foreground,
|
|
self.font,
|
|
self.fontsize_left,
|
|
self.fontshadow,
|
|
markup=self.markup,
|
|
)
|
|
self.layout_right = self.drawer.textlayout(
|
|
self.text_right,
|
|
self.foreground,
|
|
self.font,
|
|
self.fontsize_right,
|
|
self.fontshadow,
|
|
markup=self.markup,
|
|
)
|
|
|
|
def calculate_length(self):
|
|
length = 0
|
|
if self.text_left:
|
|
length += min(
|
|
self.layout_left.width,
|
|
self.bar.width
|
|
) + self.actual_padding * 2
|
|
if self.text_right:
|
|
length += min(
|
|
self.layout_right.width,
|
|
self.bar.width
|
|
) + self.actual_padding * 2
|
|
return length
|
|
|
|
def draw(self):
|
|
# if the bar hasn't placed us yet
|
|
if self.offsetx is None:
|
|
return
|
|
self.drawer.clear(self.background or self.bar.background)
|
|
self.layout_left.draw(
|
|
self.actual_padding or 0,
|
|
int(self.bar.height / 2.0 - self.layout_left.height / 2.0) + 1
|
|
)
|
|
left_width = self.layout_left.width
|
|
self.layout_right.draw(
|
|
(self.actual_padding or 0) + left_width + (self.actual_padding_between or 0),
|
|
int(self.bar.height / 2.0 - self.layout_right.height / 2.0) + 1
|
|
)
|
|
self.drawer.draw(offsetx=self.offsetx, width=self.width)
|
|
if self.changed:
|
|
# Text changed, update the bar to propagate any changes in width
|
|
self.bar.draw()
|
|
self.changed = False
|
|
|
|
@expose_command()
|
|
def set_font(self, font=base.UNSPECIFIED, fontsize_left=base.UNSPECIFIED, fontsize_right=base.UNSPECIFIED, fontshadow=base.UNSPECIFIED):
|
|
"""
|
|
Change the font used by this widget. If font is None, the current
|
|
font is used.
|
|
"""
|
|
if font is not base.UNSPECIFIED:
|
|
self.font = font
|
|
if fontsize_left is not base.UNSPECIFIED:
|
|
self.fontsize_left = fontsize_left
|
|
if fontsize_right is not base.UNSPECIFIED:
|
|
self.fontsize_right = fontsize_right
|
|
if fontshadow is not base.UNSPECIFIED:
|
|
self.fontshadow = fontshadow
|
|
self.bar.draw()
|
|
|
|
def info(self):
|
|
d = base._Widget.info(self)
|
|
d['foreground'] = self.foreground
|
|
d['text_left'] = self.text_left
|
|
d['text_right'] = self.text_right
|
|
return d
|
|
|
|
|
|
class GPUStatusWidget(base._TextBox):
|
|
"""Displays the currently used GPU."""
|
|
|
|
orientations = base.ORIENTATION_HORIZONTAL
|
|
defaults = [
|
|
('check_command', 'optimus-manager --print-mode', 'The command that shows the current mode.'),
|
|
('next_command', 'optimus-manager --print-next-mode', 'The command that shows the mode after reboot.'),
|
|
('update_interval', 60, 'The update interval in seconds.'),
|
|
('theme_path', default_icon_path(), 'Path of the icons'),
|
|
('custom_icons', {}, 'dict containing key->filename icon map'),
|
|
]
|
|
|
|
def __init__(self, **config):
|
|
super(GPUStatusWidget, self).__init__("GPU", bar.CALCULATED, **config)
|
|
self.add_defaults(GPUStatusWidget.defaults)
|
|
|
|
if self.theme_path:
|
|
self.length_type = bar.STATIC
|
|
self.length = 0
|
|
self.surfaces = {}
|
|
self.current_icon = 'gpu-unknown'
|
|
self.icons = dict([(x, '{0}.png'.format(x)) for x in (
|
|
'gpu-intel',
|
|
'gpu-nvidia',
|
|
'gpu-unknown',
|
|
)])
|
|
self.current_status = "Unknown"
|
|
self.icons.update(self.custom_icons)
|
|
|
|
def _get_info(self):
|
|
try:
|
|
output = self.call_process(self.check_command, shell=True)
|
|
except subprocess.CalledProcessError as e:
|
|
logger.error(f"Error while calling {self.check_command} - {e}")
|
|
output = None
|
|
mode = "nvidia" if "nvidia" in output else "intel" if "intel" in output else "unknown"
|
|
return {'error': False, 'mode': mode}
|
|
|
|
def timer_setup(self):
|
|
self.update()
|
|
self.timeout_add(self.update_interval, self.timer_setup)
|
|
|
|
def _configure(self, qtile, bar):
|
|
super(GPUStatusWidget, self)._configure(qtile, bar)
|
|
self.setup_images()
|
|
|
|
def _get_icon_key(self):
|
|
key = 'gpu'
|
|
info = self._get_info()
|
|
if info.get('mode') == "intel":
|
|
key += '-intel'
|
|
self.current_status = "Intel"
|
|
elif info.get('mode') == "nvidia":
|
|
key += '-nvidia'
|
|
self.current_status = "NVidia"
|
|
else:
|
|
key += '-unknown'
|
|
self.current_status = "Unknown"
|
|
return key
|
|
|
|
def update(self):
|
|
icon = self._get_icon_key()
|
|
if icon != self.current_icon:
|
|
self.current_icon = icon
|
|
self.draw()
|
|
|
|
def draw(self):
|
|
if self.theme_path:
|
|
self.drawer.clear(self.background or self.bar.background)
|
|
self.drawer.ctx.set_source(self.surfaces[self.current_icon])
|
|
self.drawer.ctx.paint()
|
|
self.drawer.draw(offsetx=self.offset, width=self.length)
|
|
else:
|
|
self.text = self.current_icon[8:]
|
|
base._TextBox.draw(self)
|
|
|
|
def setup_images(self):
|
|
for key, name in self.icons.items():
|
|
try:
|
|
path = os.path.join(self.theme_path, name)
|
|
img = cairocffi.ImageSurface.create_from_png(path)
|
|
except cairocffi.Error:
|
|
self.theme_path = None
|
|
logger.warning('GPU Status Icon switching to text mode')
|
|
return
|
|
input_width = img.get_width()
|
|
input_height = img.get_height()
|
|
|
|
sp = input_height / (self.bar.height - 1)
|
|
|
|
width = input_width / sp
|
|
if width > self.length:
|
|
# cast to `int` only after handling all potentially-float values
|
|
self.length = int(width + self.actual_padding * 2)
|
|
|
|
imgpat = cairocffi.SurfacePattern(img)
|
|
|
|
scaler = cairocffi.Matrix()
|
|
|
|
scaler.scale(sp, sp)
|
|
scaler.translate(self.actual_padding * -1, 0)
|
|
imgpat.set_matrix(scaler)
|
|
|
|
imgpat.set_filter(cairocffi.FILTER_BEST)
|
|
self.surfaces[key] = imgpat
|
|
|
|
def button_press(self, x, y, button):
|
|
if button == BUTTON_LEFT:
|
|
try:
|
|
next_gpu = self.call_process(self.next_command, shell=True).split(":")[1].strip()
|
|
except subprocess.CalledProcessError as e:
|
|
logger.error(f"Error while calling {self.next_command} - {e}")
|
|
except IndexError:
|
|
next_gpu = "Unknown"
|
|
|
|
if self.current_status == "Unknown":
|
|
notify(None, "GPU Status", "The currently used GPU is <b>unknown</b>.\n\nAfter the next login it will be the <b>{}</b> GPU.".format(next_gpu),
|
|
image=os.path.join(self.theme_path, "gpu-unknown.png"))
|
|
else:
|
|
notify(None, "GPU Status", "The system is currently running on the <b>{}</b> GPU. Press the middle mouse "
|
|
"button on this icon to switch GPUs.\n\nAfter the next login it will be the <b>{}</b> GPU.".format(
|
|
self.current_status, next_gpu
|
|
),
|
|
image=os.path.join(self.theme_path, "gpu-{}.png".format(self.current_status.lower()))
|
|
)
|
|
|
|
if button == BUTTON_MIDDLE:
|
|
command = ["optimus-manager", "--no-confirm", "--switch", "auto"]
|
|
try:
|
|
output = self.call_process(command)
|
|
except subprocess.CalledProcessError as e:
|
|
logger.error(f"Error while calling {command} - {e}")
|
|
output = ""
|
|
if "nvidia" in output:
|
|
notify(None, "GPU Switched", "The GPU has been switched from Intel to NVidia.\n"
|
|
"Please log out and log back in to apply the changes to the session.",
|
|
image=os.path.join(self.theme_path, "gpu-nvidia.png"))
|
|
elif "intel" in output:
|
|
notify(None, "GPU Switched", "The GPU has been switched from NVidia to Intel.\n"
|
|
"Please log out and log back in to apply the changes to the session.",
|
|
image=os.path.join(self.theme_path, "gpu-intel.png"))
|
|
else:
|
|
notify(None, "GPU Switch Error", "I could not determine if the GPU was switched successfully.\n"
|
|
"Please log out and log back in to clear up the inconsistency.",
|
|
image=os.path.join(self.theme_path, "gpu-unknown.png"))
|
|
|
|
|
|
class ThermalSensorWidget(DualPaneTextboxBase):
|
|
defaults = [
|
|
('show_tag', False, 'Show tag sensor'),
|
|
('update_interval', 2, 'Update interval in seconds'),
|
|
('tag_sensor', None, 'Tag of the temperature sensor. For example: "temp1" or "Core 0"'),
|
|
('chip', None, 'Chip argument for sensors command'),
|
|
('threshold', 70, 'If the current temperature value is above, then change to foreground_alert colour'),
|
|
('foreground_alert', 'ff0000', 'Foreground colour alert'),
|
|
]
|
|
|
|
def __init__(self, **config):
|
|
super(ThermalSensorWidget, self).__init__("TEMP", "", bar.CALCULATED, **config)
|
|
self.add_defaults(ThermalSensorWidget.defaults)
|
|
self.sensors_temp = re.compile(
|
|
(r"\n([\w ]+):" # Sensor tag name
|
|
r"\s+[+|-]" # temp signed
|
|
r"(\d+\.\d+)" # temp value
|
|
"({degrees}" # degree symbol match
|
|
"[C|F])" # Celsius or Fahrenheit
|
|
).format(degrees=u"\xb0"),
|
|
re.UNICODE | re.VERBOSE
|
|
)
|
|
self.value_temp = re.compile(r"\d+\.\d+")
|
|
self.foreground_normal = self.foreground
|
|
self.values = None
|
|
|
|
def get_command(self):
|
|
command = ["sensors"]
|
|
if self.chip:
|
|
command.append(self.chip)
|
|
return command
|
|
|
|
def timer_setup(self):
|
|
self.update()
|
|
self.timeout_add(self.update_interval, self.timer_setup)
|
|
|
|
def _update_values(self):
|
|
try:
|
|
sensors_out = self.call_process(self.get_command())
|
|
except subprocess.CalledProcessError as e:
|
|
logger.error(f"Error while calling {self.get_command()} - {e}")
|
|
return
|
|
temperature_values = {}
|
|
for name, temp, symbol in self.sensors_temp.findall(sensors_out):
|
|
name = name.strip()
|
|
temperature_values[name] = temp, symbol
|
|
self.values = temperature_values
|
|
|
|
def update(self):
|
|
self._update_values()
|
|
self.draw()
|
|
|
|
def draw(self):
|
|
self.text_left = ""
|
|
if self.values is None:
|
|
self.text_right = None
|
|
else:
|
|
text = ""
|
|
if self.show_tag and self.tag_sensor is not None:
|
|
text = self.tag_sensor + ": "
|
|
text += "".join(self.values.get(self.tag_sensor, ['N/A']))
|
|
temp_value = float(self.values.get(self.tag_sensor, [0])[0])
|
|
if temp_value > self.threshold:
|
|
self.layout_right.colour = self.foreground_alert
|
|
else:
|
|
self.layout_right.colour = self.foreground_normal
|
|
self.text_right = text
|
|
super(ThermalSensorWidget, self).draw()
|
|
|
|
def button_press(self, x, y, button):
|
|
if button == BUTTON_LEFT:
|
|
notify(None, "Temperature Information", "\n".join(
|
|
"{}: {}{}".format(name, *values) for name, values in self.values.items()
|
|
))
|
|
|
|
|
|
class CPUInfoWidget(DualPaneTextboxBase):
|
|
"""Displays information about the CPU usage"""
|
|
orientations = base.ORIENTATION_HORIZONTAL
|
|
defaults = [
|
|
('update_interval', 3, 'The update interval in seconds.'),
|
|
('text_pattern', "{cpu}%", 'The pattern for the text that is displayed.'),
|
|
('normal_color', "#ffffff", "Color when value is normal"),
|
|
('warning_color', "#ffffff", "Color when value is warning"),
|
|
('critical_color', "#ffffff", "Color when value is critical"),
|
|
]
|
|
|
|
def __init__(self, **config):
|
|
super(CPUInfoWidget, self).__init__("CPU", "", bar.CALCULATED, **config)
|
|
self.add_defaults(CPUInfoWidget.defaults)
|
|
self.text = "..."
|
|
self.cpu = 0
|
|
self.cpu_old = [0, 0, 0, 0]
|
|
|
|
def timer_setup(self):
|
|
self.update()
|
|
self.timeout_add(self.update_interval, self.timer_setup)
|
|
|
|
def _update_values(self):
|
|
cpu = psutil.cpu_times()
|
|
user = cpu.user * 100
|
|
nice = cpu.nice * 100
|
|
sys = cpu.system * 100
|
|
idle = cpu.idle * 100
|
|
nval = (int(user), int(nice), int(sys), int(idle))
|
|
oval = self.cpu_old
|
|
busy = nval[0] + nval[1] + nval[2] - oval[0] - oval[1] - oval[2]
|
|
total = busy + nval[3] - oval[3]
|
|
if total:
|
|
self.cpu = math.ceil(busy * 100.0 / total)
|
|
self.cpu_old = nval
|
|
|
|
def update(self):
|
|
self._update_values()
|
|
self.draw()
|
|
|
|
def draw(self):
|
|
self.text_left = ""
|
|
self.text_right = self.text_pattern.format(cpu=self.cpu)
|
|
super(CPUInfoWidget, self).draw()
|
|
|
|
def button_press(self, x, y, button):
|
|
if button == BUTTON_LEFT:
|
|
total = sum([self.cpu_old[0], self.cpu_old[1], self.cpu_old[2], self.cpu_old[3]])
|
|
notify(None, "CPU Information", "user: {} %\nnice: {} %\nsys: {} %\nidle: {} %\ntotal: {} %".format(
|
|
math.ceil((self.cpu_old[0] / total) * 100),
|
|
math.ceil((self.cpu_old[1] / total) * 100),
|
|
math.ceil((self.cpu_old[2] / total) * 100),
|
|
math.ceil((self.cpu_old[3] / total) * 100),
|
|
self.cpu
|
|
))
|
|
|
|
|
|
class MemoryInfoWidget(DualPaneTextboxBase):
|
|
"""Displays information about the Memory usage"""
|
|
orientations = base.ORIENTATION_HORIZONTAL
|
|
defaults = [
|
|
('update_interval', 3, 'The update interval in seconds.'),
|
|
('text_pattern', "{mem}%", 'The pattern for the text that is displayed.'),
|
|
('normal_color', "#ffffff", "Color when value is normal"),
|
|
('warning_color', "#ffffff", "Color when value is warning"),
|
|
('critical_color', "#ffffff", "Color when value is critical"),
|
|
]
|
|
|
|
def __init__(self, **config):
|
|
super(MemoryInfoWidget, self).__init__("MEM", "", bar.CALCULATED, **config)
|
|
self.add_defaults(MemoryInfoWidget.defaults)
|
|
self.text = "..."
|
|
self.memory = 0
|
|
|
|
def timer_setup(self):
|
|
self.update()
|
|
self.timeout_add(self.update_interval, self.timer_setup)
|
|
|
|
def _update_values(self):
|
|
mem = psutil.virtual_memory()
|
|
self.memory = math.ceil((mem.used / mem.total) * 100)
|
|
|
|
def update(self):
|
|
self._update_values()
|
|
self.draw()
|
|
|
|
def draw(self):
|
|
self.text_left = ""
|
|
self.text_right = self.text_pattern.format(mem=self.memory)
|
|
super(MemoryInfoWidget, self).draw()
|
|
|
|
def button_press(self, x, y, button):
|
|
mem = psutil.virtual_memory()
|
|
swap = psutil.swap_memory()
|
|
val = {}
|
|
val['MemUsed'] = mem.used // 1024 // 1024
|
|
val['MemTotal'] = mem.total // 1024 // 1024
|
|
val['SwapTotal'] = swap.total // 1024 // 1024
|
|
val['SwapUsed'] = swap.used // 1024 // 1024
|
|
|
|
if button == BUTTON_LEFT:
|
|
notify(None, "Memory Information", "Memory: {}MB / {}MB\n {}%\nSwap: {}MB / {}MB\n {}%".format(
|
|
val['MemUsed'], val['MemTotal'],
|
|
math.ceil((mem.used / mem.total) * 100),
|
|
val['SwapUsed'], val['SwapTotal'],
|
|
math.ceil((swap.used / swap.total) * 100),
|
|
))
|
|
|
|
|
|
class DiskIOInfoWidget(DualPaneTextboxBase):
|
|
"""Displays information about the DiskIO usage"""
|
|
orientations = base.ORIENTATION_HORIZONTAL
|
|
defaults = [
|
|
('update_interval', 3, 'The update interval in seconds.'),
|
|
('text_pattern', "{io}ms", 'The pattern for the text that is displayed.'),
|
|
('hdd_device', 'nvme0n1', 'The device name of the disk to use for IO stats'),
|
|
('normal_color', "#ffffff", "Color when value is normal"),
|
|
('warning_color', "#ffffff", "Color when value is warning"),
|
|
('critical_color', "#ffffff", "Color when value is critical"),
|
|
]
|
|
|
|
def __init__(self, **config):
|
|
super(DiskIOInfoWidget, self).__init__("DiskIO", "", bar.CALCULATED, **config)
|
|
self.add_defaults(DiskIOInfoWidget.defaults)
|
|
self.text = "..."
|
|
self.io = 0
|
|
self.io_old = 0
|
|
self.hdd_path = '/sys/block/{dev}/stat'.format(
|
|
dev=self.hdd_device
|
|
)
|
|
|
|
def timer_setup(self):
|
|
self.update()
|
|
self.timeout_add(self.update_interval, self.timer_setup)
|
|
|
|
def _update_values(self):
|
|
try:
|
|
# io_ticks is field number 9
|
|
with open(self.hdd_path) as f:
|
|
io_ticks = int(f.read().split()[9])
|
|
except IOError:
|
|
return 0
|
|
activity = io_ticks - self.io_old
|
|
self.io_old = io_ticks
|
|
self.io = math.ceil(activity)
|
|
|
|
def update(self):
|
|
self._update_values()
|
|
self.draw()
|
|
|
|
def draw(self):
|
|
self.text_left = ""
|
|
self.text_right = self.text_pattern.format(io=self.io)
|
|
super(DiskIOInfoWidget, self).draw()
|
|
|
|
def button_press(self, x, y, button):
|
|
if button == BUTTON_LEFT:
|
|
notify(None, "Disk IO Information",
|
|
"Time that there were IO requests queued for /dev/{}: {} ms".format(self.hdd_device, self.io))
|
|
|
|
|
|
class NetworkInfoWidget(DualPaneTextboxBase):
|
|
"""Displays information about the (wireless) network that we are connected to"""
|
|
orientations = base.ORIENTATION_HORIZONTAL
|
|
wired_up_regex = re.compile(" state (DOWN|UP) ")
|
|
defaults = [
|
|
('update_interval', 10, 'The update interval in seconds.'),
|
|
('text_pattern', "{name}", 'The pattern for the text that is displayed.'),
|
|
('text_disconnected', "", "The text to show when not connected using WiFi"),
|
|
('normal_color', "#ffffff", "Color when value is normal"),
|
|
('warning_color', "#ffffff", "Color when value is warning"),
|
|
('critical_color', "#ffffff", "Color when value is critical"),
|
|
('wireless_interface', "wifi0", "Wireless interface device name"),
|
|
('wired_interface', "enp7s0", "Wired interface device name"),
|
|
]
|
|
|
|
def __init__(self, **config):
|
|
super(NetworkInfoWidget, self).__init__("NET", "", bar.CALCULATED, **config)
|
|
self.add_defaults(NetworkInfoWidget.defaults)
|
|
self.wireless_text = "..."
|
|
self.wireless_quality = 0
|
|
self.wireless_signal = 0
|
|
self.wireless_name = 0
|
|
self.wireless_connected = False
|
|
self.wireless_accesspoint = ""
|
|
self.wireless_frequency = ""
|
|
self.wireless_ipv4 = ""
|
|
self.wireless_ipv6 = ""
|
|
self.wireless_mac = ""
|
|
|
|
self.wired_name = 0
|
|
self.wired_connected = False
|
|
self.wired_ipv4 = ""
|
|
self.wired_ipv6 = ""
|
|
self.wired_mac = ""
|
|
|
|
def timer_setup(self):
|
|
self.update()
|
|
self.timeout_add(self.update_interval, self.timer_setup)
|
|
|
|
def _update_values(self):
|
|
# Wifi
|
|
try:
|
|
essid, quality = get_status(self.wireless_interface)
|
|
status = iwlib.get_iwconfig(self.wireless_interface)
|
|
self.wireless_ips = netifaces.ifaddresses(self.wireless_interface)
|
|
disconnected = essid is None
|
|
percent = math.ceil(((quality or 0) / 70) * 100)
|
|
self.wireless_quality = quality
|
|
self.wireless_signal = percent
|
|
self.wireless_name = essid
|
|
self.wireless_connected = not disconnected
|
|
self.wireless_accesspoint = status.get('Access Point', b'Unknown').decode()
|
|
self.wireless_frequency = status.get('Frequency', b'Unknown').decode()
|
|
self.wireless_ipv4 = self.wireless_ips.get(netifaces.AF_INET, [{'addr': ""}])[0]['addr']
|
|
self.wireless_ipv6 = self.wireless_ips.get(netifaces.AF_INET6, [{'addr': ""}])[0]['addr']
|
|
self.wireless_mac = self.wireless_ips.get(netifaces.AF_LINK, [{'addr': ""}])[0]['addr']
|
|
except EnvironmentError:
|
|
pass
|
|
|
|
# Wired
|
|
try:
|
|
self.wired_ips = netifaces.ifaddresses(self.wired_interface)
|
|
self.wired_ipv4 = self.wired_ips.get(netifaces.AF_INET, [{'addr': ""}])[0]['addr']
|
|
self.wired_ipv6 = self.wired_ips.get(netifaces.AF_INET6, [{'addr': ""}])[0]['addr']
|
|
self.wired_mac = self.wired_ips.get(netifaces.AF_LINK, [{'addr': ""}])[0]['addr']
|
|
command = ["ip", "link", "show", "{}".format(self.wired_interface)]
|
|
try:
|
|
eth_status = call_process(command)
|
|
except subprocess.CalledProcessError as e:
|
|
logger.error(f"Error while calling {command} - {e}")
|
|
return
|
|
m = self.wired_up_regex.search(eth_status)
|
|
if m:
|
|
self.wired_connected = "UP" in m.group(1)
|
|
else:
|
|
self.wired_connected = False
|
|
|
|
except (EnvironmentError, ValueError):
|
|
pass
|
|
|
|
def update(self):
|
|
self._update_values()
|
|
self.draw()
|
|
|
|
def draw(self):
|
|
if self.wireless_connected:
|
|
strength = ""
|
|
if self.wireless_signal < 66:
|
|
strength = ""
|
|
if self.wireless_signal < 33:
|
|
strength = ""
|
|
self.text_left = strength
|
|
else:
|
|
self.text_left = ""
|
|
|
|
if self.wired_connected:
|
|
self.text_right = ""
|
|
else:
|
|
self.text_right = ""
|
|
|
|
super(NetworkInfoWidget, self).draw()
|
|
|
|
def button_press(self, x, y, button):
|
|
if button == BUTTON_LEFT:
|
|
if self.wireless_connected:
|
|
title = "Wireless {}".format(self.wireless_interface)
|
|
wireless_ipv4 = "\nIPv4: {}".format(self.wireless_ipv4) if self.wireless_ipv4 else ""
|
|
wireless_ipv6 = "\nIPv6: {}".format(self.wireless_ipv6) if self.wireless_ipv6 else ""
|
|
wireless_mac = "\nMAC: {}".format(self.wireless_mac) if self.wireless_mac else ""
|
|
wifi_text = "SSID: {}\n" \
|
|
"Quality: {} / 70\n"\
|
|
"Strength: {}%" \
|
|
"{}{}{}".format(self.wireless_name, self.wireless_quality,
|
|
self.wireless_signal, wireless_ipv4, wireless_ipv6, wireless_mac)
|
|
else:
|
|
title = "Wireless: Not connected"
|
|
wifi_text = ""
|
|
|
|
if self.wired_connected:
|
|
wired_ipv4 = "\nIPv4: {}".format(self.wired_ipv4) if self.wired_ipv4 else ""
|
|
wired_ipv6 = "\nIPv6: {}".format(self.wired_ipv6) if self.wired_ipv6 else ""
|
|
wired_mac = "\nMAC: {}".format(self.wired_mac) if self.wired_mac else ""
|
|
wired_text = "<b>Wired {}</b>" \
|
|
"{}{}{}".format(self.wired_interface, wired_ipv4, wired_ipv6, wired_mac)
|
|
else:
|
|
wired_text = "<b>Wired: Not connected</b>"
|
|
|
|
if wifi_text:
|
|
notify(None, title, "{}\n\n{}".format(wifi_text, wired_text))
|
|
else:
|
|
notify(None, title, "\n{}".format(wired_text))
|
|
|
|
|
|
class BatteryInfoWidget(DualPaneTextboxBase):
|
|
"""Displays information about the battery"""
|
|
orientations = base.ORIENTATION_HORIZONTAL
|
|
status_cmd = "acpi"
|
|
defaults = [
|
|
('update_interval', 10, 'The update interval in seconds.'),
|
|
('text_pattern', "{percentage}%", 'The pattern for the text that is displayed.'),
|
|
('charging_color', "#ffffff", "Color when battery is charging"),
|
|
('normal_color', "#ffffff", "Color when value is normal"),
|
|
('warning_color', "#ffffff", "Color when value is warning"),
|
|
('critical_color', "#ffffff", "Color when value is critical"),
|
|
('battery', 0, 'Which battery should be monitored'),
|
|
]
|
|
|
|
def __init__(self, **config):
|
|
super(BatteryInfoWidget, self).__init__("BAT", "", bar.CALCULATED, **config)
|
|
self.add_defaults(BatteryInfoWidget.defaults)
|
|
self.text = "..."
|
|
self.state = 0
|
|
self.percentage = 0
|
|
self.power = 0
|
|
self.time = 0
|
|
self._battery = self._load_battery(**config)
|
|
|
|
@staticmethod
|
|
def _load_battery(**config):
|
|
return load_battery(**config)
|
|
|
|
def timer_setup(self):
|
|
self.update()
|
|
self.timeout_add(self.update_interval, self.timer_setup)
|
|
|
|
def _update_values(self):
|
|
status = self._battery.update_status()
|
|
self.state = status.state
|
|
self.percentage = math.ceil(status.percent * 100)
|
|
self.power = status.power
|
|
self.time = status.time
|
|
|
|
def update(self):
|
|
self._update_values()
|
|
self.draw()
|
|
|
|
def draw(self):
|
|
if self.state == BatteryState.EMPTY:
|
|
self.text_left = ""
|
|
self.text_right = self.text_pattern.format(percentage=0)
|
|
elif self.state == BatteryState.CHARGING:
|
|
self.text_left = ""
|
|
self.text_right = None
|
|
elif self.state == BatteryState.DISCHARGING:
|
|
capacity = ""
|
|
if self.percentage < 80:
|
|
capacity = ""
|
|
if self.percentage < 60:
|
|
capacity = ""
|
|
if self.percentage < 40:
|
|
capacity = ""
|
|
if self.percentage < 20:
|
|
capacity = ""
|
|
self.text_left = capacity
|
|
self.text_right = self.text_pattern.format(percentage=self.percentage)
|
|
elif self.state == BatteryState.FULL:
|
|
self.text_left = ""
|
|
self.text_right = self.text_pattern.format(percentage=100)
|
|
else:
|
|
self.text_left = ""
|
|
self.text_right = self.text_pattern.format(percentage=self.percentage)
|
|
super(BatteryInfoWidget, self).draw()
|
|
|
|
def button_press(self, x, y, button):
|
|
if button == BUTTON_LEFT:
|
|
output = subprocess.check_output(self.status_cmd).decode('utf-8')
|
|
notify(None, "Battery Status", output)
|
|
|
|
|
|
class VolumeInfoWidget(DualPaneTextboxBase):
|
|
"""Displays information about the volume"""
|
|
orientations = base.ORIENTATION_HORIZONTAL
|
|
defaults = [
|
|
('update_interval', 5, 'The update interval in seconds.'),
|
|
('text_pattern', "{percentage}%", 'The pattern for the text that is displayed.'),
|
|
('charging_color', "#ffffff", "Color when battery is charging"),
|
|
('normal_color', "#ffffff", "Color when value is normal"),
|
|
('warning_color', "#ffffff", "Color when value is warning"),
|
|
('critical_color', "#ffffff", "Color when value is critical"),
|
|
("pulse_sink", None, "PulseAudio sink name to control"),
|
|
("status_cmd", "pamixer{sink} --get-volume", "Command to get current volume"),
|
|
("mute_cmd", "pamixer{sink} -t", "Command to mute volume"),
|
|
("up_cmd", "pamixer{sink} -i 2", "Command to turn volume up"),
|
|
("down_cmd", "pamixer{sink} -d 2", "Command to turn volume down"),
|
|
("volume_app", "pavucontrol", "Volume mixer app to open on middle click"),
|
|
]
|
|
|
|
def __init__(self, **config):
|
|
super(VolumeInfoWidget, self).__init__("VOL", "", bar.CALCULATED, **config)
|
|
self.add_defaults(VolumeInfoWidget.defaults)
|
|
self.text = "..."
|
|
self.percentage = 0
|
|
self.volume = 0
|
|
|
|
def get_volume(self):
|
|
try:
|
|
if "{sink}" in self.status_cmd:
|
|
cmd = self.status_cmd.format(sink=" --sink {}".format(self.pulse_sink) if self.pulse_sink else "")
|
|
else:
|
|
cmd = self.status_cmd
|
|
mixer_out = self.call_process(cmd.split(" "))
|
|
except subprocess.CalledProcessError as e:
|
|
logger.error(f"Error while calling {cmd} - {e}")
|
|
return -1
|
|
try:
|
|
return int(mixer_out)
|
|
except ValueError:
|
|
return -1
|
|
|
|
def timer_setup(self):
|
|
self.update()
|
|
self.timeout_add(self.update_interval, self.timer_setup)
|
|
|
|
def _update_values(self):
|
|
self.volume = self.get_volume()
|
|
|
|
def update(self):
|
|
self._update_values()
|
|
self.draw()
|
|
|
|
def draw(self):
|
|
mute = ""
|
|
volume = ""
|
|
if self.volume < 75:
|
|
volume = ""
|
|
if self.volume < 50:
|
|
volume = ""
|
|
if self.volume < 25:
|
|
volume = ""
|
|
if self.volume < 0:
|
|
self.text_left = mute
|
|
self.text_right = ""
|
|
else:
|
|
self.text_left = volume
|
|
self.text_right = self.text_pattern.format(percentage=self.volume)
|
|
super(VolumeInfoWidget, self).draw()
|
|
|
|
def button_press(self, x, y, button):
|
|
if button == BUTTON_LEFT:
|
|
if "{sink}" in self.status_cmd:
|
|
cmd = self.status_cmd.format(sink=" --sink {}".format(self.pulse_sink) if self.pulse_sink else "")
|
|
else:
|
|
cmd = self.status_cmd
|
|
output = subprocess.check_output(cmd.split(" ")).decode('utf-8')
|
|
|
|
sink = "Sink {}\n".format(self.pulse_sink) if self.pulse_sink else ""
|
|
notify(None, "Volume Status", sink+output)
|
|
|
|
elif button == BUTTON_RIGHT:
|
|
if "{sink}" in self.volume_app:
|
|
cmd = self.volume_app.format(sink=" --sink {}".format(self.pulse_sink) if self.pulse_sink else "")
|
|
else:
|
|
cmd = self.volume_app
|
|
subprocess.Popen(cmd.split(" "))
|
|
|
|
elif button == BUTTON_DOWN:
|
|
if "{sink}" in self.down_cmd:
|
|
cmd = self.down_cmd.format(sink=" --sink {}".format(self.pulse_sink) if self.pulse_sink else "")
|
|
else:
|
|
cmd = self.down_cmd
|
|
subprocess.call(cmd.split(" "))
|
|
|
|
elif button == BUTTON_UP:
|
|
if "{sink}" in self.up_cmd:
|
|
cmd = self.up_cmd.format(sink=" --sink {}".format(self.pulse_sink) if self.pulse_sink else "")
|
|
else:
|
|
cmd = self.up_cmd
|
|
subprocess.call(cmd.split(" "))
|
|
|
|
elif button == BUTTON_MUTE:
|
|
if "{sink}" in self.mute_cmd:
|
|
cmd = self.mute_cmd.format(sink=" --sink {}".format(self.pulse_sink) if self.pulse_sink else "")
|
|
else:
|
|
cmd = self.mute_cmd
|
|
subprocess.call(cmd.split(" "))
|
|
|
|
self.update()
|
|
|
|
@expose_command()
|
|
def increase_vol(self):
|
|
# Emulate button press.
|
|
self.button_press(0, 0, BUTTON_UP)
|
|
|
|
@expose_command()
|
|
def decrease_vol(self):
|
|
# Emulate button press.
|
|
self.button_press(0, 0, BUTTON_DOWN)
|
|
|
|
@expose_command()
|
|
def mute(self):
|
|
# Emulate button press.
|
|
self.button_press(0, 0, BUTTON_MUTE)
|
|
|
|
@expose_command()
|
|
def run_app(self):
|
|
# Emulate button press.
|
|
self.button_press(0, 0, BUTTON_RIGHT)
|
|
|
|
|
|
class KuroGroupBox(GroupBox):
|
|
@property
|
|
def length(self):
|
|
try:
|
|
return super(KuroGroupBox, self).length
|
|
except AttributeError:
|
|
return 1
|
|
|
|
@length.setter
|
|
def length(self, length):
|
|
logger.warning(f"Setting groupbox length to {length}")
|