diff --git a/CHANGELOG.md b/CHANGELOG.md index 76ea513..301c790 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Add `nvidia-ml-py` 13.595.45 to support list. - Add support for open kernel-module driver packages (e.g., `nvidia-driver-595-open`) in `install-nvidia-driver.sh` with new `--proprietary` and `--open` flags by [@XuehaiPan](https://github.com/XuehaiPan). - Add TLS and mutual TLS (mTLS) support for `nvitop-exporter` via new `--certfile`, `--keyfile`, `--client-cafile`, `--client-capath`, and `--client-auth-required` CLI flags by [@XuehaiPan](https://github.com/XuehaiPan) in [#213](https://github.com/XuehaiPan/nvitop/pull/213). Issued by [@StefanSander3](https://github.com/StefanSander3) in [#131](https://github.com/XuehaiPan/nvitop/issues/131). +- Add `--readonly` CLI flag (and equivalent `NVITOP_MONITOR_MODE="readonly"` env token) for monitor mode that disables all process-mutating shortcuts (`Ctrl-c` / `T` / `K` / `I` / `k`) by [@XuehaiPan](https://github.com/XuehaiPan). ### Changed diff --git a/README.md b/README.md index dd04997..78bb917 100644 --- a/README.md +++ b/README.md @@ -292,6 +292,8 @@ You can configure the default monitor mode with the `NVITOP_MONITOR_MODE` enviro In monitor mode, you can use Ctrl-c / T / K keys to interrupt / terminate / kill a process. And it's recommended to *terminate* or *kill* a process in the **tree-view screen** (shortcut: t). For normal users, `nvitop` will shallow other users' processes (in low-intensity colors). For **system administrators**, you can use `sudo nvitop` to terminate other users' processes. +To run `nvitop` as a viewer only and disable all process-mutating shortcuts, pass `--readonly` (or set `NVITOP_MONITOR_MODE="readonly"`). The signal keys above become no-ops, the on-screen "Press ^C(INT)/T(TERM)/K(KILL) to send signals" hint is hidden, and the corresponding rows in the help screen are dimmed. Use this when sharing a session over SSH, demoing on a multi-tenant box, or wrapping `nvitop` in a non-admin alias. + Also, to enter the process metrics screen, select a process and then press the Enter / Return key . `nvitop` dynamically displays the process metrics with live graphs.

@@ -343,11 +345,11 @@ Type `nvitop --help` for more command options: ```text usage: nvitop [--help] [--version] [--once | --monitor [{auto,full,compact}]] - [--interval SEC] [--ascii] [--colorful] [--force-color] [--light] - [--gpu-util-thresh th1 th2] [--mem-util-thresh th1 th2] - [--only INDEX [INDEX ...]] [--only-visible] - [--compute] [--only-compute] [--graphics] [--only-graphics] - [--user [USERNAME ...]] [--pid PID [PID ...]] + [--interval SEC] [--no-unicode] [--readonly] [--colorful] + [--force-color] [--light] [--gpu-util-thresh th1 th2] + [--mem-util-thresh th1 th2] [--only INDEX [INDEX ...]] + [--only-visible] [--compute] [--only-compute] [--graphics] + [--only-graphics] [--user [USERNAME ...]] [--pid PID [PID ...]] An interactive NVIDIA-GPU process viewer. @@ -355,19 +357,22 @@ options: --help, -h Show this help message and exit. --version, -V Show nvitop's version number and exit. --once, -1 Report query data only once. - --monitor [{auto,full,compact}], -m [{auto,full,compact}] + --monitor, -m [{auto,full,compact}] Run as a resource monitor. Continuously report query data and handle user inputs. If the argument is omitted, the value from `NVITOP_MONITOR_MODE` will be used. (default fallback mode: auto) --interval SEC Process status update interval in seconds. (default: 2) - --ascii, --no-unicode, -U + --no-unicode, --ascii, -U Use ASCII characters only, which is useful for terminals without Unicode support. + --readonly Disable all system and process changing features (e.g., terminating processes). + Set variable `NVITOP_MONITOR_MODE="readonly"` for convenience. coloring: - --colorful Use gradient colors to get spectrum-like bar charts. This option is only available - when the terminal supports 256 colors. You may need to set environment variable - `TERM="xterm-256color"`. Note that the terminal multiplexer, such as `tmux`, may - override the `TERM` variable. + --colorful Use gradient colors to get spectrum-like bar charts. + Set variable `NVITOP_MONITOR_MODE="colorful"` for convenience. + This option is only available when the terminal supports 256 colors. + You may need to set environment variable `TERM="xterm-256color"`. Note that the + terminal multiplexer, such as `tmux`, may override the `TERM` variable. --force-color Force colorize even when `stdout` is not a TTY terminal. --light Tweak visual results for light theme terminals in monitor mode. Set variable `NVITOP_MONITOR_MODE="light"` on light terminals for convenience. @@ -381,7 +386,7 @@ coloring: ( 1 <= th1 < th2 <= 99, defaults: 10 80 ) device filtering: - --only INDEX [INDEX ...], -o INDEX [INDEX ...] + --only, -o INDEX [INDEX ...] Only show the specified devices, suppress option `--only-visible`. --only-visible, -ov Only show devices in the `CUDA_VISIBLE_DEVICES` environment variable. @@ -390,9 +395,9 @@ process filtering: --only-compute, -C Only show GPU processes exactly with the compute context. (type: 'C' only) --graphics, -g Only show GPU processes with the graphics context. (type: 'G' or 'C+G') --only-graphics, -G Only show GPU processes exactly with the graphics context. (type: 'G' only) - --user [USERNAME ...], -u [USERNAME ...] + --user, -u [USERNAME ...] Only show processes of the given users (or `$USER` for no argument). - --pid PID [PID ...], -p PID [PID ...] + --pid, -p PID [PID ...] Only show processes of the given PIDs. ``` @@ -400,7 +405,7 @@ process filtering: | Name | Description | Valid Values | Default Value | | -------------------------------------- | --------------------------------------------------- | ----------------------------------------------------------------------- | ----------------- | -| `NVITOP_MONITOR_MODE` | The default display mode (a comma-separated string) | `auto` / `full` / `compact`
`plain` / `colorful`
`dark` / `light` | `auto,plain,dark` | +| `NVITOP_MONITOR_MODE` | The default display mode (a comma-separated string) | `auto` / `full` / `compact`
`plain` / `colorful`
`dark` / `light`
`readonly` (disables process-mutating shortcuts) | `auto,plain,dark` | | `NVITOP_GPU_UTILIZATION_THRESHOLDS` | Thresholds of GPU utilization | `10,75` , `1,99`, ... | `10,75` | | `NVITOP_MEMORY_UTILIZATION_THRESHOLDS` | Thresholds of GPU memory percent | `10,80` , `1,99`, ... | `10,80` | | `LOGLEVEL` | Log level for log messages | `DEBUG` , `INFO`, `WARNING`, ... | `WARNING` | @@ -450,9 +455,9 @@ echo 'set -gx NVITOP_MONITOR_MODE "full"' >> ~/.config/fish/config.fish | | | | `` | Tag/untag current process. | | `` | Clear process selection. | -| ``
`I` | Send `signal.SIGINT` to the selected process (interrupt). | -| `T` | Send `signal.SIGTERM` to the selected process (terminate). | -| `K` | Send `signal.SIGKILL` to the selected process (kill). | +| ``
`I` | Send `signal.SIGINT` to the selected process (interrupt). *(disabled under `--readonly`)* | +| `T` | Send `signal.SIGTERM` to the selected process (terminate). *(disabled under `--readonly`)* | +| `K` | Send `signal.SIGKILL` to the selected process (kill). *(disabled under `--readonly`)* | | | | | `e` | Show process environment. | | `t` | Toggle tree-view screen. | diff --git a/nvitop/cli.py b/nvitop/cli.py index 5a8da80..31c296d 100644 --- a/nvitop/cli.py +++ b/nvitop/cli.py @@ -102,6 +102,15 @@ def parse_arguments() -> argparse.Namespace: action='store_true', help='Use ASCII characters only, which is useful for terminals without Unicode support.', ) + parser.add_argument( + '--readonly', + dest='readonly', + action='store_true', + help=( + 'Disable all system and process changing features (e.g., terminating processes).\n' + 'Set variable `NVITOP_MONITOR_MODE="readonly"` for convenience.' + ), + ) coloring = parser.add_argument_group('coloring') coloring.add_argument( @@ -109,10 +118,11 @@ def parse_arguments() -> argparse.Namespace: dest='colorful', action='store_true', help=( - 'Use gradient colors to get spectrum-like bar charts. This option is only available\n' - 'when the terminal supports 256 colors. You may need to set environment variable\n' - '`TERM="xterm-256color"`. Note that the terminal multiplexer, such as `tmux`, may\n' - 'override the `TERM` variable.' + 'Use gradient colors to get spectrum-like bar charts.\n' + 'Set variable `NVITOP_MONITOR_MODE="colorful"` for convenience.\n' + 'This option is only available when the terminal supports 256 colors.\n' + 'You may need to set environment variable `TERM="xterm-256color"`. Note that the\n' + 'terminal multiplexer, such as `tmux`, may override the `TERM` variable.' ), ) coloring.add_argument( @@ -234,6 +244,8 @@ def parse_arguments() -> argparse.Namespace: args.colorful = 'colorful' in NVITOP_MONITOR_MODE and 'plain' not in NVITOP_MONITOR_MODE if not args.light: args.light = 'light' in NVITOP_MONITOR_MODE and 'dark' not in NVITOP_MONITOR_MODE + if not args.readonly: + args.readonly = 'readonly' in NVITOP_MONITOR_MODE if args.user is not None and len(args.user) == 0: args.user.append(USERNAME) if args.gpu_util_thresh is None: @@ -355,6 +367,7 @@ def main() -> int: no_unicode=args.no_unicode, mode=args.monitor, interval=args.interval, + readonly=args.readonly, win=win, ) tui.loop() @@ -364,7 +377,7 @@ def main() -> int: messages.append(f'ERROR: Failed to initialize `curses` ({ex})') if tui is None: - tui = TUI(devices, filters, no_unicode=args.no_unicode) + tui = TUI(devices, filters, no_unicode=args.no_unicode, readonly=args.readonly) if not sys.stdout.isatty(): parent = HostProcess().parent() if parent is not None: diff --git a/nvitop/tui/library/__init__.py b/nvitop/tui/library/__init__.py index 4227a73..ea70712 100644 --- a/nvitop/tui/library/__init__.py +++ b/nvitop/tui/library/__init__.py @@ -18,7 +18,12 @@ from nvitop.tui.library.keybinding import ( normalize_keybinding, ) from nvitop.tui.library.libcurses import libcurses, setlocale_utf8 -from nvitop.tui.library.messagebox import MessageBox +from nvitop.tui.library.messagebox import ( + SIGNAL_HINT_BLANK, + SIGNAL_HINT_KEY_SPANS, + SIGNAL_HINT_TEXT, + MessageBox, +) from nvitop.tui.library.mouse import MouseEvent from nvitop.tui.library.process import GpuProcess, HostProcess from nvitop.tui.library.selection import Selection @@ -58,6 +63,9 @@ __all__ = [ 'NA', 'PASSIVE_ACTION', 'QUANT_KEY', + 'SIGNAL_HINT_BLANK', + 'SIGNAL_HINT_KEY_SPANS', + 'SIGNAL_HINT_TEXT', 'SPECIAL_KEYS', 'USERNAME', 'USER_CONTEXT', diff --git a/nvitop/tui/library/messagebox.py b/nvitop/tui/library/messagebox.py index 13d0c83..c7306a4 100644 --- a/nvitop/tui/library/messagebox.py +++ b/nvitop/tui/library/messagebox.py @@ -11,7 +11,7 @@ import string import threading import time from functools import partial -from typing import TYPE_CHECKING, Literal +from typing import TYPE_CHECKING, ClassVar, Literal from nvitop.tui.library import host from nvitop.tui.library.displayable import Displayable @@ -28,11 +28,30 @@ if TYPE_CHECKING: from nvitop.tui.tui import TUI -__all__ = ['MessageBox'] +__all__ = ['SIGNAL_HINT_BLANK', 'SIGNAL_HINT_KEY_SPANS', 'SIGNAL_HINT_TEXT', 'MessageBox'] DIGITS: frozenset[str] = frozenset(string.digits) +SignalHintSpanKind = Literal['key', 'label'] + +# Single source of truth for the on-screen hint that surfaces the kill/terminate/interrupt +# bindings. The process panel and the tree-view screen both render this string with +# screen-specific coloring; they share these positions and widths so the two layouts stay in +# lockstep when the wording is updated. +SIGNAL_HINT_TEXT: str = '(Press ^C(INT)/T(TERM)/K(KILL) to send signals)' +SIGNAL_HINT_BLANK: str = ' ' * len(SIGNAL_HINT_TEXT) +# (kind, offset_within_text, width) — ``kind`` is ``'key'`` for the shortcut tokens +# (``^C``, ``T``, ``K``) and ``'label'`` for the signal names (``INT``, ``TERM``, ``KILL``). +SIGNAL_HINT_KEY_SPANS: tuple[tuple[SignalHintSpanKind, int, int], ...] = ( + ('key', 7, 2), + ('label', 10, 3), + ('key', 15, 1), + ('label', 17, 4), + ('key', 23, 1), + ('label', 25, 4), +) + class MessageBox(Displayable): # pylint: disable=too-many-instance-attributes class Option: # pylint: disable=too-few-public-methods @@ -302,12 +321,45 @@ class MessageBox(Displayable): # pylint: disable=too-many-instance-attributes keymaps.alias('messagebox', '', '') keymaps.alias('messagebox', '', '') + # Keybindings that route into `confirm_sending_signal_to_processes`. Centralized here so + # every selectable screen can register the same bindings without duplicating the + # `signal -> primary key + aliases` mapping. + _SIGNAL_KEYBINDINGS: ClassVar[ + tuple[tuple[Literal['terminate', 'kill', 'interrupt'], str, tuple[str, ...]], ...] + ] = ( + ('terminate', 'T', ()), + ('kill', 'K', ('k',)), + ('interrupt', '', ('I',)), + ) + + @classmethod + def register_signal_keybindings(cls, screen: BaseSelectableScreen, keymap_name: str) -> None: + """Bind the kill/terminate/interrupt keys for ``screen`` under ``keymap_name``. + + The bindings are skipped entirely when the selection is read-only so the + ``--readonly`` contract is honored at the keybinding layer in addition to the + :meth:`Selection` boundary. + """ + if screen.selection.readonly: + return + keymaps = screen.root.keymaps + for signal, key, aliases in cls._SIGNAL_KEYBINDINGS: + keymaps.bind( + keymap_name, + key, + partial(cls.confirm_sending_signal_to_processes, signal=signal, screen=screen), + ) + for alias in aliases: + keymaps.alias(keymap_name, key, alias) + @staticmethod def confirm_sending_signal_to_processes( signal: Literal['terminate', 'kill', 'interrupt'], screen: BaseSelectableScreen, ) -> None: assert signal in ('terminate', 'kill', 'interrupt') + if screen.selection.readonly: + return default = {'terminate': 0, 'kill': 1, 'interrupt': 2}.get(signal) processes = [] for process in screen.selection.processes(): diff --git a/nvitop/tui/library/selection.py b/nvitop/tui/library/selection.py index 50bf1fc..2c22801 100644 --- a/nvitop/tui/library/selection.py +++ b/nvitop/tui/library/selection.py @@ -5,9 +5,10 @@ from __future__ import annotations +import functools import signal import time -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, TypeVar from weakref import WeakValueDictionary from nvitop.api import NA, Snapshot @@ -25,7 +26,27 @@ if TYPE_CHECKING: __all__ = ['Selection'] -class Selection: # pylint: disable=too-many-instance-attributes +_F = TypeVar('_F', bound='Callable[..., None]') + + +def _writable_only(method: _F) -> _F: + """Defense-in-depth: skip the call when the bound root is read-only. + + Closes the bypass where a dialog callback captured before the flag was flipped, or + any caller holding a reference to one of these methods, would otherwise route + around the keybinding and `MessageBox` guards. + """ + + @functools.wraps(method) + def wrapper(self: Selection, *args: object, **kwargs: object) -> None: + if self.readonly: + return + method(self, *args, **kwargs) + + return wrapper # type: ignore[return-value] + + +class Selection: # pylint: disable=too-many-instance-attributes,too-many-public-methods def __init__(self, displayable: Displayable) -> None: self.tagged: WeakValueDictionary[int, GpuProcess | HostProcess] = WeakValueDictionary() self.displayable: Displayable = displayable @@ -114,6 +135,15 @@ class Selection: # pylint: disable=too-many-instance-attributes return (self.process,) # type: ignore[return-value] return () + def has_actionable_processes(self) -> bool: + """Whether the selection currently identifies at least one process to send signals to. + + Mirrors the visibility predicate of the on-screen + ``Press ^C(INT)/T(TERM)/K(KILL) to send signals`` hint: ``True`` if any process is + tagged, or if the focused process is owned by the user and within the visible window. + """ + return len(self.tagged) > 0 or (self.owned() and self.within_window) + def foreach(self, func: Callable[[GpuProcess | HostProcess], None]) -> None: flag = False for process in self.processes(): @@ -128,9 +158,15 @@ class Selection: # pylint: disable=too-many-instance-attributes time.sleep(0.25) self.clear() + @property + def readonly(self) -> bool: + return bool(self.displayable.root.readonly) # type: ignore[union-attr] + + @_writable_only def send_signal(self, sig: int) -> None: self.foreach(lambda process: process.send_signal(sig)) + @_writable_only def interrupt(self) -> None: try: # pylint: disable-next=no-member @@ -138,9 +174,11 @@ class Selection: # pylint: disable=too-many-instance-attributes except SystemError: pass + @_writable_only def terminate(self) -> None: self.foreach(lambda process: process.terminate()) + @_writable_only def kill(self) -> None: self.foreach(lambda process: process.kill()) diff --git a/nvitop/tui/screens/help.py b/nvitop/tui/screens/help.py index a4afc5f..4d781f1 100644 --- a/nvitop/tui/screens/help.py +++ b/nvitop/tui/screens/help.py @@ -85,6 +85,11 @@ class HelpScreen(BaseScreen): # pylint: disable=too-many-instance-attributes **dict.fromkeys(range(24, 29), ('blue', 'blue')), 29: ('magenta', 'magenta'), } + # Rows whose right-hand action column is colored red are exactly the + # kill/terminate/interrupt keybindings — the same set ``--readonly`` disables. + self.readonly_disabled_rows: frozenset[int] = frozenset( + row for row, (_, right) in self.color_matrix.items() if right == 'red' + ) self.x, self.y = root.x, root.y self.width: int = max(map(len, self.infos)) @@ -119,9 +124,13 @@ class HelpScreen(BaseScreen): # pylint: disable=too-many-instance-attributes for dy, (left, right) in self.color_matrix.items(): if left is not None: self.color_at(self.y + dy, self.x, width=12, fg=left, attr='bold') - if right is not None: + if right is not None and not (self.root.readonly and dy in self.readonly_disabled_rows): self.color_at(self.y + dy, self.x + 39, width=13, fg=right, attr='bold') + if self.root.readonly: + for dy in self.readonly_disabled_rows: + self.color_at(self.y + dy, self.x + 39, width=self.width - 39, attr='dim') + def press(self, key: int) -> bool: self.root.keymaps.use_keymap('help') return self.root.press(key) diff --git a/nvitop/tui/screens/main/__init__.py b/nvitop/tui/screens/main/__init__.py index fa6d54b..89cb286 100644 --- a/nvitop/tui/screens/main/__init__.py +++ b/nvitop/tui/screens/main/__init__.py @@ -295,35 +295,7 @@ class MainScreen(BaseSelectableScreen): # pylint: disable=too-many-instance-att keymaps.bind('main', '', select_clear) keymaps.bind('main', '', tag) - keymaps.bind( - 'main', - 'T', - partial( - MessageBox.confirm_sending_signal_to_processes, - signal='terminate', - screen=self, - ), - ) - keymaps.bind( - 'main', - 'K', - partial( - MessageBox.confirm_sending_signal_to_processes, - signal='kill', - screen=self, - ), - ) - keymaps.alias('main', 'K', 'k') - keymaps.bind( - 'main', - '', - partial( - MessageBox.confirm_sending_signal_to_processes, - signal='interrupt', - screen=self, - ), - ) - keymaps.alias('main', '', 'I') + MessageBox.register_signal_keybindings(self, keymap_name='main') keymaps.bind('main', ',', order_previous) keymaps.alias('main', ',', '<') diff --git a/nvitop/tui/screens/main/panels/process.py b/nvitop/tui/screens/main/panels/process.py index d6c4e75..d1c2da5 100644 --- a/nvitop/tui/screens/main/panels/process.py +++ b/nvitop/tui/screens/main/panels/process.py @@ -18,6 +18,9 @@ from nvitop.tui.library import ( IS_WINDOWS, IS_WSL, LARGE_INTEGER, + SIGNAL_HINT_BLANK, + SIGNAL_HINT_KEY_SPANS, + SIGNAL_HINT_TEXT, USER_CONTEXT, USERNAME, Device, @@ -632,19 +635,25 @@ class ProcessPanel(BaseSelectablePanel): # pylint: disable=too-many-instance-at message = ' No running processes found{} '.format(' (in WSL)' if IS_WSL else '') self.addstr(self.y + 5, self.x, f'│ {message.ljust(self.width - 4)} │') - text_offset = self.x + self.width - 47 - if len(self.selection.tagged) > 0 or ( - self.selection.owned() and self.selection.within_window - ): - self.addstr(self.y, text_offset, '(Press ^C(INT)/T(TERM)/K(KILL) to send signals)') - self.color_at(self.y, text_offset + 7, width=2, fg='magenta', attr='bold | italic') - self.color_at(self.y, text_offset + 10, width=3, fg='red', attr='bold') - self.color_at(self.y, text_offset + 15, width=1, fg='magenta', attr='bold | italic') - self.color_at(self.y, text_offset + 17, width=4, fg='red', attr='bold') - self.color_at(self.y, text_offset + 23, width=1, fg='magenta', attr='bold | italic') - self.color_at(self.y, text_offset + 25, width=4, fg='red', attr='bold') - else: - self.addstr(self.y, text_offset, ' ' * 47) + if self.root.readonly: + return + text_offset = self.x + self.width - len(SIGNAL_HINT_TEXT) + if not self.selection.has_actionable_processes(): + self.addstr(self.y, text_offset, SIGNAL_HINT_BLANK) + return + + self.addstr(self.y, text_offset, SIGNAL_HINT_TEXT) + for kind, dx, width in SIGNAL_HINT_KEY_SPANS: + if kind == 'key': + self.color_at( + self.y, + text_offset + dx, + width=width, + fg='magenta', + attr='bold | italic', + ) + else: + self.color_at(self.y, text_offset + dx, width=width, fg='red', attr='bold') def finalize(self) -> None: self.y_mouse = None diff --git a/nvitop/tui/screens/treeview.py b/nvitop/tui/screens/treeview.py index e04c9f2..40f2871 100644 --- a/nvitop/tui/screens/treeview.py +++ b/nvitop/tui/screens/treeview.py @@ -16,6 +16,8 @@ from nvitop.tui.library import ( IS_SUPERUSER, IS_WSL, NA, + SIGNAL_HINT_KEY_SPANS, + SIGNAL_HINT_TEXT, USERNAME, Device, GpuProcess, @@ -395,7 +397,7 @@ class TreeViewScreen(BaseSelectableScreen): # pylint: disable=too-many-instance super().poke() - def draw(self) -> None: # pylint: disable=too-many-statements,too-many-locals + def draw(self) -> None: # pylint: disable=too-many-statements,too-many-locals,too-many-branches self.color_reset() pid_width = max(3, max((len(str(process.pid)) for process in self.snapshots), default=3)) @@ -510,60 +512,32 @@ class TreeViewScreen(BaseSelectableScreen): # pylint: disable=too-many-instance if not hint: self.selection.clear() + if self.root.readonly or not self.selection.has_actionable_processes(): + return + self.color(fg='cyan', attr='bold | reverse') - text_offset = self.x + self.width - 47 - if len(self.selection.tagged) > 0 or ( - self.selection.owned() and self.selection.within_window - ): - self.addstr(self.y, text_offset - 1, ' (Press ^C(INT)/T(TERM)/K(KILL) to send signals)') - self.color_at( - self.y, - text_offset + 7, - width=2, - fg='cyan', - bg='yellow', - attr='bold | italic | reverse', - ) - self.color_at( - self.y, - text_offset + 10, - width=3, - fg='cyan', - bg='red', - attr='bold | reverse', - ) - self.color_at( - self.y, - text_offset + 15, - width=1, - fg='cyan', - bg='yellow', - attr='bold | italic | reverse', - ) - self.color_at( - self.y, - text_offset + 17, - width=4, - fg='cyan', - bg='red', - attr='bold | reverse', - ) - self.color_at( - self.y, - text_offset + 23, - width=1, - fg='cyan', - bg='yellow', - attr='bold | italic | reverse', - ) - self.color_at( - self.y, - text_offset + 25, - width=4, - fg='cyan', - bg='red', - attr='bold | reverse', - ) + text_offset = self.x + self.width - len(SIGNAL_HINT_TEXT) + # Leading space lets the cell share the row-above's reverse-highlighted background. + self.addstr(self.y, text_offset - 1, ' ' + SIGNAL_HINT_TEXT) + for kind, dx, width in SIGNAL_HINT_KEY_SPANS: + if kind == 'key': + self.color_at( + self.y, + text_offset + dx, + width=width, + fg='cyan', + bg='yellow', + attr='bold | italic | reverse', + ) + else: + self.color_at( + self.y, + text_offset + dx, + width=width, + fg='cyan', + bg='red', + attr='bold | reverse', + ) def finalize(self) -> None: self.y_mouse = None @@ -632,32 +606,4 @@ class TreeViewScreen(BaseSelectableScreen): # pylint: disable=too-many-instance keymaps.bind('treeview', '', select_clear) keymaps.bind('treeview', '', tag) - keymaps.bind( - 'treeview', - 'T', - partial( - MessageBox.confirm_sending_signal_to_processes, - signal='terminate', - screen=self, - ), - ) - keymaps.bind( - 'treeview', - 'K', - partial( - MessageBox.confirm_sending_signal_to_processes, - signal='kill', - screen=self, - ), - ) - keymaps.alias('treeview', 'K', 'k') - keymaps.bind( - 'treeview', - '', - partial( - MessageBox.confirm_sending_signal_to_processes, - signal='interrupt', - screen=self, - ), - ) - keymaps.alias('treeview', '', 'I') + MessageBox.register_signal_keybindings(self, keymap_name='treeview') diff --git a/nvitop/tui/tui.py b/nvitop/tui/tui.py index 932e1d0..6748046 100644 --- a/nvitop/tui/tui.py +++ b/nvitop/tui/tui.py @@ -51,6 +51,7 @@ class TUI(DisplayableContainer[Union[BaseScreen, MessageBox]]): # pylint: disab no_unicode: bool = False, mode: MonitorMode = 'auto', interval: float | None = None, + readonly: bool = False, win: curses.window | None = None, ) -> None: super().__init__(win, root=self) @@ -61,6 +62,7 @@ class TUI(DisplayableContainer[Union[BaseScreen, MessageBox]]): # pylint: disab self.termsize: tuple[int, int] | None = None self.no_unicode: bool = no_unicode + self.readonly: bool = readonly self.devices: list[Device] = devices self.device_count: int = len(self.devices)