sane-sysinfo: add a way to render memory use

This commit is contained in:
Colin 2024-06-15 11:12:45 +00:00
parent f4d806c0c2
commit 0f6c9f3cde

View File

@ -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()