From 69b702d2f16ebc9937c389a2c8c35eaac7d994ee Mon Sep 17 00:00:00 2001 From: enkore Date: Sun, 4 Aug 2013 23:40:19 +0200 Subject: [PATCH] 3.24: Introduce time wrapper, remove battery remaining_* formatter(!!!) --- README.md | 49 ++++++++++++++++++++++++++++++++++------- README.tpl.md | 35 ++++++++++++++++++++++++++++- i3pystatus/battery.py | 28 ++++++----------------- i3pystatus/core/util.py | 30 +++++++++++++++++++++++++ i3pystatus/mpd.py | 9 ++++---- setup.py | 2 +- tests/test_core_util.py | 2 ++ 7 files changed, 120 insertions(+), 35 deletions(-) diff --git a/README.md b/README.md index 4da3426..b908ce2 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,18 @@ status output compatible to i3status / i3bar of the i3 window manager. * [Arch Linux](https://aur.archlinux.org/packages/i3pystatus-git/) +### Release Notes + +#### 3.24 + +**This release introduced changes that may require manual changes to your +configuration file** + +* Introduced TimeWrapper +* battery module: removed remaining_\* formatters in favor of TimeWrapper, +as it can not only reproduce all the variants removed, but can do much more. +* mpd: Uses TimeWrapper for song_length, song_elapsed + ## Configuration You can keep your config file at various places, i3pystatus will look @@ -75,7 +87,7 @@ from network, wireless and pulseaudio in this example): # This would also display a desktop notification (via dbus) if the percentage # goes below 5 percent while discharging. The block will also color RED. status.register("battery", - format="{status}/{consumption:.2f}W {percentage:.2f}% [{percentage_design:.2f}%] {remaining_hm}", + format="{status}/{consumption:.2f}W {percentage:.2f}% [{percentage_design:.2f}%] {remaining:%E%hh:%Mm}", alert=True, alert_percentage=5, status={ @@ -185,6 +197,27 @@ Inside a group always all format specifiers must evaluate to true (logical and). You can nest groups. The inner group will only become part of the output if both the outer group and the inner group are eligible for output. +#### TimeWrapper + +Some modules that output times use TimeWrapper to format these. TimeWrapper is +a mere extension of the standard formatting method. + +The time format that should be used is specified using the format specifier, i.e. +with some_time being 3951 seconds a format string like `{some_time:%h:%m:%s}` +would produce `1:5:51` + +* `%h`, `%m` and `%s` are the hours, minutes and seconds without leading zeros +(i.e. 0 to 59 for minutes and seconds) +* `%H`, `%M` and `%S` are padded with a leading zero to two digits, i.e. 00 to 59 +* `%l` and `%L` produce hours non-padded and padded but only if hours is not zero. +If the hours are zero it produces an empty string. +* `%%` produces a literal % +* `%E` (only valid on beginning of the string) if the time is null, don't format +anything but rather produce an empty string. If the time is non-null it is +removed from the string. +* When the module in question also uses formatp, 0 seconds counts as "not known". +* The formatted time is stripped, i.e. spaces on both ends of the result are removed + ## Modules @@ -244,8 +277,7 @@ battery status Available formatters for format and alert_format_\*: -* `{remaining_str}` — remaining time for charging or discharging in the format H:MM -* `{remaining_hm}`- remaining time in the format Hh:MMm +* `{remaining}` — remaining time for charging or discharging, uses TimeWrapper formatting, default format is `%E%h:%M` * `{percentage}` — battery percentage relative to the last full value * `{percentage_design}` — absolute battery charge percentage * `{consumption (Watts)}` — current power flowing into/out of the battery @@ -256,13 +288,13 @@ Available formatters for format and alert_format_\*: __Settings:__ * `battery_ident` — The name of your battery, usually BAT0 or BAT1 (default: `BAT0`) -* `format` — (default: `{status} {remaining_hm}`) +* `format` — (default: `{status} {remaining}`) * `alert` — Display a libnotify-notification on low battery (default: `False`) * `alert_percentage` — (default: `10`) * `alert_format_title` — The title of the notification, all formatters can be used (default: `Low battery`) * `alert_format_body` — The body text of the notification, all formatters can be used (default: `Battery {battery_ident} has only {percentage:.2f}% ({remaining_hm}) remaining!`) * `path` — Override the default-generated path (default: `None`) -* `status` — A dictionary mapping ('DIS', 'CHR', 'FULL') to alternative names (default: `{'FULL': 'FULL', 'DIS': 'DIS', 'CHR': 'CHR'}`) +* `status` — A dictionary mapping ('DIS', 'CHR', 'FULL') to alternative names (default: `{'CHR': 'CHR', 'DIS': 'DIS', 'FULL': 'FULL'}`) @@ -431,8 +463,8 @@ Available formatters (uses formatp) * `{title}` — (the title of the current song) * `{album}` — (the album of the current song, can be an empty string (e.g. for online streams)) * `{artist}` — (can be empty, too) -* `{song_elapsed}` — (Position in the currently playing song, looks like 3:54) -* `{song_length}` — (Length of the current song, same format as song_elapsed) +* `{song_elapsed}` — (Position in the currently playing song, **uses TimeWrapper**, default is `%m:%S`) +* `{song_length}` — (Length of the current song, same as song_elapsed) * `{pos}` — (Position of current song in playlist, one-based) * `{len}` — (Songs in playlist) * `{status}` — (play, pause, stop mapped through the `status` dictionary) @@ -447,7 +479,7 @@ __Settings:__ * `host` — (default: `localhost`) * `port` — MPD port (default: `6600`) * `format` — formatp string (default: `{title} {status}`) -* `status` — Dictionary mapping pause, play and stop to output (default: `{'stop': '◾', 'pause': '▷', 'play': '▶'}`) +* `status` — Dictionary mapping pause, play and stop to output (default: `{'play': '▶', 'stop': '◾', 'pause': '▷'}`) @@ -503,6 +535,7 @@ Shows volume of default PulseAudio sink (output). Available formatters: * `{volume}` — volume in percent (0...100) * `{db}` — volume in decibels relative to 100 %, i.e. 100 % = 0 dB, 50 % = -18 dB, 0 % = -infinity dB +(the literal value for -infinity is `-∞`) __Settings:__ diff --git a/README.tpl.md b/README.tpl.md index faeec7a..c12e4b8 100644 --- a/README.tpl.md +++ b/README.tpl.md @@ -19,6 +19,18 @@ status output compatible to i3status / i3bar of the i3 window manager. * [Arch Linux](https://aur.archlinux.org/packages/i3pystatus-git/) +### Release Notes + +#### 3.24 + +**This release introduced changes that may require manual changes to your +configuration file** + +* Introduced TimeWrapper +* battery module: removed remaining_\* formatters in favor of TimeWrapper, +as it can not only reproduce all the variants removed, but can do much more. +* mpd: Uses TimeWrapper for song_length, song_elapsed + ## Configuration You can keep your config file at various places, i3pystatus will look @@ -75,7 +87,7 @@ from network, wireless and pulseaudio in this example): # This would also display a desktop notification (via dbus) if the percentage # goes below 5 percent while discharging. The block will also color RED. status.register("battery", - format="{status}/{consumption:.2f}W {percentage:.2f}% [{percentage_design:.2f}%] {remaining_hm}", + format="{status}/{consumption:.2f}W {percentage:.2f}% [{percentage_design:.2f}%] {remaining:%E%hh:%Mm}", alert=True, alert_percentage=5, status={ @@ -185,6 +197,27 @@ Inside a group always all format specifiers must evaluate to true (logical and). You can nest groups. The inner group will only become part of the output if both the outer group and the inner group are eligible for output. +#### TimeWrapper + +Some modules that output times use TimeWrapper to format these. TimeWrapper is +a mere extension of the standard formatting method. + +The time format that should be used is specified using the format specifier, i.e. +with some_time being 3951 seconds a format string like `{some_time:%h:%m:%s}` +would produce `1:5:51` + +* `%h`, `%m` and `%s` are the hours, minutes and seconds without leading zeros +(i.e. 0 to 59 for minutes and seconds) +* `%H`, `%M` and `%S` are padded with a leading zero to two digits, i.e. 00 to 59 +* `%l` and `%L` produce hours non-padded and padded but only if hours is not zero. +If the hours are zero it produces an empty string. +* `%%` produces a literal % +* `%E` (only valid on beginning of the string) if the time is null, don't format +anything but rather produce an empty string. If the time is non-null it is +removed from the string. +* When the module in question also uses formatp, 0 seconds counts as "not known". +* The formatted time is stripped, i.e. spaces on both ends of the result are removed + ## Modules !!module_doc!! diff --git a/i3pystatus/battery.py b/i3pystatus/battery.py index c88ccdc..a25a9d2 100644 --- a/i3pystatus/battery.py +++ b/i3pystatus/battery.py @@ -4,9 +4,9 @@ import re import configparser -from . import IntervalModule -from .core.util import PrefixedKeyDict, lchop -from .core.desktop import display_notification +from i3pystatus import IntervalModule +from i3pystatus.core.util import PrefixedKeyDict, lchop, TimeWrapper +from i3pystatus.core.desktop import display_notification class UEventParser(configparser.ConfigParser): @staticmethod @@ -78,18 +78,6 @@ class BatteryEnergy(Battery): else: return (self.bat["ENERGY_FULL"] - self.bat["ENERGY_NOW"]) / self.bat["POWER_NOW"] * 60 -def format_remaining(minutes, prefix): - hours, minutes = map(int, divmod(minutes, 60)) - - d = PrefixedKeyDict(prefix) - d.update({ - "str": "{}:{:02}".format(hours, minutes), - "hm": "{}h:{:02}m".format(hours, minutes), - "hours": hours, - "mins": minutes, - }) - return d - class BatteryChecker(IntervalModule): """ This class uses the /sys/class/power_supply/…/uevent interface to check for the @@ -97,8 +85,7 @@ class BatteryChecker(IntervalModule): Available formatters for format and alert_format_\*: - * `{remaining_str}` — remaining time for charging or discharging in the format H:MM - * `{remaining_hm}`- remaining time in the format Hh:MMm + * `{remaining}` — remaining time for charging or discharging, uses TimeWrapper formatting, default format is `%E%h:%M` * `{percentage}` — battery percentage relative to the last full value * `{percentage_design}` — absolute battery charge percentage * `{consumption (Watts)}` — current power flowing into/out of the battery @@ -117,7 +104,7 @@ class BatteryChecker(IntervalModule): ("status", "A dictionary mapping ('DIS', 'CHR', 'FULL') to alternative names"), ) battery_ident = "BAT0" - format = "{status} {remaining_hm}" + format = "{status} {remaining}" status = { "CHR": "CHR", "DIS": "DIS", @@ -143,17 +130,16 @@ class BatteryChecker(IntervalModule): fdict = { "battery_ident": self.battery_ident, - "remaining_str": "", - "remaining_hm": "", "percentage": battery.percentage(), "percentage_design": battery.percentage(design=True), "consumption": battery.consumption(), + "remaining": TimeWrapper(0, "%E%h:%M"), } status = battery.status() if status in ["Discharging", "Charging"]: remaining = battery.remaining() - fdict.update(format_remaining(remaining, "remaining_")) + fdict["remaining"] = TimeWrapper(remaining * 60, "%E%h:%M") if status == "Discharging": fdict["status"] = "DIS" if remaining < 15: diff --git a/i3pystatus/core/util.py b/i3pystatus/core/util.py index ba5ad0a..d9d8d50 100644 --- a/i3pystatus/core/util.py +++ b/i3pystatus/core/util.py @@ -2,6 +2,7 @@ import collections import itertools import re +import string from i3pystatus.core.exceptions import * from i3pystatus.core.imputil import ClassFinder @@ -240,3 +241,32 @@ def formatp(string, **kwargs): return merge_tree(tree) formatp.field_re = re.compile(r"({(\w+)[^}]*})") + +class TimeWrapper: + class TimeTemplate(string.Template): + delimiter = "%" + idpattern = r"[a-zA-Z]" + + def __init__(self, seconds, default_format="%m:%S"): + self.seconds = int(seconds) + self.default_format = default_format + + def __bool__(self): + return bool(self.seconds) + + def __format__(self, format_spec): + format_spec = format_spec or self.default_format + h = self.seconds // 3600 + m, s = divmod(self.seconds % 3600, 60) + l = h if h else "" + L = "%02d" % h if h else "" + + if format_spec.startswith("%E"): + format_spec = format_spec[2:] + if not self.seconds: + return "" + return self.TimeTemplate(format_spec).substitute( + h=h, m=m, s=s, + H="%02d" % h, M="%02d" % m, S="%02d" % s, + l=l, L=L, + ).strip() diff --git a/i3pystatus/mpd.py b/i3pystatus/mpd.py index ca7ea30..3e2d124 100644 --- a/i3pystatus/mpd.py +++ b/i3pystatus/mpd.py @@ -2,6 +2,7 @@ import socket from i3pystatus import IntervalModule, formatp +from i3pystatus.core.util import TimeWrapper def format_time(seconds): return "{}:{:02}".format(*divmod(int(seconds), 60)) if seconds else "" @@ -14,8 +15,8 @@ class MPD(IntervalModule): * `{title}` — (the title of the current song) * `{album}` — (the album of the current song, can be an empty string (e.g. for online streams)) * `{artist}` — (can be empty, too) - * `{song_elapsed}` — (Position in the currently playing song, looks like 3:54) - * `{song_length}` — (Length of the current song, same format as song_elapsed) + * `{song_elapsed}` — (Position in the currently playing song, **uses TimeWrapper**, default is `%m:%S`) + * `{song_length}` — (Length of the current song, same as song_elapsed) * `{pos}` — (Position of current song in playlist, one-based) * `{len}` — (Songs in playlist) * `{status}` — (play, pause, stop mapped through the `status` dictionary) @@ -78,8 +79,8 @@ class MPD(IntervalModule): "title": currentsong.get("Title", ""), "album": currentsong.get("Album", ""), "artist": currentsong.get("Artist", ""), - "song_length": format_time(currentsong.get("Time", 0)), - "song_elapsed": format_time(float(status.get("elapsed", 0))), + "song_length": TimeWrapper(currentsong.get("Time", 0)), + "song_elapsed": TimeWrapper(float(status.get("elapsed", 0))), "bitrate": int(status.get("bitrate", 0)), } diff --git a/setup.py b/setup.py index 5873932..d028b22 100755 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ from setuptools import setup setup(name="i3pystatus", - version="3.23", + version="3.24", description="Like i3status, this generates status line for i3bar / i3wm", url="http://github.com/enkore/i3pystatus", license="MIT", diff --git a/tests/test_core_util.py b/tests/test_core_util.py index f40071c..a658355 100644 --- a/tests/test_core_util.py +++ b/tests/test_core_util.py @@ -266,6 +266,8 @@ class FormatPTests(unittest.TestCase): def test_side_by_side(self): s = "{status} [{artist} / [{album} / ]]{title}[ {song_elapsed}/{song_length}]" assert util.formatp(s, status="▷", title="Only For The Weak", song_elapsed="1:41", song_length="4:55") == "▷ Only For The Weak 1:41/4:55" + assert util.formatp(s, status="", album="Foo", title="Die, Die, Crucified", song_elapsed="2:52") == " Die, Die, Crucified" + assert util.formatp("[[{a}][{b}]]", b=1) == "1" def test_complex_field(self): class NS: