diff --git a/config.py b/config.py
index acf0e36..c3abfcd 100644
--- a/config.py
+++ b/config.py
@@ -53,12 +53,12 @@ except ImportError as e:
try:
- logger.error("Initializing theme...")
+ logger.info("Initializing theme...")
# Initialize the Theme
Theme.initialize()
- logger.error("Initialize done")
+ logger.info("Initialize done")
- logger.error("Hooking theme into callbacks...")
+ logger.info("Hooking theme into callbacks...")
# Hook theme into all hooks we know of
hook.subscribe.startup_once(Theme.callback_startup_once)
hook.subscribe.startup(Theme.callback_startup)
@@ -73,8 +73,6 @@ try:
hook.subscribe.client_new(Theme.callback_client_new)
hook.subscribe.client_managed(Theme.callback_client_managed)
hook.subscribe.client_killed(Theme.callback_client_killed)
- hook.subscribe.client_state_changed(Theme.callback_client_state_changed)
- hook.subscribe.client_type_changed(Theme.callback_client_type_changed)
hook.subscribe.client_focus(Theme.callback_client_focus)
hook.subscribe.client_mouse_enter(Theme.callback_client_mouse_enter)
hook.subscribe.client_name_updated(Theme.callback_client_name_updated)
@@ -85,9 +83,9 @@ try:
hook.subscribe.selection_change(Theme.callback_selection_change)
hook.subscribe.screen_change(Theme.callback_screen_change)
hook.subscribe.current_screen_change(Theme.callback_current_screen_change)
- logger.error("Hooking done")
+ logger.info("Hooking done")
- logger.error("Initializing theme variables")
+ logger.info("Initializing theme variables")
# Initialize variables from theme
keys = Theme.keys
mouse = Theme.mouse
@@ -104,8 +102,9 @@ try:
floating_layout = Theme.floating_layout
auto_fullscreen = Theme.auto_fullscreen
focus_on_window_activation = Theme.focus_on_window_activation
- extentions = Theme.extensions
- logger.error("Variable initialization done")
+ extensions = Theme.extensions
+ wmname = Theme.wmname
+ logger.info("Variable initialization done")
except Exception as e:
Theme = None
Config = None
diff --git a/kuro/config.py b/kuro/config.py
index 83d6f8b..05f2f87 100644
--- a/kuro/config.py
+++ b/kuro/config.py
@@ -19,7 +19,7 @@ class Config(BaseConfig):
app_launcher = "/home/kevin/bin/dmenu_wal.sh"
web_browser = "firefox-developer-edition"
file_manager = "thunar"
- app_chat = "franz"
+ app_chat = "/home/kevin/bin/ramboxpro"
app_irc = "quasselclient"
app_mail = "thunderbird"
cmd_brightness_up = "sudo /usr/bin/xbacklight -inc 10"
@@ -27,6 +27,7 @@ class Config(BaseConfig):
lock_command = "bash /home/kevin/bin/lock.sh"
visualizer_app = "glava"
cmd_screenshot = "xfce4-screenshooter -r -c -d 1"
+ cmd_alt_screenshot = "xfce4-screenshooter -w -c -d 0"
# Commands
wallpaper_config_command = "/home/kevin/bin/wal-nitrogen-noupdate"
@@ -71,7 +72,7 @@ class Config(BaseConfig):
bar_hover_opacity = 1
# Groupbox variables
- font_groupbox = "FontAwesome"
+ font_groupbox = "Font Awesome 5 Pro"
fontsize_groupbox = 15
width_groupbox_border = 1
height_groupbox = 24
@@ -119,10 +120,8 @@ class Config(BaseConfig):
volume_font = "Noto Sans"
volume_fontsize = 11
volume_theme_path = "/home/kevin/.config/qtile/kuro/resources/volume"
- volume_get_command = "pamixer --get-volume".split()
- volume_mute_command = "pamixer -t".split()
- volume_up_command = "pamixer -i 2".split()
- volume_down_command = "pamixer -d 2".split()
+ volume_pulse_sink = "alsa_output.pci-0000_00_1f.3.analog-stereo"
+
volume_is_bluetooth_icon = False
volume_update_interval = 0.2
@@ -143,8 +142,8 @@ class Config(BaseConfig):
updates_colour_available = '#f4d742'
# Screen organization
- laptop_screen_nvidia = "DP-2"
- laptop_screen_intel = "DP-2"
+ laptop_screen_nvidia = "eDP-1-1"
+ laptop_screen_intel = "eDP1"
# Keyboard colors
do_keyboard_updates = False
@@ -155,3 +154,7 @@ class Config(BaseConfig):
# Show thermal widget
show_temperature = True
+
+ # Audio control applications
+ # apps_audio = ["pavucontrol"]
+ apps_audio_afterstart = "/home/kevin/bin/start_jack_audio_chain.sh"
diff --git a/kuro/theme.py b/kuro/theme.py
index c354d0b..ac322df 100644
--- a/kuro/theme.py
+++ b/kuro/theme.py
@@ -71,7 +71,7 @@ class Kuro(BaseTheme):
do_keyboard_updates = True
# Window manager name
- wmname = "QTile"
+ wmname = "qtile"
# Floating layout override
floating_layout = kuro_layouts.KuroFloating(float_rules=[
@@ -180,6 +180,9 @@ class Kuro(BaseTheme):
# Screenshot key
Key([], "Print", lazy.spawn(Config.get('cmd_screenshot', 'xfce4-screenshooter'))),
+ # Alt Screenshot
+ Key([self.mod], "Print", lazy.spawn(Config.get('cmd_alt_screenshot', 'xfce4-screenshooter'))),
+
# Toggle between different layouts as defined below
Key([self.mod], "Tab", lazy.next_layout()),
@@ -239,6 +242,20 @@ class Kuro(BaseTheme):
groups.append(Group(""))
groups.append(Group(""))
groups.append(Group(""))
+ groups.append(Group("", spawn=Config.get('apps_audio', "true"), layout='verticaltile', layouts=[
+ layout.VerticalTile(
+ border_focus=Config.get('colour_border_focus', "#ffffff"),
+ border_normal=Config.get('colour_border_normal', "#777777"),
+ border_width=Config.get('width_border', "1"),
+ margin=Config.get('margin_layout', "0"),
+ )
+ ]))
+ groups.append(Group("", layout='floating', layouts=[
+ layout.Floating(
+ border_focus="#990000",
+ border_normal="#440000"
+ )
+ ]))
return groups
@@ -314,25 +331,15 @@ class Kuro(BaseTheme):
])
if Config.get('show_audio_visualizer', False):
- widgets.append(kuro.utils.widgets.AudioVisualizerWidget(
- graph_color=Config.get('visualizer_graph_color', "#ffffff"),
- fill_color=Config.get('visualizer_fill_color', "#ffffff"),
- border_color=Config.get('visualizer_border_color', "#000000"),
- border_width=Config.get('visualizer_graph_width', 0),
- line_width=Config.get('visualizer_line_width', 0),
- margin_x=0,
- margin_y=0,
- frequency=1
- ))
+ widgets.append(kuro.utils.widgets.AudioVisualizerWidget(margin_x=0, margin_y=0))
widgets.extend([
kuro.utils.widgets.MediaWidget(),
-
- kuro.utils.widgets.SeparatorWidget(),
+ kuro.utils.widgets.TextSpacerWidget(fontsize=14),
])
if Config.get('show_temperature', False):
- widgets.append(
+ widgets.extend([
kuro.utils.widgets.ThermalSensorWidget(
font=Config.get('font_topbar', 'Arial'),
fontsize=Config.get('fontsize_topbar', 16),
@@ -342,99 +349,28 @@ class Kuro(BaseTheme):
chip=Config.get('thermal_chip', None),
threshold=Config.get('thermal_threshold', 70),
update_interval=5,
- )
- )
+ fontsize_left=18, fontsize_right=11
+ ),
+ ])
widgets.extend([
- widget.CPUGraph(
- width=Config.get('cpu_width', 25),
- border_color=Config.get('cpu_border_colour', "#000000"),
- graph_color=Config.get('cpu_graph_colour', "#00ffff"),
- border_width=Config.get('cpu_graph_width', 0),
- line_width=Config.get('cpu_line_width', 1),
- samples=Config.get('cpu_samples', 10),
- frequency=2,
+ kuro.utils.widgets.CPUInfoWidget(fontsize_left=16, fontsize_right=11),
+ kuro.utils.widgets.MemoryInfoWidget(fontsize_left=18, fontsize_right=11),
+ kuro.utils.widgets.DiskIOInfoWidget(fontsize_left=16, fontsize_right=11),
+ kuro.utils.widgets.BatteryInfoWidget(fontsize_left=16, fontsize_right=11),
+ kuro.utils.widgets.VolumeInfoWidget(
+ pulse_sink=Config.get('volume_pulse_sink', None),
+ fontsize_left=18,
+ fontsize_right=11,
),
-
- widget.MemoryGraph(
- width=Config.get('mem_width', 25),
- border_color=Config.get('mem_border_colour', "#000000"),
- graph_color=Config.get('mem_graph_colour', "#00ffff"),
- border_width=Config.get('mem_graph_width', 0),
- line_width=Config.get('mem_line_width', 1),
- samples=Config.get('mem_samples', 10),
- frequency=2,
+ kuro.utils.widgets.TextSpacerWidget(fontsize=14),
+ kuro.utils.widgets.NetworkInfoWidget(fontsize_left=16, fontsize_right=14),
+ kuro.utils.widgets.GPUStatusWidget(
+ theme_path=Config.get('gpu_theme_path', '/home/docs/checkouts/readthedocs.org/user_builds/qtile'
+ '/checkouts/latest/libqtile/resources/battery-icons'),
+ padding=0,
),
-
- widget.HDDBusyGraph(
- width=Config.get('hdd_width', 25),
- border_color=Config.get('hdd_border_colour', "#000000"),
- graph_color=Config.get('hdd_graph_colour', "#00ffff"),
- border_width=Config.get('hdd_border_width', 0),
- line_width=Config.get('hdd_line_width', 1),
- samples=Config.get('hdd_samples', 10),
- frequency=2,
- ),
-
- widget.NetGraph(
- width=Config.get('net_width', 25),
- border_color=Config.get('net_border_colour', "#000000"),
- graph_color=Config.get('net_graph_colour', "#00ffff"),
- border_width=Config.get('net_border_width', 0),
- line_width=Config.get('net_line_width', 1),
- samples=Config.get('net_samples', 10),
- frequency=2,
- ),
-
- kuro.utils.widgets.KuroBatteryIcon(
- battery_name=Config.get('battery_name', 'BAT0'),
- energy_full_file=Config.get('battery_energy_full_file', 'charge_full'),
- energy_now_file=Config.get('battery_energy_now_file', 'charge_now'),
- theme_path=Config.get('battery_theme_path', '/home/docs/checkouts/readthedocs.org/user_builds/qtile'
- '/checkouts/latest/libqtile/resources/battery-icons'),
- update_delay=Config.get('battery_update_delay', 30)
- ),
-
- kuro.utils.widgets.WifiIconWidget(
- interface=Config.get('wifi_interface', 'wlp4s0'),
- theme_path=Config.get('wifi_theme_path', '/home/docs/checkouts/readthedocs.org/user_builds/qtile'
- '/checkouts/latest/libqtile/resources/battery-icons'),
- update_interval=Config.get('wifi_update_interval', 30)
- ),
-
- kuro.utils.widgets.PulseVolumeWidget(
- cardid=Config.get('volume_cardid', None),
- channel=Config.get('volume_channel', 'Master'),
- device=Config.get('volume_device', None),
- font=Config.get('volume_font', 'Arial'),
- fontsize=Config.get('volume_fontsize', 15),
- foreground=Config.get('volume_foreground', '#ffffff'),
- get_volume_command=Config.get('volume_get_command', None),
- mute_command=Config.get('volume_mute_command', None),
- theme_path=Config.get('volume_theme_path', '/home/docs/checkouts/readthedocs.org/user_builds/qtile'
- '/checkouts/latest/libqtile/resources/volume-icons'),
- volume_down_command=Config.get('volume_down_command', None),
- volume_up_command=Config.get('volume_up_command', None),
- is_bluetooth_icon=Config.get('volume_is_bluetooth_icon', False),
- update_interval=Config.get('volume_update_interval', 0.2)
- ),
-
- kuro.utils.widgets.PulseVolumeWidget(
- cardid=Config.get('bluevol_cardid', None),
- channel=Config.get('bluevol_channel', 'Master'),
- device=Config.get('bluevol_device', None),
- font=Config.get('bluevol_font', 'Arial'),
- fontsize=Config.get('bluevol_fontsize', 15),
- foreground=Config.get('bluevol_foreground', '#ffffff'),
- get_volume_command=Config.get('bluevol_get_command', None),
- mute_command=Config.get('bluevol_mute_command', None),
- theme_path=Config.get('bluevol_theme_path', '/home/docs/checkouts/readthedocs.org/user_builds/qtile'
- '/checkouts/latest/libqtile/resources/volume-icons'),
- volume_down_command=Config.get('bluevol_down_command', None),
- volume_up_command=Config.get('bluevol_up_command', None),
- is_bluetooth_icon=Config.get('bluevol_is_bluetooth_icon', False),
- update_interval=Config.get('bluevol_update_interval', 0.2)
- )
+ kuro.utils.widgets.TextSpacerWidget(fontsize=14),
])
# Systray can only be on one screen, so put it on the first
@@ -505,6 +441,8 @@ class Kuro(BaseTheme):
for i, g in enumerate(self.groups):
if i == 9:
i = -1
+ elif i > 9:
+ continue
# mod1 + number = switch to group
self.keys.append(
Key([self.mod], str(i + 1), lazy.group[g.name].toscreen())
@@ -604,11 +542,13 @@ class Kuro(BaseTheme):
utils.notify(title="Window properties {}".format(name),
content="{}".format(pprint.pformat(vars(window))))
+ logger.warning("{}".format(pprint.pformat(vars(window))))
if info:
info = pprint.pformat(info)
utils.notify(title="Window info of {}".format(name),
content="{}".format(info))
+ logger.warning("{}".format(info))
# @staticmethod
def toggle_window_static(self, qtile):
@@ -628,6 +568,10 @@ class Kuro(BaseTheme):
def callback_startup_once(self, *args, **kwargs):
self.update_wallpaper(self.qtile)
+ # Setup audio
+ # p = utils.execute_once(["qjackctl"])
+ # p.wait()
+
def callback_startup(self):
if self.current_wallpaper:
p = utils.execute_once(["wal", "-n", "-i", "{}".format(self.current_wallpaper)])
@@ -692,10 +636,17 @@ class Kuro(BaseTheme):
self.callback_client_new()
+ # After first startup is complete, start the audio apps that can only be started after boot is complete
+ if not self.qtile.no_spawn:
+ for app in Config.get("apps_audio_afterstart", []):
+ utils.execute_once(app)
+
# Update color scheme
Kuro.update_colorscheme(self.qtile)
def callback_client_new(self, *args, **kwargs):
+ client = args[0] if len(args) > 0 else None
+
if len(self.initial_windows) > 0:
init = self.initial_windows.copy()
for pid, gname in init:
@@ -703,6 +654,8 @@ class Kuro(BaseTheme):
if len(group.windows) > 0:
for window in group.windows:
w_pid = window.window.get_net_wm_pid()
+ self.log_info("Comparing pid {} with window PID {}, window {}".format(pid, w_pid,
+ window.name))
if pid == w_pid:
c = self.qtile.dgroups.rules_map.copy()
for rid, r in c.items():
@@ -711,11 +664,10 @@ class Kuro(BaseTheme):
self.initial_windows.remove((pid, gname))
self.log_info("Removed group rule for PID {}, window {}".format(pid,
window.name))
- self.log_info(str(self.qtile.dgroups.rules_map))
+ self.log_info(str(self.qtile.dgroups.rules_map))
# Check if it is a visualizer
if Config.get("show_audio_visualizer", False):
- client = args[0] if len(args) > 0 else None
if client is not None and client.window.get_name() == "GLava":
placed = False
for screen in self.qtile.screens:
@@ -742,6 +694,14 @@ class Kuro(BaseTheme):
logger.warning("Not repositioning GLava {} because there is no widget where it can fit".format(client))
utils.notify("Glava", "Not repisitioning new GLava process because there is no screen without a visualizer")
+ # If this is Non-Mixer, move it to the audio group
+ logger.warning("Processing window {}".format(client))
+ if client is not None and client.window.get_wm_class() == ('Non-Mixer', 'Non-Mixer'):
+ logger.warning("Moving to correct group!")
+ client.window.togroup("")
+
+
+
def callback_client_killed(self, *args, **kwargs):
client = args[0]
logger.warning("Client {} Killed".format(client))
diff --git a/kuro/utils/general.py b/kuro/utils/general.py
index 81b4cb7..da8d96b 100644
--- a/kuro/utils/general.py
+++ b/kuro/utils/general.py
@@ -1,19 +1,25 @@
import re
import subprocess
+import traceback
from time import sleep
import notify2
import six
+from dbus import DBusException
from libqtile import widget
from libqtile.window import Internal
from libqtile.bar import Bar
from notify2 import Notification, URGENCY_NORMAL
+from libqtile.log_utils import logger
notify2.init("QTileWM")
BUTTON_LEFT = 1
BUTTON_MIDDLE = 2
BUTTON_RIGHT = 3
+BUTTON_UP = 4
+BUTTON_DOWN = 5
+BUTTON_MUTE = 1
BUTTON_SCROLL_UP = 4
BUTTON_SCROLL_DOWN = 5
@@ -92,7 +98,11 @@ def notify(title, content, urgency=URGENCY_NORMAL, timeout=5000, image=None):
notification.set_timeout(timeout)
notification.set_urgency(urgency)
- return notification.show()
+ try:
+ return notification.show()
+ except DBusException as e:
+ logger.warning("Showing notification failed: {}".format(e))
+ logger.warning(traceback.format_exc())
def spawn_popup(qtile, x, y, text):
diff --git a/kuro/utils/widgets.py b/kuro/utils/widgets.py
index da629cd..a122a50 100644
--- a/kuro/utils/widgets.py
+++ b/kuro/utils/widgets.py
@@ -1,43 +1,35 @@
+import math
import os
import re
import subprocess
import cairocffi
+import iwlib
+import netifaces
+import psutil
import six
from libqtile import bar, pangocffi
from libqtile.log_utils import logger
-from libqtile.utils import catch_exception_and_warn, UnixCommandNotFound
from libqtile.widget import base
-from libqtile.widget.battery import BatteryIcon, default_icon_path
+from libqtile.widget.base import ORIENTATION_HORIZONTAL
+from libqtile.widget.battery import default_icon_path, load_battery, BatteryState
from libqtile.widget.check_updates import CheckUpdates
from libqtile.widget.currentlayout import CurrentLayoutIcon
from libqtile.widget.graph import _Graph
-from libqtile.widget.image import Image
-from libqtile.widget.sensors import ThermalSensor
from libqtile.widget.tasklist import TaskList
-from libqtile.widget.volume import Volume
from libqtile.widget.wlan import get_status
from libqtile.window import Window
-from kuro.utils.general import BUTTON_LEFT, execute, spawn_popup, notify, BUTTON_MIDDLE, bluetooth_audio_connected, \
- bluetooth_audio_sink, BUTTON_RIGHT
-
-
-class AppLauncherIcon(Image):
- def button_press(self, x, y, button):
- if button == BUTTON_LEFT:
- execute("dmenu_run -i -p '»' -nb '#000000' -fn 'Noto Sans-11' -nf '#777777' -sb '#1793d0' -sf '#ffffff'")
-
- def handle_hover(self, event):
- spawn_popup(self.qtile, self.offsetx, self.offsety, "Hovered over AppLauncherIcon!")
+from kuro.utils.general import notify, BUTTON_LEFT, BUTTON_MIDDLE, BUTTON_RIGHT, BUTTON_DOWN, BUTTON_UP, BUTTON_MUTE, \
+ call_process
class CheckUpdatesYay(CheckUpdates):
def __init__(self, **config):
super(CheckUpdatesYay, self).__init__(**config)
# Override command and output with yay command
- self.cmd = "yay -Qua".split()
- self.status_cmd = "yay -Qua --color never".split()
+ self.cmd = "yay -Qu".split()
+ self.status_cmd = "yay -Qu --color never".split()
self.update_cmd = "sudo yay".split()
self.subtr = 0
@@ -69,296 +61,187 @@ class CheckUpdatesYay(CheckUpdates):
subprocess.Popen(self.execute, shell=True)
-class KuroBatteryIcon(BatteryIcon):
- status_cmd = "acpi"
-
- def button_press(self, x, y, button):
- if button == BUTTON_LEFT:
- output = subprocess.check_output(self.status_cmd).decode('utf-8')
-
- notify(
- "Battery Status",
- output
- )
-
-
-class PulseVolumeWidget(Volume):
-
+class DualPaneTextboxBase(base._Widget):
+ """
+ Base class for widgets that are two boxes next to each other both containing text.
+ """
+ orientations = ORIENTATION_HORIZONTAL
defaults = [
- ("cardid", None, "Card Id"),
- ("device", "default", "Device Name"),
- ("channel", "Master", "Channel"),
- ("padding", 3, "Padding left and right. Calculated if None."),
- ("theme_path", None, "Path of the icons"),
- ("update_interval", 0.2, "Update time in seconds."),
- ("emoji", False, "Use emoji to display volume states, only if ``theme_path`` is not set."
- "The specified font needs to contain the correct unicode characters."),
- ("mute_command", None, "Mute command"),
- ("volume_up_command", None, "Volume up command"),
- ("volume_down_command", None, "Volume down command"),
- ("get_volume_command", None, "Command to get the current volume"),
- ("is_bluetooth_icon", False, "Is this icon for a Bluetooth Audio device?"),
+ ("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
- _old_length = 0
+ 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)
- def __init__(self, **config):
- super(PulseVolumeWidget, self).__init__(**config)
- self._old_length = self._length
+ @property
+ def text_left(self):
+ return self._text_left
- # Augment commands with bluetooth sink ID if this is a bluetooth icon
- if self.is_bluetooth_icon and bluetooth_audio_connected():
- bsink = bluetooth_audio_sink()
- self.mute_command = " ".join(self._user_config['mute_command']).format(bsink=bsink).split()
- self.volume_up_command = " ".join(self._user_config['volume_up_command']).format(bsink=bsink).split()
- self.volume_down_command = " ".join(self._user_config['volume_down_command']).format(bsink=bsink).split()
- self.get_volume_command = " ".join(self._user_config['get_volume_command']).format(bsink=bsink).split()
- logger.info("Updated bluetooth commands with bluetooth sink {}".format(bsink))
- self._length = self._old_length
- self.commands_need_reset = False
- elif self.is_bluetooth_icon:
- self.commands_need_reset = True
+ @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 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:
- self.commands_need_reset = False
+ return self.padding
- self._old_length = self._length
-
- def reset_bluetooth_commands(self):
- if self.is_bluetooth_icon and bluetooth_audio_connected():
- bsink = 0 if bluetooth_audio_sink() == -1 else bluetooth_audio_sink()
- self.mute_command = " ".join(self._user_config['mute_command']).format(bsink=bsink).split()
- self.volume_up_command = " ".join(self._user_config['volume_up_command']).format(bsink=bsink).split()
- self.volume_down_command = " ".join(self._user_config['volume_down_command']).format(bsink=bsink).split()
- self.get_volume_command = " ".join(self._user_config['get_volume_command']).format(bsink=bsink).split()
- logger.info("Updated bluetooth commands with bluetooth sink {}".format(bsink))
- self._length = self._old_length
- self.commands_need_reset = False
-
- def get_volume(self):
- try:
- get_volume_cmd = "echo 0".split()
-
- if self.get_volume_command:
- if self.is_bluetooth_icon and bluetooth_audio_sink() == -1:
- pass
- else:
- get_volume_cmd = self.get_volume_command
-
- mixer_out = self.call_process(get_volume_cmd)
- except subprocess.CalledProcessError:
- return -1
-
- try:
- return int(mixer_out.strip())
- except ValueError:
- return -1
-
- def _update_drawer(self):
- if self.volume is not None:
- super(PulseVolumeWidget, self)._update_drawer()
- self.text = ""
- if self.is_bluetooth_icon and not bluetooth_audio_connected():
- self._length = 0
-
- def draw(self):
- if self.is_bluetooth_icon and not bluetooth_audio_connected():
- if not self.commands_need_reset:
- logger.info("Bluetooth device disconnected. Hiding bluetooth audio mixer")
- self.commands_need_reset = True
- base._TextBox.draw(self)
+ @property
+ def actual_padding_between(self):
+ if self.padding_between is None:
+ return max(self.fontsize_left, self.fontsize_right) / 4
else:
- if self.commands_need_reset:
- self.reset_bluetooth_commands()
- if self.theme_path:
- self.drawer.draw(offsetx=self.offset, width=self.length)
- else:
- base._TextBox.draw(self)
-
- def button_press(self, x, y, button):
- if button == BUTTON_LEFT:
- volume = self.get_volume()
-
- width = 15
-
- if volume >= 0:
- volume_amount = round((volume/100)*width)
- else:
- volume_amount = 0
-
- msg = "[{}{}]".format(
- "".join(["#" for x in range(volume_amount)]),
- "".join(["-" for x in range(width-volume_amount)])
- )
-
- notify(
- "{}Volume : {}%".format("Bluetooth " if self.is_bluetooth_icon else "", volume),
- msg
- )
- else:
- super(PulseVolumeWidget, self).button_press(x, y, button)
-
-
-class WifiIconWidget(base._TextBox):
- """WiFi connection strength indicator widget."""
-
- orientations = base.ORIENTATION_HORIZONTAL
- defaults = [
- ('interface', 'wlan0', 'The interface to monitor'),
- ('update_interval', 1, 'The update interval.'),
- ('theme_path', default_icon_path(), 'Path of the icons'),
- ('custom_icons', {}, 'dict containing key->filename icon map'),
- ('disconnected_message', {'error': False, 'essid': None, 'quality': 0, 'percent': 0}, 'Message to show when WiFi is disconnected'),
- ]
-
- def __init__(self, **config):
- super(WifiIconWidget, self).__init__("WLAN", bar.CALCULATED, **config)
- self.add_defaults(WifiIconWidget.defaults)
-
- if self.theme_path:
- self.length_type = bar.STATIC
- self.length = 0
- self.surfaces = {}
- self.current_icon = 'wireless-disconnected'
- self.icons = dict([(x, '{0}.png'.format(x)) for x in (
- 'wireless-disconnected',
- 'wireless-none',
- 'wireless-low',
- 'wireless-medium',
- 'wireless-high',
- 'wireless-full',
- )])
- self.icons.update(self.custom_icons)
-
- def _get_info(self):
- try:
- essid, quality = get_status(self.interface)
- disconnected = essid is None
- if disconnected:
- return self.disconnected_message
-
- return {
- 'error': False,
- 'essid': essid,
- 'quality': quality,
- 'percent': (quality / 70)
- }
- except EnvironmentError:
- logger.error(
- '%s: Probably your wlan device is switched off or '
- ' otherwise not present in your system.',
- self.__class__.__name__)
-
- return {'error': True}
-
- def timer_setup(self):
- self.update()
- self.timeout_add(self.update_interval, self.timer_setup)
+ return self.padding_between
def _configure(self, qtile, bar):
- super(WifiIconWidget, self)._configure(qtile, bar)
- self.setup_images()
+ 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 _get_icon_key(self):
- key = 'wireless'
- info = self._get_info()
- if info is False or info.get('error'):
- key += '-none'
- elif info.get('essid') is None:
- key += '-disconnected'
- else:
- percent = info['percent']
- if percent < 0.2:
- key += '-low'
- elif percent < 0.4:
- key += '-medium'
- elif percent < 0.8:
- key += '-high'
- else:
- key += '-full'
-
- return key
-
- def update(self):
- icon = self._get_icon_key()
- if icon != self.current_icon:
- self.current_icon = icon
- self.draw()
+ 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 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)
+ # 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
- 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('Wireless 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
-
-
-class ThermalSensorWidget(ThermalSensor):
- defaults = [
- ('metric', True, 'True to use metric/C, False to use imperial/F'),
- ('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'),
- ]
-
- @catch_exception_and_warn(warning=UnixCommandNotFound, excepts=OSError)
- def get_temp_sensors(self):
- """calls the unix `sensors` command with `-f` flag if user has specified that
- the output should be read in Fahrenheit.
+ def cmd_set_font(self, font=base.UNSPECIFIED, fontsize_left=base.UNSPECIFIED, fontsize_right=base.UNSPECIFIED, fontshadow=base.UNSPECIFIED):
"""
- command = ["sensors", ]
- if self.chip:
- command.append(self.chip)
- if not self.metric:
- command.append("-f")
- sensors_out = self.call_process(command)
- return self._format_sensors_output(sensors_out)
+ 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()
-
-class SeparatorWidget(base._TextBox):
- def __init__(self):
- super(SeparatorWidget, self).__init__(text="|", width=bar.CALCULATED, fontsize=14)
+ 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 MediaWidget(base.InLoopPollText):
@@ -381,7 +264,7 @@ class MediaWidget(base.InLoopPollText):
player_icons = {
'spotify': '',
- 'vlc': '',
+ 'vlc': '',
'firefox': '',
'mpv': '',
}
@@ -442,6 +325,11 @@ class MediaWidget(base.InLoopPollText):
if player is not None:
command = ["playerctl", "-p", player, "next"]
_ = self.call_process(command)
+ if button == BUTTON_MIDDLE:
+ # Jump to the screen that the player is on
+ # clients = list(self.bar.qtile.windows_map.values())
+ # logger.warning("{}")
+ pass
def cmd_update_custom_player(self, player_name, data):
# Update firefox player
@@ -559,6 +447,13 @@ class AudioVisualizerWidget(_Graph):
"""Display Audio Visualization graph"""
orientations = base.ORIENTATION_HORIZONTAL
fixed_upper_bound = True
+ defaults = [
+ ("graph_color", "FFFFFF.0", "Graph color"),
+ ("fill_color", "FFFFFF.0", "Fill color for linefill graph"),
+ ("border_color", "FFFFFF.0", "Widget border color"),
+ ("border_width", 0, "Widget border width"),
+ ("line_width", 0, "Line width"),
+ ]
def __init__(self, **config):
_Graph.__init__(self, **config)
@@ -589,11 +484,15 @@ class AudioVisualizerWidget(_Graph):
fullscreen = True
break
- logger.warning("Repositioning {} {} to {}x{}".format(self.client, self.client.window.wid, pos_x, pos_y))
+ logger.debug("Repositioning {} {} to {}x{}".format(self.client, self.client.window.wid, pos_x, pos_y))
self.client.reposition(pos_x, pos_y, above=not fullscreen)
self.draw()
+ def draw(self):
+ self.drawer.clear(self.background or self.bar.background)
+ self.drawer.draw(offsetx=self.offset, width=self.width)
+
class KuroCurrentLayoutIcon(CurrentLayoutIcon):
def _get_layout_names(self):
@@ -687,6 +586,7 @@ class GPUStatusWidget(base._TextBox):
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'),
@@ -784,13 +684,18 @@ class GPUStatusWidget(base._TextBox):
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 IndexError:
+ next_gpu = "Unknown"
+
if self.current_status == "Unknown":
- notify("GPU Status", "The currently used GPU is unknown.",
+ notify("GPU Status", "The currently used GPU is unknown.\n\nAfter the next login it will be the {} GPU.".format(next_gpu),
image=os.path.join(self.theme_path, "gpu-unknown.png"))
else:
- notify("GPU Status", "The system is currently running on the {} GPU.\n"
- "Press the middle mouse button on this icon to switch GPUs.".format(
- self.current_status
+ notify("GPU Status", "The system is currently running on the {} GPU. Press the middle mouse "
+ "button on this icon to switch GPUs.\n\nAfter the next login it will be the {} GPU.".format(
+ self.current_status, next_gpu
),
image=os.path.join(self.theme_path, "gpu-{}.png".format(self.current_status.lower()))
)
@@ -810,3 +715,582 @@ class GPUStatusWidget(base._TextBox):
notify("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 TextSpacerWidget(base._TextBox):
+ """Displays a text separator"""
+ orientations = base.ORIENTATION_HORIZONTAL
+ defaults = [
+ ('spacer', None, 'The character/text to use as separator. Default "|" if None.'),
+ ('color', "#ffffff", "Color of the text."),
+ ]
+
+ def __init__(self, **config):
+ super(TextSpacerWidget, self).__init__("Separator", bar.CALCULATED, **config)
+ self.add_defaults(TextSpacerWidget.defaults)
+ self.text = self.spacer or "|"
+
+ def draw(self):
+ base._TextBox.draw(self)
+
+
+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):
+ sensors_out = self.call_process(self.get_command())
+ 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("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("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("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("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 / 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']
+ eth_status = call_process(["ip", "link", "show", "{}".format(self.wired_interface)])
+ 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 = "Wired {}" \
+ "{}{}{}".format(self.wired_interface, wired_ipv4, wired_ipv6, wired_mac)
+ else:
+ wired_text = "Wired: Not connected"
+
+ if wifi_text:
+ notify(title, "{}\n\n{}".format(wifi_text, wired_text))
+ else:
+ notify(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("Battery Status", output)
+
+
+class VolumeInfoWidget(DualPaneTextboxBase):
+ """Displays information about the volume"""
+ orientations = base.ORIENTATION_HORIZONTAL
+ 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"),
+ ("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:
+ 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("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()
+
+ def cmd_increase_vol(self):
+ # Emulate button press.
+ self.button_press(0, 0, BUTTON_UP)
+
+ def cmd_decrease_vol(self):
+ # Emulate button press.
+ self.button_press(0, 0, BUTTON_DOWN)
+
+ def cmd_mute(self):
+ # Emulate button press.
+ self.button_press(0, 0, BUTTON_MUTE)
+
+ def cmd_run_app(self):
+ # Emulate button press.
+ self.button_press(0, 0, BUTTON_RIGHT)
diff --git a/required_packages.txt b/required_packages.txt
index 9a1cfa4..8b3391e 100644
--- a/required_packages.txt
+++ b/required_packages.txt
@@ -2,6 +2,7 @@ pamixer
nitrogen
notification-daemon
otf-font-awesome
+python-osc
# /optional/
playerctl