1339 lines
49 KiB
Python
1339 lines
49 KiB
Python
import math
|
|
import os
|
|
import re
|
|
import subprocess
|
|
|
|
import cairocffi
|
|
import iwlib
|
|
import netifaces
|
|
import psutil
|
|
import six
|
|
import unicodedata
|
|
from libqtile import bar, pangocffi
|
|
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.check_updates import CheckUpdates
|
|
from libqtile.widget.currentlayout import CurrentLayoutIcon
|
|
from libqtile.widget.graph import _Graph
|
|
from libqtile.widget.tasklist import TaskList
|
|
from libqtile.widget.wlan import get_status
|
|
from libqtile.backend.x11.window import Window
|
|
|
|
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 -Qu".split()
|
|
self.status_cmd = "yay -Qu --color never".split()
|
|
self.update_cmd = "sudo yay".split()
|
|
self.subtr = 0
|
|
|
|
def _check_updates(self):
|
|
#subprocess.check_output(self.update_cmd)
|
|
res = super(CheckUpdatesYay, self)._check_updates()
|
|
return res
|
|
|
|
def button_press(self, x, y, button):
|
|
if button == BUTTON_LEFT:
|
|
output = subprocess.check_output(self.status_cmd).decode('utf-8').split('\n')
|
|
|
|
num_updates = len(output)-1
|
|
msg = "{} updates available.".format(num_updates)
|
|
|
|
if num_updates > 0:
|
|
msg += "\n\n"
|
|
for x in range(min(num_updates, 9)):
|
|
msg += output[x] + "\n"
|
|
if num_updates > 9:
|
|
msg += "and {} more...".format(num_updates-9)
|
|
|
|
notify(
|
|
"System updates",
|
|
msg
|
|
)
|
|
|
|
elif button == BUTTON_MIDDLE and self.execute is not None:
|
|
subprocess.Popen(self.execute, shell=True)
|
|
|
|
|
|
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
|
|
|
|
def cmd_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 MediaWidget(base.InLoopPollText):
|
|
"""Media Status Widget"""
|
|
|
|
class Status:
|
|
OFFLINE = 0
|
|
PLAYING = 1
|
|
PAUSED = 2
|
|
STOPPED = 3
|
|
|
|
orientations = base.ORIENTATION_HORIZONTAL
|
|
defaults = [
|
|
('off_text', '', 'The pattern for the text if no players are found.'),
|
|
('on_text_play', ' {}', 'The pattern for the text if music is playing.'),
|
|
('on_text_pause', ' {}', 'The pattern for the text if music is paused.'),
|
|
('on_text_stop', ' {}', 'The pattern for the text if music is stopped.'),
|
|
('update_interval', 1, 'The update interval.'),
|
|
('max_chars_per_player', 50, 'Maximum characters of text per player.'),
|
|
('ignore_players', '', 'Comma-separated list of players to ignore.')
|
|
]
|
|
|
|
player_icons = {
|
|
'spotify': '',
|
|
'vlc': '',
|
|
'firefox': '',
|
|
'mpv': '',
|
|
}
|
|
|
|
custom_player_data = {
|
|
'firefox': {
|
|
'showing': False,
|
|
'title': '',
|
|
'state': Status.STOPPED,
|
|
}
|
|
}
|
|
|
|
image_urls = {}
|
|
current_image_url = None
|
|
player_to_control = None
|
|
|
|
def __init__(self, **config):
|
|
super(MediaWidget, self).__init__(**config)
|
|
self.add_defaults(MediaWidget.defaults)
|
|
self.surfaces = {}
|
|
self.player_to_control = None
|
|
|
|
def _player_to_control(self):
|
|
info = self._get_info()
|
|
players = {}
|
|
for player in info.keys():
|
|
if player not in self.custom_player_data.keys():
|
|
if info[player][0] in [MediaWidget.Status.PLAYING, MediaWidget.Status.PAUSED]:
|
|
players[player] = info[player]
|
|
|
|
if self.player_to_control is not None and self.player_to_control not in players.keys():
|
|
self.player_to_control = None
|
|
|
|
if self.player_to_control is not None:
|
|
players = {self.player_to_control: players[self.player_to_control]}
|
|
|
|
if len(players.keys()) == 1:
|
|
player = list(players.keys())[0]
|
|
self.player_to_control = player
|
|
return player
|
|
|
|
elif len(players) == 0:
|
|
notify("MediaWidget", "Nothing to control!")
|
|
else:
|
|
notify("MediaWidget", "Multiple players to control, I don't know what you want to do!")
|
|
|
|
return None
|
|
|
|
def button_press(self, x, y, button):
|
|
if button == BUTTON_LEFT:
|
|
player = self._player_to_control()
|
|
if player is not None:
|
|
command = ["playerctl", "-i", self.ignore_players, "-p", player, "play-pause"]
|
|
_ = self.call_process(command)
|
|
notify("MediaWidget", "Toggled {}".format(player))
|
|
if button == BUTTON_RIGHT:
|
|
player = self._player_to_control()
|
|
if player is not None:
|
|
command = ["playerctl", "-i", self.ignore_players, "-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
|
|
if player_name.startswith("firefox"):
|
|
if data['playing'] and data['muted']:
|
|
self.custom_player_data['firefox']['showing'] = True
|
|
self.custom_player_data['firefox']['state'] = MediaWidget.Status.PAUSED
|
|
self.custom_player_data['firefox']['title'] = data['title'][:50]
|
|
elif data['playing'] and not data['muted']:
|
|
self.custom_player_data['firefox']['showing'] = True
|
|
self.custom_player_data['firefox']['state'] = MediaWidget.Status.PLAYING
|
|
self.custom_player_data['firefox']['title'] = data['title'][:50]
|
|
elif not data['playing'] and data['muted']:
|
|
self.custom_player_data['firefox']['showing'] = True
|
|
self.custom_player_data['firefox']['state'] = MediaWidget.Status.STOPPED
|
|
self.custom_player_data['firefox']['title'] = data['title'][:50]
|
|
elif not data['playing'] and not data['muted']:
|
|
self.custom_player_data['firefox']['showing'] = False
|
|
self.custom_player_data['firefox']['state'] = MediaWidget.Status.OFFLINE
|
|
self.custom_player_data['firefox']['title'] = data['title'][:50]
|
|
|
|
def _get_players(self):
|
|
players = []
|
|
|
|
# Playerctl players
|
|
try:
|
|
result = self.call_process(["playerctl", "-i", self.ignore_players, "-l"])
|
|
except subprocess.CalledProcessError:
|
|
result = None
|
|
|
|
if result:
|
|
players.extend([x for x in result.split("\n") if x])
|
|
|
|
# Custom players - Firefox
|
|
if self.custom_player_data['firefox']['showing']:
|
|
players.append('firefox')
|
|
|
|
if players:
|
|
return players
|
|
else:
|
|
return None
|
|
|
|
def _get_info(self):
|
|
players = self._get_players()
|
|
|
|
if not players:
|
|
return {}
|
|
else:
|
|
result = {}
|
|
|
|
for player in players:
|
|
if player in self.custom_player_data.keys():
|
|
# Custom player -- Firefox
|
|
if player == "firefox":
|
|
result[player] = [self.custom_player_data['firefox']['state'], self.custom_player_data['firefox']['title']]
|
|
|
|
# Other custom players -- generic attempt with error catching
|
|
else:
|
|
try:
|
|
result[player] = [self.custom_player_data[player]['state'],
|
|
self.custom_player_data[player]['title']]
|
|
except KeyError:
|
|
pass
|
|
|
|
else:
|
|
# PlayerCtl player
|
|
command = ["playerctl", "-i", self.ignore_players, "-p", player, "status"]
|
|
cmd_result = self.call_process(command).strip()
|
|
|
|
text = "Unknown"
|
|
if cmd_result in ["Playing", "Paused"]:
|
|
try:
|
|
artist = self.call_process(['playerctl', "-i", self.ignore_players, '-p', player, 'metadata', 'artist']).strip()
|
|
except subprocess.CalledProcessError:
|
|
artist = None
|
|
try:
|
|
title = self.call_process(['playerctl', "-i", self.ignore_players, '-p', player, 'metadata', 'title']).strip()
|
|
except subprocess.CalledProcessError:
|
|
title = None
|
|
|
|
if artist and title:
|
|
text = "{} - {}".format(artist, title)
|
|
elif artist:
|
|
text = artist
|
|
elif title:
|
|
text = title
|
|
|
|
if cmd_result == "Playing":
|
|
result[player] = [MediaWidget.Status.PLAYING, text]
|
|
elif cmd_result == "Paused":
|
|
result[player] = [MediaWidget.Status.PAUSED, text]
|
|
elif cmd_result == "Stopped":
|
|
result[player] = [MediaWidget.Status.STOPPED, ""]
|
|
|
|
return result
|
|
|
|
def _get_formatted_text(self, status):
|
|
if status[0] == MediaWidget.Status.PLAYING:
|
|
res = self.on_text_play.format(status[1])
|
|
elif status[0] == MediaWidget.Status.PAUSED:
|
|
res = self.on_text_pause.format(status[1])
|
|
elif status[0] == MediaWidget.Status.STOPPED:
|
|
res = self.on_text_stop.format(status[1])
|
|
else:
|
|
res = "Unknown"
|
|
res = pangocffi.markup_escape_text(res)
|
|
res = unicodedata.normalize('NFKD', res)
|
|
if len(res) > self.max_chars_per_player:
|
|
res = res[:self.max_chars_per_player] + "..."
|
|
return res
|
|
|
|
def draw(self):
|
|
super(MediaWidget, self).draw()
|
|
|
|
def poll(self):
|
|
text = []
|
|
status = self._get_info()
|
|
if not status:
|
|
return self.off_text
|
|
else:
|
|
for player in status.keys():
|
|
# Shorten firefox.instance[0-9]+ to just firefox for icon finding
|
|
if player.startswith("firefox"):
|
|
player_icon = "firefox"
|
|
else:
|
|
player_icon = player
|
|
icon = self.player_icons.get(player_icon, player_icon)
|
|
text.append("{} {}".format(icon, self._get_formatted_text(status[player])))
|
|
|
|
return " | ".join(text) if text else self.off_text
|
|
|
|
|
|
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)
|
|
self.add_defaults(AudioVisualizerWidget.defaults)
|
|
|
|
self.client = None
|
|
self.screen = None
|
|
|
|
self.old_position = None
|
|
|
|
def set_client(self, c, s):
|
|
self.client = c
|
|
self.screen = s
|
|
|
|
def update_graph(self):
|
|
if self.client is not None:
|
|
viz_info = self.info()
|
|
pos_x = viz_info['offset'] + self.margin_x + self.screen.x
|
|
pos_y = 0 + self.margin_y + self.screen.y
|
|
if self.old_position != (pos_x, pos_y):
|
|
self.old_position = (pos_x, pos_y)
|
|
|
|
# Check if a window on this screen is full-screen
|
|
fullscreen = False
|
|
for window in self.screen.group.windows:
|
|
if isinstance(window, Window):
|
|
if window.fullscreen:
|
|
fullscreen = True
|
|
break
|
|
|
|
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):
|
|
names = super(KuroCurrentLayoutIcon, self)._get_layout_names()
|
|
|
|
from kuro.utils import layouts as kuro_layouts
|
|
from libqtile.layout.base import Layout
|
|
klayouts = [
|
|
layout_class_name.lower()
|
|
for layout_class, layout_class_name
|
|
in map(lambda x: (getattr(kuro_layouts, x), x), dir(kuro_layouts))
|
|
if isinstance(layout_class, six.class_types) and issubclass(layout_class, Layout)
|
|
]
|
|
names.extend(klayouts)
|
|
|
|
return list(set(names))
|
|
|
|
|
|
class KuroTaskList(TaskList):
|
|
defaults = [
|
|
(
|
|
'txt_pinned',
|
|
'P ',
|
|
'Text representation of the pinned window state. '
|
|
'e.g., "P " or "\U0001F5D7 "'
|
|
),
|
|
(
|
|
'markup_pinned',
|
|
None,
|
|
'Text markup of the pinned window state. Supports pangomarkup with markup=True.'
|
|
'e.g., "{}" or "<span underline="low">{}</span>"'
|
|
),
|
|
]
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
super(KuroTaskList, self).__init__(*args, **kwargs)
|
|
self.add_defaults(KuroTaskList.defaults)
|
|
|
|
def get_taskname(self, window):
|
|
"""
|
|
Get display name for given window.
|
|
Depending on its state minimized, maximized and floating
|
|
appropriate characters are prepended.
|
|
"""
|
|
state = ''
|
|
markup_str = self.markup_normal
|
|
|
|
# Enforce markup and new string format behaviour when
|
|
# at least one markup_* option is used.
|
|
# Mixing non markup and markup may cause problems.
|
|
if self.markup_minimized or self.markup_maximized\
|
|
or self.markup_floating or self.markup_focused or self.markup_pinned:
|
|
enforce_markup = True
|
|
else:
|
|
enforce_markup = False
|
|
|
|
if window is None:
|
|
pass
|
|
elif hasattr(window, "is_static_window") and window.is_static_window:
|
|
state = self.txt_pinned
|
|
markup_str = self.markup_pinned
|
|
elif window.minimized:
|
|
state = self.txt_minimized
|
|
markup_str = self.markup_minimized
|
|
elif window.maximized:
|
|
state = self.txt_maximized
|
|
markup_str = self.markup_maximized
|
|
elif window.floating:
|
|
state = self.txt_floating
|
|
markup_str = self.markup_floating
|
|
elif window is window.group.current_window:
|
|
markup_str = self.markup_focused
|
|
|
|
window_name = window.name if window and window.name else "?"
|
|
|
|
# Emulate default widget behavior if markup_str is None
|
|
if enforce_markup and markup_str is None:
|
|
markup_str = "%s{}" % (state)
|
|
|
|
if markup_str is not None:
|
|
self.markup = True
|
|
window_name = pangocffi.markup_escape_text(window_name)
|
|
return markup_str.format(window_name)
|
|
|
|
return "%s%s" % (state, window_name)
|
|
|
|
|
|
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):
|
|
output = self.call_process(self.check_command, shell=True)
|
|
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 IndexError:
|
|
next_gpu = "Unknown"
|
|
|
|
if self.current_status == "Unknown":
|
|
notify("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("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"]
|
|
output = self.call_process(command)
|
|
if "nvidia" in output:
|
|
notify("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("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("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 = "<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(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', 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:
|
|
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)
|