Merge remote-tracking branch 'upstream/master'
This commit is contained in:
commit
ab25a1b6eb
@ -122,3 +122,23 @@ Also change your i3wm config to the following:
|
||||
position top
|
||||
workspace_buttons yes
|
||||
}
|
||||
|
||||
Settings that require credentials can utilize the keyring module to keep sensitive information out of config files.
|
||||
To take advantage of this feature, simply use the setting_util.py script to set the credentials for a module. Once this
|
||||
is done you can add the module to your config without specifying the credentials, eg:
|
||||
|
||||
::
|
||||
|
||||
# Use the default keyring to retrieve credentials. To determine which backend is the default on your system, run
|
||||
# python -c 'import keyring; print(keyring.get_keyring())'
|
||||
status.register('github')
|
||||
|
||||
If you don't want to use the default you can set a specific keyring like so:
|
||||
|
||||
::
|
||||
|
||||
from keyring.backends.file import PlaintextKeyring
|
||||
status.register('github', keyring_backend=PlaintextKeyring())
|
||||
|
||||
|
||||
i3pystatus will locate and set the credentials during the module loading process. Currently supported credentals are "password", "email" and "username".
|
@ -14,6 +14,9 @@ tools for this which make this even easier:
|
||||
periodically.
|
||||
- Settings (already built into above classes) allow you to easily
|
||||
specify user-modifiable attributes of your class for configuration.
|
||||
- For modules that require credentials, it is recommended to add a
|
||||
keyring_backend setting to allow users to specify their own backends
|
||||
for retrieving sensitive credentials.
|
||||
|
||||
Required settings and default values are also handled.
|
||||
|
||||
|
@ -32,13 +32,24 @@ class Clock(IntervalModule):
|
||||
on_downscroll = ["scroll_format", -1]
|
||||
|
||||
def init(self):
|
||||
lang, enc = os.environ.get('LANG', None).split('.', 1)
|
||||
if lang != locale.getlocale(locale.LC_TIME)[0]:
|
||||
env_lang = os.environ.get('LC_TIME', None)
|
||||
if env_lang is None:
|
||||
env_lang = os.environ.get('LANG', None)
|
||||
|
||||
if env_lang is not None:
|
||||
if env_lang.find('.') != -1:
|
||||
lang = tuple(env_lang.split('.', 1))
|
||||
else:
|
||||
lang = (env_lang, None)
|
||||
else:
|
||||
lang = (None, None)
|
||||
|
||||
if lang != locale.getlocale(locale.LC_TIME):
|
||||
# affects datetime.time.strftime() in whole program
|
||||
locale.setlocale(locale.LC_TIME, (lang, enc))
|
||||
locale.setlocale(locale.LC_TIME, lang)
|
||||
|
||||
if self.format is None:
|
||||
if lang == 'en_US':
|
||||
if lang[0] == 'en_US':
|
||||
# MDY format - United States of America
|
||||
self.format = ["%a %b %-d %X"]
|
||||
else:
|
||||
|
@ -2,10 +2,10 @@ from i3pystatus.core.util import KeyConstraintDict
|
||||
from i3pystatus.core.exceptions import ConfigKeyError, ConfigMissingError
|
||||
import inspect
|
||||
import logging
|
||||
import getpass
|
||||
|
||||
|
||||
class SettingsBase:
|
||||
|
||||
"""
|
||||
Support class for providing a nice and flexible settings interface
|
||||
|
||||
@ -18,6 +18,8 @@ class SettingsBase:
|
||||
Settings are stored as attributes of self.
|
||||
"""
|
||||
|
||||
__PROTECTED_SETTINGS = ["password", "email", "username"]
|
||||
|
||||
settings = (
|
||||
("log_level", "Set to true to log error to .i3pystatus-<pid> file"),
|
||||
)
|
||||
@ -44,21 +46,25 @@ class SettingsBase:
|
||||
return kwargs
|
||||
|
||||
def merge_with_parents_settings():
|
||||
|
||||
settings = tuple()
|
||||
|
||||
# getmro returns base classes according to Method Resolution Order
|
||||
for cls in inspect.getmro(self.__class__):
|
||||
if hasattr(cls, "settings"):
|
||||
settings = settings + cls.settings
|
||||
return settings
|
||||
|
||||
self.__name__ = "{}.{}".format(
|
||||
self.__module__, self.__class__.__name__)
|
||||
|
||||
settings = merge_with_parents_settings()
|
||||
settings = self.flatten_settings(settings)
|
||||
|
||||
sm = KeyConstraintDict(settings, self.required)
|
||||
settings_source = get_argument_dict(args, kwargs)
|
||||
|
||||
protected = self.get_protected_settings(settings_source)
|
||||
settings_source.update(protected)
|
||||
|
||||
try:
|
||||
sm.update(settings_source)
|
||||
except KeyError as exc:
|
||||
@ -70,13 +76,48 @@ class SettingsBase:
|
||||
raise ConfigMissingError(
|
||||
type(self).__name__, missing=exc.keys) from exc
|
||||
|
||||
self.__name__ = "{}.{}".format(
|
||||
self.__module__, self.__class__.__name__)
|
||||
|
||||
self.logger = logging.getLogger(self.__name__)
|
||||
self.logger.setLevel(self.log_level)
|
||||
self.init()
|
||||
|
||||
def get_protected_settings(self, settings_source):
|
||||
"""
|
||||
Attempt to retrieve protected settings from keyring if they are not already set.
|
||||
"""
|
||||
user_backend = settings_source.get('keyring_backend')
|
||||
found_settings = dict()
|
||||
for setting_name in self.__PROTECTED_SETTINGS:
|
||||
# Nothing to do if the setting is already defined.
|
||||
if settings_source.get(setting_name):
|
||||
continue
|
||||
|
||||
setting = None
|
||||
identifier = "%s.%s" % (self.__name__, setting_name)
|
||||
if hasattr(self, 'required') and setting_name in getattr(self, 'required'):
|
||||
setting = self.get_setting_from_keyring(identifier, user_backend)
|
||||
elif hasattr(self, setting_name):
|
||||
setting = self.get_setting_from_keyring(identifier, user_backend)
|
||||
if setting:
|
||||
found_settings.update({setting_name: setting})
|
||||
return found_settings
|
||||
|
||||
def get_setting_from_keyring(self, setting_identifier, keyring_backend=None):
|
||||
"""
|
||||
Retrieves a protected setting from keyring
|
||||
:param setting_identifier: must be in the format package.module.Class.setting
|
||||
"""
|
||||
# If a custom keyring backend has been defined, use it.
|
||||
if keyring_backend:
|
||||
return keyring_backend.get_password(setting_identifier, getpass.getuser())
|
||||
|
||||
# Otherwise try and use default keyring.
|
||||
try:
|
||||
import keyring
|
||||
except ImportError:
|
||||
pass
|
||||
else:
|
||||
return keyring.get_password(setting_identifier, getpass.getuser())
|
||||
|
||||
def init(self):
|
||||
"""Convenience method which is called after all settings are set
|
||||
|
||||
|
@ -383,7 +383,7 @@ def make_graph(values, lower_limit=0.0, upper_limit=100.0, style="blocks"):
|
||||
extent = mx - mn
|
||||
|
||||
if style == 'blocks':
|
||||
bar = u'_▁▂▃▄▅▆▇█'
|
||||
bar = '_▁▂▃▄▅▆▇█'
|
||||
bar_count = len(bar) - 1
|
||||
if extent == 0:
|
||||
graph = '_' * len(values)
|
||||
@ -436,7 +436,7 @@ def make_vertical_bar(percentage, width=1):
|
||||
:param width: How many characters wide the bar should be.
|
||||
:returns: Bar as a String
|
||||
"""
|
||||
bar = u' _▁▂▃▄▅▆▇█'
|
||||
bar = ' _▁▂▃▄▅▆▇█'
|
||||
percentage //= 10
|
||||
if percentage < 0:
|
||||
output = bar[0]
|
||||
|
@ -29,6 +29,7 @@ class CpuUsage(IntervalModule):
|
||||
format = "{usage:02}%"
|
||||
format_all = "{core}:{usage:02}%"
|
||||
exclude_average = False
|
||||
interval = 1
|
||||
settings = (
|
||||
("format", "format string."),
|
||||
("format_all", ("format string used for {usage_all} per core. "
|
||||
@ -40,7 +41,6 @@ class CpuUsage(IntervalModule):
|
||||
def init(self):
|
||||
self.prev_total = defaultdict(int)
|
||||
self.prev_busy = defaultdict(int)
|
||||
self.interval = 1
|
||||
self.formatter = Formatter()
|
||||
|
||||
def get_cpu_timings(self):
|
||||
|
@ -38,6 +38,8 @@ class Disk(IntervalModule):
|
||||
self.output = {}
|
||||
return
|
||||
|
||||
critical = available < self.critical_limit
|
||||
|
||||
cdict = {
|
||||
"total": (stat.f_bsize * stat.f_blocks) / self.divisor,
|
||||
"free": (stat.f_bsize * stat.f_bfree) / self.divisor,
|
||||
@ -51,6 +53,6 @@ class Disk(IntervalModule):
|
||||
|
||||
self.output = {
|
||||
"full_text": self.format.format(**cdict),
|
||||
"color": self.color if available > self.critical_limit else self.critical_color,
|
||||
"urgent": available > self.critical_limit
|
||||
"color": self.critical_color if critical else self.color,
|
||||
"urgent": critical
|
||||
}
|
||||
|
@ -16,18 +16,20 @@ class Github(IntervalModule):
|
||||
* `{unread_count}` - number of unread notifications, empty if 0
|
||||
"""
|
||||
|
||||
unread_marker = u"●"
|
||||
unread_marker = "●"
|
||||
unread = ''
|
||||
color = '#78EAF2'
|
||||
username = ''
|
||||
password = ''
|
||||
format = '{unread}'
|
||||
interval = 600
|
||||
keyring_backend = None
|
||||
|
||||
on_leftclick = 'open_github'
|
||||
|
||||
settings = (
|
||||
('format', 'format string'),
|
||||
('keyring_backend', 'alternative keyring backend for retrieving credentials'),
|
||||
('unread_marker', 'sets the string that the "unread" formatter shows when there are pending notifications'),
|
||||
("username", ""),
|
||||
("password", ""),
|
||||
|
@ -16,10 +16,12 @@ class IMAP(Backend):
|
||||
settings = (
|
||||
"host", "port",
|
||||
"username", "password",
|
||||
('keyring_backend', 'alternative keyring backend for retrieving credentials'),
|
||||
"ssl",
|
||||
"mailbox",
|
||||
)
|
||||
required = ("host", "username", "password")
|
||||
keyring_backend = None
|
||||
|
||||
port = 993
|
||||
ssl = True
|
||||
|
@ -20,10 +20,12 @@ class ModsDeChecker(IntervalModule):
|
||||
settings = (
|
||||
("format",
|
||||
"""Use {unread} as the formatter for number of unread posts"""),
|
||||
('keyring_backend', 'alternative keyring backend for retrieving credentials'),
|
||||
("offset", """subtract number of posts before output"""),
|
||||
"color", "username", "password"
|
||||
)
|
||||
required = ("username", "password")
|
||||
keyring_backend = None
|
||||
|
||||
color = "#7181fe"
|
||||
offset = 0
|
||||
|
@ -1,5 +1,6 @@
|
||||
import socket
|
||||
from os.path import basename
|
||||
from math import floor
|
||||
|
||||
from i3pystatus import IntervalModule, formatp
|
||||
from i3pystatus.core.util import TimeWrapper
|
||||
@ -34,8 +35,10 @@ class MPD(IntervalModule):
|
||||
("format", "formatp string"),
|
||||
("status", "Dictionary mapping pause, play and stop to output"),
|
||||
("color", "The color of the text"),
|
||||
("text_len", "Defines max length for title, album and artist, if truncated ellipsis are appended as indicator"),
|
||||
("truncate_fields", "fileds that will be truncated if exceeding text_len"),
|
||||
("max_field_len", "Defines max length for in truncate_fields defined fields, if truncated, ellipsis are appended as indicator. It's applied *before* max_len. Value of 0 disables this."),
|
||||
("max_len", "Defines max length for the hole string, if exceeding fields specefied in truncate_fields are truncated equaly. If truncated, ellipsis are appended as indicator. It's applied *after* max_field_len. Value of 0 disables this."),
|
||||
("truncate_fields", "fields that will be truncated if exceeding max_field_len or max_len."),
|
||||
|
||||
)
|
||||
|
||||
host = "localhost"
|
||||
@ -48,7 +51,8 @@ class MPD(IntervalModule):
|
||||
"stop": "◾",
|
||||
}
|
||||
color = "#FFFFFF"
|
||||
text_len = 25
|
||||
max_field_len = 25
|
||||
max_len = 100
|
||||
truncate_fields = ("title", "album", "artist")
|
||||
on_leftclick = "switch_playpause"
|
||||
on_rightclick = "next_song"
|
||||
@ -91,17 +95,30 @@ class MPD(IntervalModule):
|
||||
|
||||
}
|
||||
|
||||
for key in self.truncate_fields:
|
||||
if len(fdict[key]) > self.text_len:
|
||||
fdict[key] = fdict[key][:self.text_len - 1] + "…"
|
||||
|
||||
if not fdict["title"] and "filename" in fdict:
|
||||
fdict["filename"] = '.'.join(
|
||||
basename(currentsong["file"]).split('.')[:-1])
|
||||
else:
|
||||
fdict["filename"] = ""
|
||||
|
||||
if self.max_field_len > 0:
|
||||
for key in self.truncate_fields:
|
||||
if len(fdict[key]) > self.max_field_len:
|
||||
fdict[key] = fdict[key][:self.max_field_len - 1] + "…"
|
||||
|
||||
full_text = formatp(self.format, **fdict).strip()
|
||||
full_text_len = len(full_text)
|
||||
if full_text_len > self.max_len and self.max_len > 0:
|
||||
shrink = floor((self.max_len - full_text_len) /
|
||||
len(self.truncate_fields)) - 1
|
||||
|
||||
for key in self.truncate_fields:
|
||||
fdict[key] = fdict[key][:shrink] + "…"
|
||||
|
||||
full_text = formatp(self.format, **fdict).strip()
|
||||
|
||||
self.output = {
|
||||
"full_text": formatp(self.format, **fdict).strip(),
|
||||
"full_text": full_text,
|
||||
"color": self.color,
|
||||
}
|
||||
|
||||
|
@ -338,7 +338,6 @@ class Network(IntervalModule, ColorRangeModule):
|
||||
format_values = dict(kbs="", network_graph="", bytes_sent="", bytes_recv="", packets_sent="", packets_recv="",
|
||||
interface="", v4="", v4mask="", v4cidr="", v6="", v6mask="", v6cidr="", mac="",
|
||||
essid="", freq="", quality="", quality_bar="")
|
||||
color = None
|
||||
if self.network_traffic:
|
||||
network_usage = self.network_traffic.get_usage(self.interface)
|
||||
format_values.update(network_usage)
|
||||
@ -352,22 +351,22 @@ class Network(IntervalModule, ColorRangeModule):
|
||||
format_values['network_graph'] = self.get_network_graph(kbs)
|
||||
format_values['kbs'] = "{0:.1f}".format(round(kbs, 2)).rjust(6)
|
||||
color = self.get_gradient(kbs, self.colors, self.upper_limit)
|
||||
else:
|
||||
color = None
|
||||
|
||||
if sysfs_interface_up(self.interface, self.unknown_up):
|
||||
if not color:
|
||||
color = self.color_up
|
||||
format_str = self.format_up
|
||||
else:
|
||||
color = self.color_down
|
||||
format_str = self.format_down
|
||||
|
||||
network_info = self.network_info.get_info(self.interface)
|
||||
format_values.update(network_info)
|
||||
|
||||
format_values['interface'] = self.interface
|
||||
if sysfs_interface_up(self.interface, self.unknown_up):
|
||||
if not self.dynamic_color:
|
||||
color = self.color_up
|
||||
|
||||
self.output = {
|
||||
"full_text": self.format_up.format(**format_values),
|
||||
'color': color,
|
||||
}
|
||||
else:
|
||||
color = self.color_down
|
||||
self.output = {
|
||||
"full_text": self.format_down.format(**format_values),
|
||||
'color': color,
|
||||
}
|
||||
self.output = {
|
||||
"full_text": format_str.format(**format_values),
|
||||
'color': color,
|
||||
}
|
||||
|
@ -80,9 +80,13 @@ class NowPlaying(IntervalModule):
|
||||
def get_player(self):
|
||||
if self.player:
|
||||
player = "org.mpris.MediaPlayer2." + self.player
|
||||
try:
|
||||
return dbus.SessionBus().get_object(player, "/org/mpris/MediaPlayer2")
|
||||
except dbus.exceptions.DBusException:
|
||||
raise NoPlayerException()
|
||||
else:
|
||||
player = self.find_player()
|
||||
return dbus.SessionBus().get_object(player, "/org/mpris/MediaPlayer2")
|
||||
return dbus.SessionBus().get_object(player, "/org/mpris/MediaPlayer2")
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
|
@ -29,9 +29,11 @@ class pyLoad(IntervalModule):
|
||||
"format",
|
||||
"captcha_true", "captcha_false",
|
||||
"download_true", "download_false",
|
||||
"username", "password"
|
||||
"username", "password",
|
||||
('keyring_backend', 'alternative keyring backend for retrieving credentials'),
|
||||
)
|
||||
required = ("username", "password")
|
||||
keyring_backend = None
|
||||
|
||||
address = "http://127.0.0.1:8000"
|
||||
format = "{captcha} {progress_all:.1f}% {speed:.1f} kb/s"
|
||||
|
@ -35,6 +35,7 @@ class Reddit(IntervalModule):
|
||||
("format", "Format string used for output."),
|
||||
("username", "Reddit username."),
|
||||
("password", "Reddit password."),
|
||||
('keyring_backend', 'alternative keyring backend for retrieving credentials'),
|
||||
("subreddit", "Subreddit to monitor. Uses frontpage if unspecified."),
|
||||
("sort_by", "'hot', 'new', 'rising', 'controversial', or 'top'."),
|
||||
("color", "Standard color."),
|
||||
@ -48,6 +49,7 @@ class Reddit(IntervalModule):
|
||||
format = "[{submission_subreddit}] {submission_title} ({submission_domain})"
|
||||
username = ""
|
||||
password = ""
|
||||
keyring_backend = None
|
||||
subreddit = ""
|
||||
sort_by = "hot"
|
||||
color = "#FFFFFF"
|
||||
|
@ -1,6 +1,3 @@
|
||||
import re
|
||||
import glob
|
||||
|
||||
from i3pystatus import IntervalModule
|
||||
|
||||
|
||||
@ -16,10 +13,14 @@ class Temperature(IntervalModule):
|
||||
"format string used for output. {temp} is the temperature in degrees celsius"),
|
||||
"color",
|
||||
"file",
|
||||
"alert_temp",
|
||||
"alert_color",
|
||||
)
|
||||
format = "{temp} °C"
|
||||
color = "#FFFFFF"
|
||||
file = "/sys/class/thermal/thermal_zone0/temp"
|
||||
alert_temp = 90
|
||||
alert_color = "#FF0000"
|
||||
|
||||
def run(self):
|
||||
with open(self.file, "r") as f:
|
||||
@ -27,5 +28,5 @@ class Temperature(IntervalModule):
|
||||
|
||||
self.output = {
|
||||
"full_text": self.format.format(temp=temp),
|
||||
"color": self.color,
|
||||
"color": self.color if temp < self.alert_temp else self.alert_color,
|
||||
}
|
||||
|
@ -5,7 +5,6 @@ from bs4 import BeautifulSoup
|
||||
|
||||
|
||||
class WhosOnLocation():
|
||||
|
||||
email = None
|
||||
password = None
|
||||
session = None
|
||||
@ -68,9 +67,11 @@ class WOL(IntervalModule):
|
||||
password = None
|
||||
|
||||
settings = (
|
||||
('keyring_backend', 'alternative keyring backend for retrieving credentials'),
|
||||
'email',
|
||||
'password'
|
||||
)
|
||||
keyring_backend = None
|
||||
|
||||
color_on_site = '#00FF00'
|
||||
color_off_site = '#ff0000'
|
||||
|
80
setting_util.py
Executable file
80
setting_util.py
Executable file
@ -0,0 +1,80 @@
|
||||
#!/usr/bin/env python
|
||||
import glob
|
||||
import inspect
|
||||
import os
|
||||
import keyring
|
||||
import getpass
|
||||
import sys
|
||||
import signal
|
||||
from i3pystatus import Module, SettingsBase
|
||||
from i3pystatus.core import ClassFinder
|
||||
from collections import defaultdict, OrderedDict
|
||||
|
||||
def signal_handler(signal, frame):
|
||||
sys.exit(0)
|
||||
signal.signal(signal.SIGINT, signal_handler)
|
||||
|
||||
|
||||
def get_int_in_range(prompt, _range):
|
||||
while True:
|
||||
answer = input(prompt)
|
||||
try:
|
||||
n = int(answer.strip())
|
||||
if n in _range:
|
||||
return n
|
||||
else:
|
||||
print("Value out of range!")
|
||||
except ValueError:
|
||||
print("Invalid input!")
|
||||
|
||||
modules = [os.path.basename(m.replace('.py', ''))
|
||||
for m in glob.glob(os.path.join(os.path.dirname(__file__), "i3pystatus", "*.py"))
|
||||
if not os.path.basename(m).startswith('_')]
|
||||
|
||||
protected_settings = SettingsBase._SettingsBase__PROTECTED_SETTINGS
|
||||
class_finder = ClassFinder(Module)
|
||||
credential_modules = defaultdict(dict)
|
||||
for module_name in modules:
|
||||
try:
|
||||
module = class_finder.get_module(module_name)
|
||||
clazz = class_finder.get_class(module)
|
||||
members = [m[0] for m in inspect.getmembers(clazz) if not m[0].startswith('_')]
|
||||
if any([hasattr(clazz, setting) for setting in protected_settings]):
|
||||
credential_modules[clazz.__name__]['credentials'] = list(set(protected_settings) & set(members))
|
||||
credential_modules[clazz.__name__]['key'] = "%s.%s" % (clazz.__module__, clazz.__name__)
|
||||
elif hasattr(clazz, 'required'):
|
||||
protected = []
|
||||
required = getattr(clazz, 'required')
|
||||
for setting in protected_settings:
|
||||
if setting in required:
|
||||
protected.append(setting)
|
||||
if protected:
|
||||
credential_modules[clazz.__name__]['credentials'] = protected
|
||||
credential_modules[clazz.__name__]['key'] = "%s.%s" % (clazz.__module__, clazz.__name__)
|
||||
|
||||
except ImportError:
|
||||
continue
|
||||
|
||||
choices = [k for k in credential_modules.keys()]
|
||||
for idx, module in enumerate(choices, start=1):
|
||||
print("%s - %s" % (idx, module))
|
||||
|
||||
index = get_int_in_range("Choose module:\n> ", range(1, len(choices) + 1))
|
||||
module_name = choices[index - 1]
|
||||
module = credential_modules[module_name]
|
||||
|
||||
for idx, setting in enumerate(module['credentials'], start=1):
|
||||
print("%s - %s" % (idx, setting))
|
||||
|
||||
choices = module['credentials']
|
||||
index = get_int_in_range("Choose setting for %s:\n> " % module_name, range(1, len(choices) + 1))
|
||||
setting = choices[index - 1]
|
||||
|
||||
answer = getpass.getpass("Enter value for %s:\n> " % setting)
|
||||
answer2 = getpass.getpass("Re-enter value\n> ")
|
||||
if answer == answer2:
|
||||
key = "%s.%s" % (module['key'], setting)
|
||||
keyring.set_password(key, getpass.getuser(), answer)
|
||||
print("%s set!" % setting)
|
||||
else:
|
||||
print("Values don't match - nothing set.")
|
Loading…
Reference in New Issue
Block a user