diff options
-rw-r--r-- | src/tiptop/__about__.py | 2 | ||||
-rw-r--r-- | src/tiptop/_app.py | 35 | ||||
-rw-r--r-- | src/tiptop/_battery.py | 6 | ||||
-rw-r--r-- | src/tiptop/_cpu.py | 13 | ||||
-rw-r--r-- | src/tiptop/_disk.py | 163 | ||||
-rw-r--r-- | src/tiptop/_helpers.py | 2 | ||||
-rw-r--r-- | src/tiptop/_mem.py | 6 | ||||
-rw-r--r-- | src/tiptop/_net.py | 9 | ||||
-rw-r--r-- | src/tiptop/_procs_list.py | 43 |
9 files changed, 234 insertions, 45 deletions
diff --git a/src/tiptop/__about__.py b/src/tiptop/__about__.py index 9cb17e7..d3ec452 100644 --- a/src/tiptop/__about__.py +++ b/src/tiptop/__about__.py @@ -1 +1 @@ -__version__ = "0.1.8" +__version__ = "0.2.0" diff --git a/src/tiptop/_app.py b/src/tiptop/_app.py index e0bfc1c..2a53505 100644 --- a/src/tiptop/_app.py +++ b/src/tiptop/_app.py @@ -9,6 +9,7 @@ from textual.app import App from .__about__ import __version__ from ._battery import Battery from ._cpu import CPU +from ._disk import Disk from ._info import InfoLine from ._mem import Mem from ._net import Net @@ -71,8 +72,8 @@ def run(argv=None): # 34/55: approx golden ratio. See # <https://gist.github.com/nschloe/ab6c3c90b4a6bc02c40405803fa8fa35> # for the error. - grid.add_column(fraction=34, name="left") - grid.add_column(fraction=55, name="right") + grid.add_column(fraction=55, name="left") + grid.add_column(fraction=34, name="right") if psutil.sensors_battery() is None: grid.add_row(size=1, name="topline") @@ -94,25 +95,27 @@ def run(argv=None): area3=ProcsList(), ) else: - grid.add_row(size=1, name="topline") - grid.add_row(fraction=3, name="top") - grid.add_row(fraction=1, name="center1") - grid.add_row(fraction=3, name="center2") - grid.add_row(fraction=3, name="bottom") + grid.add_row(size=1, name="r0") + grid.add_row(fraction=17, name="r1") + grid.add_row(fraction=4, name="r2") + grid.add_row(fraction=15, name="r3") + grid.add_row(fraction=15, name="r4") grid.add_areas( - area0="left-start|right-end,topline", - area1="left-start|right-end,top", - area2a="left,center1", - area2b="left,center2", - area2c="left,bottom", - area3="right,center1-start|bottom-end", + area0="left-start|right-end,r0", + area1="left,r1", + area2a="right,r1", + area2b="right,r2", + area2c="right,r3", + area2d="right,r4", + area3="left,r2-start|r4-end", ) grid.place( area0=InfoLine(), area1=CPU(), - area2a=Battery(), - area2b=Mem(), - area2c=Net(args.net), + area2a=Mem(), + area2b=Battery(), + area2c=Disk(), + area2d=Net(args.net), area3=ProcsList(), ) diff --git a/src/tiptop/_battery.py b/src/tiptop/_battery.py index 68dbaf0..2d6ed96 100644 --- a/src/tiptop/_battery.py +++ b/src/tiptop/_battery.py @@ -14,7 +14,8 @@ class Battery(Widget): "", title="", title_align="left", - border_style="yellow", + # border_style="yellow", + border_style="white", box=box.SQUARE, ) self.collect_data() @@ -23,6 +24,7 @@ class Battery(Widget): def collect_data(self): bat = psutil.sensors_battery() + assert bat is not None self.bat_stream.add_value(bat.percent) @@ -40,7 +42,7 @@ class Battery(Widget): time_left_str.append(f"{mm}min") status = " ".join(time_left_str) + " left" - title = f"battery - {self.bat_stream.values[-1]:.1f}% - {status}" + title = f"[b]battery[/] - {self.bat_stream.values[-1]:.1f}% - {status}" if bat.percent < 15 and not bat.power_plugged: title = "[red reverse bold]" + title + "[/]" diff --git a/src/tiptop/_cpu.py b/src/tiptop/_cpu.py index 75c7f75..93c8198 100644 --- a/src/tiptop/_cpu.py +++ b/src/tiptop/_cpu.py @@ -8,8 +8,6 @@ from textual.widget import Widget from .braille_stream import BrailleStream -# from textual import log - def val_to_color(val: float, minval: float, maxval: float) -> str: t = (val - minval) / (maxval - minval) @@ -125,17 +123,18 @@ class CPU(Widget): box=box.SQUARE, expand=False, ) - self.info_box_width = 0 brand_raw = cpuinfo.get_cpu_info()["brand_raw"] self.panel = Panel( "", - title=f"cpu - {brand_raw}", + title=f"[b]cpu[/] - {brand_raw}", title_align="left", - border_style="blue", + border_style="white", box=box.SQUARE, ) + # immediately collect data to refresh info_box_width + self.collect_data() self.set_interval(2.0, self.collect_data) def collect_data(self): @@ -238,6 +237,10 @@ class CPU(Widget): # https://github.com/nschloe/tiptop/issues/25 self.info_box.subtitle = None else: + if psutil.__version__ == "5.9.0": + # Work around + # https://github.com/giampaolo/psutil/issues/2049 + cpu_freq *= 1000 self.info_box.subtitle = f"{round(cpu_freq):4d} MHz" # https://github.com/willmcgugan/rich/discussions/1559#discussioncomment-1459008 diff --git a/src/tiptop/_disk.py b/src/tiptop/_disk.py new file mode 100644 index 0000000..40bbf9e --- /dev/null +++ b/src/tiptop/_disk.py @@ -0,0 +1,163 @@ +from __future__ import annotations + +import psutil +from rich import box +from rich.console import Group +from rich.panel import Panel +from rich.table import Table +from rich.text import Text +from textual.widget import Widget + +from ._helpers import sizeof_fmt +from .braille_stream import BrailleStream + + +class Disk(Widget): + def __init__(self): + super().__init__() + + def on_mount(self): + self.down_box = Panel( + "", + title="read", + title_align="left", + style="green", + width=20, + box=box.SQUARE, + ) + self.up_box = Panel( + "", + title="write", + title_align="left", + style="blue", + width=20, + box=box.SQUARE, + ) + self.table = Table(expand=True, show_header=False, padding=0, box=None) + # Add ratio 1 to expand that column as much as possible + self.table.add_column("graph", no_wrap=True, ratio=1) + self.table.add_column("box", no_wrap=True, width=20) + self.table.add_row("", self.down_box) + self.table.add_row("", self.up_box) + + # kick out /dev/loop* devices + self.mountpoints = [ + item.mountpoint + for item in psutil.disk_partitions() + if not item.device.startswith("/dev/loop") + ] + self.total = [ + sizeof_fmt(psutil.disk_usage(mp).total, fmt=".1f") + for mp in self.mountpoints + ] + + self.group = Group(self.table, "") + self.panel = Panel( + self.group, + title="[b]disk[/]", + # border_style="magenta", + border_style="white", + title_align="left", + box=box.SQUARE, + ) + + self.last_io = None + self.max_read_bytes_s = 0 + self.max_read_bytes_s_str = "" + self.max_write_bytes_s = 0 + self.max_write_bytes_s_str = "" + + self.read_stream = BrailleStream(20, 5, 0.0, 1.0e6) + self.write_stream = BrailleStream(20, 5, 0.0, 1.0e6, flipud=True) + + self.refresh_panel() + + self.interval_s = 2.0 + self.set_interval(self.interval_s, self.refresh_panel) + + def refresh_panel(self): + io = psutil.disk_io_counters() + + if self.last_io is None: + read_bytes_s_string = "" + write_bytes_s_string = "" + else: + read_bytes_s = (io.read_bytes - self.last_io.read_bytes) / self.interval_s + read_bytes_s_string = sizeof_fmt(read_bytes_s, fmt=".1f") + "/s" + write_bytes_s = ( + io.write_bytes - self.last_io.write_bytes + ) / self.interval_s + write_bytes_s_string = sizeof_fmt(write_bytes_s, fmt=".1f") + "/s" + + if read_bytes_s > self.max_read_bytes_s: + self.max_read_bytes_s = read_bytes_s + self.max_read_bytes_s_str = sizeof_fmt(read_bytes_s, fmt=".1f") + "/s" + + if write_bytes_s > self.max_write_bytes_s: + self.max_write_bytes_s = write_bytes_s + self.max_write_bytes_s_str = sizeof_fmt(write_bytes_s, fmt=".1f") + "/s" + + self.read_stream.add_value(read_bytes_s) + self.write_stream.add_value(write_bytes_s) + + self.last_io = io + + total_read_string = sizeof_fmt(io.read_bytes, sep=" ", fmt=".1f") + total_write_string = sizeof_fmt(io.write_bytes, sep=" ", fmt=".1f") + + self.down_box.renderable = "\n".join( + [ + f"{read_bytes_s_string}", + f"max {self.max_read_bytes_s_str}", + f"total {total_read_string}", + ] + ) + self.up_box.renderable = "\n".join( + [ + f"{write_bytes_s_string}", + f"max {self.max_write_bytes_s_str}", + f"total {total_write_string}", + ] + ) + + self.table.columns[0]._cells[0] = ( + "[green]" + "\n".join(self.read_stream.graph) + "[/]" + ) + self.table.columns[0]._cells[1] = ( + "[blue]" + "\n".join(self.write_stream.graph) + "[/]" + ) + + table = Table(box=None, expand=False, padding=(0, 1), show_header=True) + table.add_column("", justify="left", no_wrap=True) + table.add_column(Text("free", justify="left"), justify="right", no_wrap=True) + table.add_column(Text("used", justify="left"), justify="right", no_wrap=True) + table.add_column(Text("total", justify="left"), justify="right", no_wrap=True) + table.add_column("", justify="right", no_wrap=True) + + for mp, total in zip(self.mountpoints, self.total): + du = psutil.disk_usage(mp) + + style = None + if du.percent > 99: + style = "red reverse bold" + elif du.percent > 95: + style = "yellow" + + table.add_row( + f"[b]{mp}[/]", + sizeof_fmt(du.free, fmt=".1f"), + sizeof_fmt(du.used, fmt=".1f"), + total, + f"({du.percent:.1f}%)", + style=style, + ) + self.group.renderables[1] = table + + self.refresh() + + def render(self): + return self.panel + + async def on_resize(self, event): + self.read_stream.reset_width(event.width - 25) + self.write_stream.reset_width(event.width - 25) diff --git a/src/tiptop/_helpers.py b/src/tiptop/_helpers.py index 9f996d2..5a58475 100644 --- a/src/tiptop/_helpers.py +++ b/src/tiptop/_helpers.py @@ -1,5 +1,5 @@ # https://stackoverflow.com/a/1094933/353337 -def sizeof_fmt(num, suffix: str = "iB", sep=" ", fmt=".0f"): +def sizeof_fmt(num, fmt=".0f", suffix: str = "iB", sep=" "): assert num >= 0 for unit in ["B", "K", "M", "G", "T", "P", "E", "Z"]: # actually 1024, but be economical with the return string size: diff --git a/src/tiptop/_mem.py b/src/tiptop/_mem.py index c3bae8a..7c009ef 100644 --- a/src/tiptop/_mem.py +++ b/src/tiptop/_mem.py @@ -45,12 +45,14 @@ class Mem(Widget): mem_total_string = sizeof_fmt(self.mem_total_bytes, fmt=".2f") self.panel = Panel( self.table, - title=f"mem - {mem_total_string}", + title=f"[b]mem[/] - {mem_total_string}", title_align="left", - border_style="green", + # border_style="green", + border_style="white", box=box.SQUARE, ) + self.refresh_table() self.set_interval(2.0, self.refresh_table) def refresh_table(self): diff --git a/src/tiptop/_net.py b/src/tiptop/_net.py index b216444..6234b98 100644 --- a/src/tiptop/_net.py +++ b/src/tiptop/_net.py @@ -9,6 +9,7 @@ from rich.panel import Panel from rich.table import Table from textual.widget import Widget +from .__about__ import __version__ from ._helpers import sizeof_fmt from .braille_stream import BrailleStream @@ -50,6 +51,7 @@ def _autoselect_interface(): class Net(Widget): def __init__(self, interface: str | None = None): self.interface = _autoselect_interface() if interface is None else interface + self.tiptop_string = f"tiptop v{__version__}" super().__init__() def on_mount(self): @@ -79,10 +81,13 @@ class Net(Widget): self.group = Group(self.table, "", "") self.panel = Panel( self.group, - title=f"net - {self.interface}", - border_style="red", + title=f"[b]net[/] - {self.interface}", + # border_style="red", + border_style="white", title_align="left", box=box.SQUARE, + subtitle=self.tiptop_string, + subtitle_align="right", ) self.last_net = None diff --git a/src/tiptop/_procs_list.py b/src/tiptop/_procs_list.py index fd0a92e..8c69052 100644 --- a/src/tiptop/_procs_list.py +++ b/src/tiptop/_procs_list.py @@ -2,15 +2,14 @@ import psutil from rich import box from rich.panel import Panel from rich.table import Table +from rich.text import Text from textual.widget import Widget -from .__about__ import __version__ from ._helpers import sizeof_fmt class ProcsList(Widget): def on_mount(self): - self.tiptop_string = f"tiptop v{__version__}" self.max_num_procs = 100 self.collect_data() self.set_interval(6.0, self.collect_data) @@ -51,21 +50,34 @@ class ProcsList(Widget): show_header=True, header_style="bold", box=None, - padding=0, + padding=(0, 1), expand=True, ) - table.add_column("pid", min_width=6, no_wrap=True) - table.add_column("program", max_width=10, style="green", no_wrap=True) - table.add_column("args", max_width=20, no_wrap=True) - table.add_column("thr", width=3, style="green", no_wrap=True) + # set ration=1 on all columns that should be expanded + # <https://github.com/Textualize/rich/issues/2030> + table.add_column(Text("pid", justify="left"), no_wrap=True, justify="right") + table.add_column("program", style="green", no_wrap=True, ratio=1) + table.add_column("args", no_wrap=True, ratio=2) + table.add_column( + Text("thr", justify="left"), + style="green", + no_wrap=True, + justify="right", + ) table.add_column("user", no_wrap=True) - table.add_column("mem", style="green", no_wrap=True) - table.add_column("[u]cpu%[/]", width=5, no_wrap=True) + table.add_column( + Text("mem", justify="left"), style="green", no_wrap=True, justify="right" + ) + table.add_column( + Text("cpu%", style="u", justify="left"), + no_wrap=True, + justify="right", + ) for p in processes[: self.max_num_procs]: # Everything can be None here, see comment above pid = p.info["pid"] - pid = "" if pid is None else f"{pid:6d}" + pid = "" if pid is None else str(pid) # name = p.info["name"] if name is None: @@ -75,7 +87,7 @@ class ProcsList(Widget): cmdline = "" if cmdline is None else " ".join(p.info["cmdline"][1:]) # num_threads = p.info["num_threads"] - num_threads = "" if num_threads is None else f"{num_threads:3d}" + num_threads = "" if num_threads is None else str(num_threads) # username = p.info["username"] if username is None: @@ -87,7 +99,7 @@ class ProcsList(Widget): ) # cpu_percent = p.info["cpu_percent"] - cpu_percent = "" if cpu_percent is None else f"{cpu_percent:5.1f}" + cpu_percent = "" if cpu_percent is None else f"{cpu_percent:.1f}" table.add_row( pid, name, cmdline, num_threads, username, mem_info, cpu_percent ) @@ -97,11 +109,10 @@ class ProcsList(Widget): self.panel = Panel( table, - title=f"proc - {len(processes)} ({total_num_threads} thr), {num_sleep} slp", + title=f"[b]proc[/] - {len(processes)} ({total_num_threads} thr), {num_sleep} slp", title_align="left", - subtitle=self.tiptop_string, - subtitle_align="right", - border_style="cyan", + # border_style="cyan", + border_style="white", box=box.SQUARE, ) |