Skip to content

Commit

Permalink
Complete the Textual TUI implementation
Browse files Browse the repository at this point in the history
Signed-off-by: Matt Wozniski <mwozniski@bloomberg.net>
  • Loading branch information
godlygeek committed Nov 9, 2023
1 parent fdb7255 commit 525f2e5
Show file tree
Hide file tree
Showing 13 changed files with 2,624 additions and 1,420 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -177,3 +177,6 @@ node_modules/

# Vendored files
src/vendor/libbacktrace/install

# pytest-textual-snapshot
snapshot_report.html
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ repos:
- id: check-yaml
- id: end-of-file-fixer
- id: trailing-whitespace
exclude: "^[.]bumpversion[.]cfg"
exclude: "^([.]bumpversion[.]cfg|.*/__snapshots__/)"

- repo: https://github.com/pre-commit/pygrep-hooks
rev: v1.10.0
Expand Down
1 change: 1 addition & 0 deletions news/274.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Migrate the :doc:`live TUI <live>` to Textual. This provides a greatly improved user experience, including the ability to scroll to view rows that don't fit on the screen.
1 change: 1 addition & 0 deletions requirements-test.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ pytest
pytest-cov
ipython
setuptools; python_version >= '3.12'
pytest-textual-snapshot
3 changes: 2 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ def build_js_files(self):
"jinja2 >= 2.9",
"typing_extensions; python_version < '3.8.0'",
"rich >= 11.2.0",
"textual >= 0.23.0",
"textual >= 0.34.0",
]
docs_requires = [
"IPython",
Expand All @@ -117,6 +117,7 @@ def build_js_files(self):
"pytest-cov",
"ipython",
"setuptools; python_version >= '3.12'",
"pytest-textual-snapshot",
]

benchmark_requires = [
Expand Down
72 changes: 5 additions & 67 deletions src/memray/commands/live.py
Original file line number Diff line number Diff line change
@@ -1,75 +1,11 @@
import argparse
import sys
import termios
from contextlib import suppress

from rich.layout import Layout
from rich.live import Live
from typing import Optional

from memray import SocketReader
from memray._errors import MemrayCommandError
from memray.reporters.tui import TUIApp

KEYS = {
"ESC": "\x1b",
"CTRL_C": "\x03",
"LEFT": "\x1b\x5b\x44",
"RIGHT": "\x1b\x5b\x43",
"O": "o",
"T": "t",
"A": "a",
}


def _readchar() -> str: # pragma: no cover
"""Read a single character from standard input without echoing.
This function configures the current terminal and its standard
input to:
* Deactivate character echoing
* Deactivate canonical mode (see 'termios' man page).
Then, it reads a single character from standard input.
After the character has been read, it restores the previous configuration.
"""
if not sys.stdin.isatty():
return sys.stdin.read(1)
fd = sys.stdin.fileno()
old_settings = termios.tcgetattr(fd)
new_settings = termios.tcgetattr(fd)
new_settings[3] = new_settings[3] & ~termios.ECHO & ~termios.ICANON
termios.tcsetattr(fd, termios.TCSANOW, new_settings)
try:
char = sys.stdin.read(1)
finally:
termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
return char


def readkey() -> str: # pragma: no cover
"""Read a key press respecting ANSI Escape sequences.
A key-stroke can have:
1 character for normal keys: 'a', 'z', '9'...
2 characters for combinations with ALT: ALT+A, ...
3 characters for cursors: ->, <-, ...
4 characters for combinations with CTRL and ALT: CTRL+ALT+SUPR
"""
c1 = _readchar()
if not c1 or ord(c1) != 0x1B: # ESC
return c1
c2 = _readchar()
if ord(c2) != 0x5B: # [
return c1 + c2
c3 = _readchar()
if ord(c3) != 0x33: # 3 (one more char is needed)
return c1 + c2 + c3
c4 = _readchar()
return c1 + c2 + c3 + c4


class LiveCommand:
"""Remotely monitor allocations in a text-based interface"""
Expand All @@ -86,8 +22,10 @@ def run(self, args: argparse.Namespace, parser: argparse.ArgumentParser) -> None
with suppress(KeyboardInterrupt):
self.start_live_interface(args.port)

def start_live_interface(self, port: int) -> None:
def start_live_interface(
self, port: int, cmdline_override: Optional[str] = None
) -> None:
if port >= 2**16 or port <= 0:
raise MemrayCommandError(f"Invalid port: {port}", exit_code=1)
with SocketReader(port=port) as reader:
TUIApp(reader).run()
TUIApp(reader, cmdline_override=cmdline_override).run()
4 changes: 3 additions & 1 deletion src/memray/commands/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,9 @@ def _run_child_process_and_attach(args: argparse.Namespace) -> None:
text=True,
) as process:
try:
LiveCommand().start_live_interface(port)
LiveCommand().start_live_interface(
port, cmdline_override=" ".join(sys.argv)
)
except (Exception, KeyboardInterrupt) as error:
process.terminate()
raise error from None
Expand Down
112 changes: 69 additions & 43 deletions src/memray/reporters/tui.css
Original file line number Diff line number Diff line change
@@ -1,78 +1,104 @@
TUI {
layout: grid;
grid-size: 1 6;
grid-rows: 2fr 7fr 3fr 70% 1fr 1fr;
padding: 0 1;
layout: vertical;
}

/* Head */
#head {
layout: horizontal;
layout: horizontal;
height: 1;
}

#head_title {
width: 30%;
#head_time_display {
width: 30;
text-align: right;
dock: right;
}

#head_time_display {
width: 70%;
text-align: right;
Header {
height: 7;
}

.narrow Header {
height: 11;
}

/* Header */
#header_container {
height: 100%;
layout: grid;
grid-size: 3 1;
grid-columns: 2fr 5fr 3fr;
text-align: left;
layout: horizontal;
}

.narrow #header_container {
layout: vertical;
}

#header_metadata {
height: auto;
layout: horizontal;
padding: 0 0 0 4;
layout: vertical;
width: 100%;
height: 7;
border: blank;
border-title-align: right;
border-title-color: $success-lighten-1;
overflow: auto auto;
}

#header_metadata_col_1 {
height: auto;
width: auto;
.narrow #header_metadata {
border: none;
height: 5;
}

#header_metadata_col_2 {
height: auto;
width: auto;
margin-left: 5;
#header_metadata_grid {
layout: grid;
height: 3;
width: auto;
grid-size: 2;
grid-columns: auto auto;
grid-gutter: 0 3;
overflow: hidden hidden;
}

/* Heap size */
#heap_size {
layout: horizontal;
#header_metadata > #status_message {
column-span: 2;
width: auto;
height: 1;
}

#current_memory_size {
width: 30%;
MemoryGraph {
dock: right;
border: round $success-lighten-1;
box-sizing: content-box;
height: 4;
max-width: 50;
}

#max_memory_seen {
width: 70%;
text-align: right;
#memory_graph_container {
dock: right;
box-sizing: content-box;
width: 30%;
max-width: 52;
}

#progress_bar {
height: 1fr;
.narrow #memory_graph_container {
dock: bottom;
box-sizing: border-box;
width: 100%;
max-width: 100%;
height: 6;
}

/* Override default footer styling */
Footer {
background: transparent;
background: transparent;
}

Footer > .footer--key {
background: rgb(30, 144, 255);
background: rgb(30, 144, 255);
}

AllocationTable {
height: 1fr;
}

.narrow AllocationTable {
height: 100%;
}

/* Override default table styling */
DataTable {
border: solid white;
AllocationTable > ScrollView {
height: 100%;
}
Loading

0 comments on commit 525f2e5

Please sign in to comment.