diff --git a/pkgs/additional/sane-sysinfo/sane-sysinfo b/pkgs/additional/sane-sysinfo/sane-sysinfo index 51c54e97..995470f6 100755 --- a/pkgs/additional/sane-sysinfo/sane-sysinfo +++ b/pkgs/additional/sane-sysinfo/sane-sysinfo @@ -24,6 +24,7 @@ logger = logging.getLogger(__name__) # these icons may only render in nerdfonts ICON_BAT_CHG = ["󰢟", "󱊤", "󱊥", "󰂅"] ICON_BAT_DIS = ["󰂎", "󱊡", "󱊢", "󱊣"] +ICON_MEM="☵" SUFFIX_ICON = " " # thin space SUFFIX_PERCENT = "%" # SUFFIX_ICON=" " @@ -68,9 +69,12 @@ class Formatter: suffix_hr: str = SUFFIX_HR suffix_min: str = SUFFIX_MIN - def render_icon(self, direction: ChargeDirection, percentage: float) -> str: + def render_charge_icon(self, direction: ChargeDirection, percentage: float) -> str: return f"{self._choose_icon(direction, percentage)}{self.suffix_icon}" + def render_mem_icon(self) -> str: + return f"{ICON_MEM}{self.suffix_icon}" + def render_hours_minutes(self, minutes: int) -> str: hr = minutes // 60 min = minutes % 60 @@ -84,7 +88,7 @@ class Formatter: level = max(0, min(3, level)) level = int(round(level)) - logger.debug(f"render_icon: direction={direction} level={level}") + logger.debug(f"render_charge_icon: direction={direction} level={level}") if direction == ChargeDirection.Charging: return ICON_BAT_CHG[level] @@ -93,6 +97,46 @@ class Formatter: raise RuntimeError(f"invalid ChargeDirection {direction}") +class MemInfo: + """ + reads values from /proc/meminfo + """ + def __init__(self): + try: + lines = open("/proc/meminfo").readlines() + except Exception as e: + logger.info(f"failed to open /proc/meminfo: {e}") + lines = [] + + # lines are like: + # MemTotal: 16262708 kB + # HugePages_Total: 0 + self.entries = {} + for l in lines: + if ":" not in l: continue + key_len = l.index(":") + key, value_str = l[:key_len].strip(), l[key_len+1:].strip() + + unit_str = "" + if " " in value_str: + value_len = value_str.index(" ") + value_str, unit_str = value_str[:value_len].strip(), value_str[value_len+1:].strip() + + try: + value = int(value_str) + except: + logger.info(f"unexpected /proc/meminfo line: {l}") + continue + + if unit_str == "kB": + value = value * 1024 + self.entries[key] = value + + def get(self, entry): + v = self.entries.get(entry) + logger.debug(f"/proc/meminfo: {entry}={v}") + return v + class PowerSupply: """ reads values from /sys/class/power_supply/$dev/ API @@ -186,7 +230,7 @@ class BatteryInfo: ) -def try_path(p: str) -> BatteryInfo | None: +def try_battery_path(p: str) -> BatteryInfo | None: """ try to read battery information from some p = "/sys/class/power_supply/$node" path """ @@ -205,10 +249,10 @@ def try_path(p: str) -> BatteryInfo | None: return BatteryInfo(ps.capacity, charges_per_hour, ps.status) -def try_all_paths() -> BatteryInfo | None: - p = try_path("/sys/class/power_supply/axp20x-battery") # Pinephone +def try_all_batteries() -> BatteryInfo | None: + p = try_battery_path("/sys/class/power_supply/axp20x-battery") # Pinephone if p is None: - p = try_path("/sys/class/power_supply/BAT0") # Thinkpad + p = try_battery_path("/sys/class/power_supply/BAT0") # Thinkpad logger.debug(f"perc: {p.percent_charged if p else None}") logger.debug(f"charge: {p.percent_charged if p else None}") @@ -216,22 +260,52 @@ def try_all_paths() -> BatteryInfo | None: return p -def pretty_output(f: Formatter, p: BatteryInfo) -> str: - if p.minutes_to_charged != None: - logger.debug("charging") - icon = f.render_icon(ChargeDirection.Charging, p.percent_charged) - duration = p.minutes_to_charged - else: - logger.debug("discharging") - icon = f.render_icon(ChargeDirection.Discharging, p.percent_charged) - duration = p.minutes_to_discharged +@dataclass +class AllInfo: + _fmt: Formatter + _mem: MemInfo | None + _bat: BatteryInfo | None - if duration is not None and duration < 1440: - details = f.render_hours_minutes(duration) - else: - details = f.render_percent(p.percent_charged) + @property + def mem_icon(self) -> str: + if self._mem is None: return "" + return self._fmt.render_mem_icon() - return f"{icon}{details}" + @property + def mem_pct(self) -> str: + if self._mem is None: return "" + + total = self._mem.get("MemTotal") + free = self._mem.get("MemAvailable") + + if total is None or free is None or free > total: + return "" + + mem_use_pct = int((total - free) / total * 100) + return self._fmt.render_percent(mem_use_pct) + + @property + def bat_icon(self) -> str: + if self._bat is None: return "" + elif self._bat.minutes_to_charged != None: + logger.debug("bat_icon: charging") + return self._fmt.render_charge_icon(ChargeDirection.Charging, self._bat.percent_charged) + else: + logger.debug("bat_icon: discharging") + return self._fmt.render_charge_icon(ChargeDirection.Discharging, self._bat.percent_charged) + + @property + def bat_time(self) -> str: + if self._bat is None: return "" + elif self._bat.minutes_to_charged != None: + duration = self._bat.minutes_to_charged + else: + duration = self._bat.minutes_to_discharged + + if duration is not None and duration < 1440: + return self._fmt.render_hours_minutes(duration) + else: + return self._fmt.render_percent(self._bat.percent_charged) def main() -> None: logging.basicConfig() @@ -243,6 +317,8 @@ def main() -> None: parser.add_argument("--hour-suffix", default=SUFFIX_HR) parser.add_argument("--minute-suffix", default=SUFFIX_MIN) parser.add_argument("--percent-suffix", default=SUFFIX_PERCENT) + parser.add_argument("--template", default="{_.bat_icon}{_.bat_time}") + # parser.add_argument("--template", default="{_.mem_icon}{_.mem_pct}") args = parser.parse_args() if args.debug: @@ -254,9 +330,12 @@ def main() -> None: f.suffix_hr = args.hour_suffix f.suffix_min = args.minute_suffix - p = try_all_paths() - if p is not None: - print(pretty_output(f, p)) + info = AllInfo( + f, + MemInfo(), + try_all_batteries(), + ) + print(args.template.format(_=info)) if __name__ == "__main__": main()