mirror of
https://github.com/XuehaiPan/nvitop.git
synced 2026-05-15 14:15:55 -06:00
feat(cli): add option --readonly to CLI (#214)
This commit is contained in:
parent
4e814c52a6
commit
8561956c12
11 changed files with 209 additions and 154 deletions
|
|
@ -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
|
||||
|
||||
|
|
|
|||
41
README.md
41
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 <kbd>Ctrl-c</kbd> / <kbd>T</kbd> / <kbd>K</kbd> keys to interrupt / terminate / kill a process. And it's recommended to *terminate* or *kill* a process in the **tree-view screen** (shortcut: <kbd>t</kbd>). 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 <kbd>Enter</kbd> / <kbd>Return</kbd> key . `nvitop` dynamically displays the process metrics with live graphs.
|
||||
|
||||
<p align="center">
|
||||
|
|
@ -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`<br>`plain` / `colorful`<br>`dark` / `light` | `auto,plain,dark` |
|
||||
| `NVITOP_MONITOR_MODE` | The default display mode (a comma-separated string) | `auto` / `full` / `compact`<br>`plain` / `colorful`<br>`dark` / `light`<br>`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
|
|||
| | |
|
||||
| `<Space>` | Tag/untag current process. |
|
||||
| `<Esc>` | Clear process selection. |
|
||||
| `<C-c>`<br>`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). |
|
||||
| `<C-c>`<br>`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. |
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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', '<Left>', '<S-Tab>')
|
||||
keymaps.alias('messagebox', '<Right>', '<Tab>')
|
||||
|
||||
# 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', '<C-c>', ('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():
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -295,35 +295,7 @@ class MainScreen(BaseSelectableScreen): # pylint: disable=too-many-instance-att
|
|||
keymaps.bind('main', '<Esc>', select_clear)
|
||||
keymaps.bind('main', '<Space>', 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',
|
||||
'<C-c>',
|
||||
partial(
|
||||
MessageBox.confirm_sending_signal_to_processes,
|
||||
signal='interrupt',
|
||||
screen=self,
|
||||
),
|
||||
)
|
||||
keymaps.alias('main', '<C-c>', 'I')
|
||||
MessageBox.register_signal_keybindings(self, keymap_name='main')
|
||||
|
||||
keymaps.bind('main', ',', order_previous)
|
||||
keymaps.alias('main', ',', '<')
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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', '<Esc>', select_clear)
|
||||
keymaps.bind('treeview', '<Space>', 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',
|
||||
'<C-c>',
|
||||
partial(
|
||||
MessageBox.confirm_sending_signal_to_processes,
|
||||
signal='interrupt',
|
||||
screen=self,
|
||||
),
|
||||
)
|
||||
keymaps.alias('treeview', '<C-c>', 'I')
|
||||
MessageBox.register_signal_keybindings(self, keymap_name='treeview')
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue