From a4f5343fb5bfc5419f6e48fe8dae45dc512e8384 Mon Sep 17 00:00:00 2001 From: Colin Date: Sat, 15 Jun 2024 08:59:50 +0000 Subject: [PATCH] sane-sysinfo: port to Python it's a pretty literal port; probably has some bugs --- pkgs/additional/sane-sysinfo/default.nix | 2 +- pkgs/additional/sane-sysinfo/sane-sysinfo | 358 ++++++++++++---------- 2 files changed, 203 insertions(+), 157 deletions(-) diff --git a/pkgs/additional/sane-sysinfo/default.nix b/pkgs/additional/sane-sysinfo/default.nix index dc8f563e..fe7a3404 100644 --- a/pkgs/additional/sane-sysinfo/default.nix +++ b/pkgs/additional/sane-sysinfo/default.nix @@ -1,5 +1,5 @@ { static-nix-shell }: -static-nix-shell.mkBash { +static-nix-shell.mkPython3Bin { pname = "sane-sysinfo"; srcRoot = ./.; } diff --git a/pkgs/additional/sane-sysinfo/sane-sysinfo b/pkgs/additional/sane-sysinfo/sane-sysinfo index a44f3356..e9e65847 100755 --- a/pkgs/additional/sane-sysinfo/sane-sysinfo +++ b/pkgs/additional/sane-sysinfo/sane-sysinfo @@ -1,183 +1,229 @@ #!/usr/bin/env nix-shell -#!nix-shell -i bash -p bash +#!nix-shell -i python3 -p "python3.withPackages (ps: [ ])" +""" +usage: sane-sysinfo [options...] -usage() { - echo "usage: sane-sysinfo [options...]" - echo - echo "pretty-prints a battery estimate (icon to indicate state, and a duration estimate)" - echo - echo "options:" - echo " --debug: output additional information, to stderr" - echo " --minute-suffix : use the provided string as a minutes suffix" - echo " --hour-suffix : use the provided string as an hours suffix" - echo " --icon-suffix : use the provided string as an icon suffix" - echo " --percent-suffix : use the provided string when displaying percents" -} +pretty-prints a battery estimate (icon to indicate state, and a duration estimate) + +options: + --debug: output additional information, to stderr + --minute-suffix : use the provided string as a minutes suffix + --hour-suffix : use the provided string as an hours suffix + --icon-suffix : use the provided string as an icon suffix + --percent-suffix : use the provided string when displaying percents +""" + +import argparse +import logging + +from dataclasses import dataclass +from enum import Enum + +logger = logging.getLogger(__name__) # these icons may only render in nerdfonts -icon_bat_chg=("󰢟" "󱊤" "󱊥" "󰂅") -icon_bat_dis=("󰂎" "󱊡" "󱊢" "󱊣") -suffix_icon=" " # thin space -suffix_percent="%" -# suffix_icon=" " +ICON_BAT_CHG = ["󰢟", "󱊤", "󱊥", "󰂅"] +ICON_BAT_DIS = ["󰂎", "󱊡", "󱊢", "󱊣"] +SUFFIX_ICON = " " # thin space +SUFFIX_PERCENT = "%" +# SUFFIX_ICON=" " # render time like: 2ʰ08ᵐ # unicode sub/super-scripts: -# symbol_hr="ʰ" -# symbol_min="ᵐ" +# SUFFIX_HR="ʰ" +# SUFFIX_MIN="ᵐ" # render time like: 2ₕ08ₘ -# symbol_hr="ₕ" -# symbol_min="ₘ" +# SUFFIX_HR="ₕ" +# SUFFIX_MIN="ₘ" # render time like: 2h08m -# symbol_hr="h" -# symbol_min="m" +# SUFFIX_HR="H" +# SUFFIX_MIN="M" # render time like: 2:08 -# symbol_hr=":" -# symbol_min= +# SUFFIX_HR=":" +# SUFFIX_MIN= # render time like: 2꞉08⧗ -symbol_hr="꞉" -symbol_min="⧗" +SUFFIX_HR="꞉" +SUFFIX_MIN="⧗" # variants: -# symbol_hr=":" -# symbol_min="⧖" -# symbol_min="⌛" +# SUFFIX_HR=":" +# SUFFIX_MIN="⧖" +# SUFFIX_MIN="⌛" # render time like: 2'08" -# symbol_hr="'" -# symbol_min='"' +# SUFFIX_HR="'" +# SUFFIX_MIN='"' -log() { - if [ "$BATTERY_ESTIMATE_DEBUG" = "1" ]; then - printf "$@" >&2 - echo >&2 - fi -} +class ChargeDirection(Enum): + Charging = "Charging" + Discharging = "Discharging" -render_icon() { - # args: - # 1: "chg" or "dis" - # 2: current battery percentage - level=$(($2 / 25)) - level=$(($level > 3 ? 3 : $level)) - level=$(($level < 0 ? 0 : $level)) - log "icon: %s %d" "$1" "$level" - if [ "$1" = "dis" ]; then - printf "%s" "${icon_bat_dis[$level]}" - elif [ "$1" = "chg" ]; then - printf "%s" "${icon_bat_chg[$level]}" - fi -} +@dataclass +class Formatter: + suffix_icon: str = SUFFIX_ICON + suffix_percent: str = SUFFIX_PERCENT + suffix_hr: str = SUFFIX_HR + suffix_min: str = SUFFIX_MIN -try_path() { - # assigns output variables: - # - perc, perc_from_full (0-100) - # - full, rate (pos means charging) - if [ -f "$1/capacity" ]; then - log "perc, perc_from_full from %s" "$1/capacity" - perc=$(cat "$1/capacity") - perc_from_full=$((100 - $perc)) - fi +@dataclass +class ParsedPowerSupply: + percent_charged: int | None = None + # unitless: could be joules, could be something else + charge_full: int | None = None + # charge per hour + charge_rate: int | None = None - if [ -f "$1/charge_full_design" ] && [ -f "$1/current_now" ]; then - log "full, rate from %s and %s" "$1/charge_full_design" "$1/current_now" - # current is positive when charging - full=$(cat "$1/charge_full_design") - rate=$(cat "$1/current_now") - elif [ -f "$1/energy_full" ] && [ -f "$1/power_now" ]; then - log "full, rate from %s and %s" "$1/energy_full" "$1/power_now" - # power_now is positive when discharging - full=$(cat "$1/energy_full") - rate=-$(cat "$1/power_now") - elif [ -f "$1/energy_full" ] && [ -f "$1/energy_now" ]; then - log "full, rate from %s and %s" "$1/energy_full" "$1/energy_now" - log " this is a compatibility path for legacy Thinkpad batteries which do not populate the 'power_now' field, and incorrectly populate 'energy_now' with power info" - # energy_now is positive when discharging - full=$(cat "$1/energy_full") - rate=-$(cat "$1/energy_now") - fi -} + def is_valid(self) -> bool: + return self.percent_charged is not None and \ + self.charge_full is not None and \ + self.charge_rate is not None -try_all_paths() { - try_path "/sys/class/power_supply/axp20x-battery" # Pinephone - try_path "/sys/class/power_supply/BAT0" # Thinkpad - log "perc: %d, perc_from_full: %d" "$perc" "$perc_from_full" - log "full: %f, rate: %f" "$full" "$rate" - log " rate > 0 means charging, else discharging" -} + @property + def percent_discharged(self) -> int | None: + if self.percent_charged is not None: + return 100 - self.percent_charged -fmt_minutes() { - # args: - # 1: icon to render - # 2: string to show if charge/discharge time is indefinite - # 3: minutes to stable state (i.e. to full charge or full discharge) - # - we work in minutes instead of hours for precision: bash math is integer-only - log "charge/discharge time: %f min" "$3" - # args: - if [ -n "$3" ] && [ "$3" -lt 1440 ]; then - hr=$(($3 / 60)) - hr_in_min=$(($hr * 60)) - min=$(($3 - $hr_in_min)) - printf "%s%s%d%s%02d%s" "$1" "$suffix_icon" "$hr" "$symbol_hr" "$min" "$symbol_min" - else - log "charge/discharge duration > 1d" - printf "%s%s%s" "$1" "$suffix_icon" "$2" # more than 1d - fi -} + @property + def discharge_rate(self) -> int | None: + if self.charge_rate is not None: + return -self.charge_rate -pretty_output() { - if [ -n "$perc" ]; then - duration="" - if [ "$rate" -gt 0 ]; then - log "charging" - icon="$(render_icon chg $perc)" - duration="$(($full * 60 * $perc_from_full / (100 * $rate)))" - else - log "discharging" - icon="$(render_icon dis $perc)" - if [ "$rate" -lt 0 ]; then - duration="$(($full * 60 * $perc / (-100 * $rate)))" - fi - fi - fmt_minutes "$icon" "$perc$suffix_percent" "$duration" - fi -} + @property + def minutes_to_charged(self) -> int | None: + if self.percent_discharged is not None and self.charge_full and self.charge_rate > 0: + return int(self.charge_full * self.percent_discharged/100 / self.charge_rate * 60) -while [ "$#" -gt 0 ]; do - case "$1" in - "--debug") - shift - BATTERY_ESTIMATE_DEBUG=1 - ;; - "--icon-suffix") - shift - suffix_icon="$1" - shift - ;; - "--hour-suffix") - shift - symbol_hr="$1" - shift - ;; - "--minute-suffix") - shift - symbol_min="$1" - shift - ;; - "--percent-suffix") - shift - suffix_percent="$1" - shift - ;; - *) - usage - exit 1 - ;; - esac -done + @property + def minutes_to_discharged(self) -> int | None: + if self.percent_charged is not None and self.charge_full and self.charge_rate < 0: + return int(self.charge_full * self.percent_charged/100 / self.discharge_rate * 60) -try_all_paths -pretty_output + +def render_icon(direction: ChargeDirection, percentage: float) -> str: + level = percentage / 25 + level = max(0, min(3, level)) + level = int(round(level)) + + logger.debug(f"render_icon: direction={direction} level={level}") + + if direction == ChargeDirection.Charging: + return ICON_BAT_CHG[level] + elif direction == ChargeDirection.Discharging: + return ICON_BAT_DIS[level] + + raise RuntimeError(f"invalid ChargeDirection {direction}") + +def try_path(p: str) -> ParsedPowerSupply | None: + """ + try to read battery information from some p = "/sys/class/power_supply/$node" path + """ + state = ParsedPowerSupply() + try: + capacity_text = open(f"{p}/capacity").read() + except: pass + else: + logger.debug(f"read from {p}/capacity: {capacity_text}") + state.percent_charged = int(capacity_text) + + try: + charge_full_design_text = open(f"{p}/charge_full_design").read() + current_now_text = open(f"{p}/current_now").read() + except: pass + else: + logger.debug(f"read from {p}/charge_full_design: {charge_full_design_text}") + logger.debug(f"read from {p}/charge_now: {charge_now_text}") + state.charge_full = int(charge_full_design_text) + # current_now is positive when charging + state.charge_rate = int(current_now_text) + + if state.is_valid(): return state + + try: + energy_full_text = open(f"{p}/energy_full").read() + power_now_text = open(f"{p}/power_now").read() + except: pass + else: + logger.debug(f"read from {p}/energy_full: {energy_full_text}") + logger.debug(f"read from {p}/power_now: {power_now_text}") + state.charge_full = int(energy_full_text) + # power_now is positive when discharging + state.charge_rate = -int(power_now_text) + + if state.is_valid(): return state + + try: + energy_full_text = open(f"{p}/energy_full").read() + energy_now_text = open(f"{p}/energy_now").read() + except: pass + else: + logger.debug(f"read from {p}/energy_full: {energy_full_text}") + logger.debug(f"read from {p}/energy_now: {energy_now_text}") + state.charge_full = int(energy_full_text) + # energy_now is positive when discharging + state.charge_rate = -int(energy_now_text) + + return state if state.percent_charged is not None else None + +def try_all_paths() -> ParsedPowerSupply | None: + p = try_path("/sys/class/power_supply/axp20x-battery") # Pinephone + if p is None: + p = try_path("/sys/class/power_supply/BAT0") # Thinkpad + + logger.debug(f"perc: {p.percent_charged if p else None}") + logger.debug(f"full: {p.charge_full if p else None}, rate: {p.charge_rate if p else None}") + logger.debug(" rate > 0 means charging, else discharging") + + return p + +def fmt_minutes(f: Formatter, icon: str, if_indefinite: str, minutes: int | None) -> str: + logger.debug(f"charge/discharge time: {minutes} min") + + if minutes < 1440: + hr = minutes // 60 + min = minutes % 60 + return f"{icon}{f.suffix_icon}{hr}{f.suffix_hr}{min:02}{f.suffix_min}" + else: + logger.debug("charge/discharge duration > 1d") + return f"{icon}{f.suffix_icon}{if_indefinite}" + +def pretty_output(f: Formatter, p: ParsedPowerSupply) -> str: + if p.charge_rate > 0: + logger.debug("charging") + icon = render_icon(ChargeDirection.Charging, p.percent_charged) + duration = p.minutes_to_charged + else: + logger.debug("discharging") + icon = render_icon(ChargeDirection.Discharging, p.percent_discharged) + duration = p.minutes_to_discharged + + return fmt_minutes(f, icon, f"{p.percent_charged}{f.suffix_percent}", duration) + +def main(): + logging.basicConfig() + logging.getLogger().setLevel(logging.INFO) + + parser = argparse.ArgumentParser(usage=__doc__) + parser.add_argument("--debug", action="store_true") + parser.add_argument("--icon-suffix", default=SUFFIX_ICON) + parser.add_argument("--hour-suffix", default=SUFFIX_HR) + parser.add_argument("--minute-suffix", default=SUFFIX_MIN) + parser.add_argument("--percent-suffix", default=SUFFIX_PERCENT) + args = parser.parse_args() + + if args.debug: + logger.setLevel(logging.DEBUG) + + f = Formatter() + f.suffix_icon = args.icon_suffix + f.suffix_percent = args.percent_suffix + f.suffix_hr = args.hour_suffix + f.suffix_min = args.minute_suffix + + p = try_all_paths() + print(pretty_output(f, p)) + +if __name__ == "__main__": + main()