import shutil from textual import on from textual.app import ComposeResult, App from textual.widgets import Footer, Header, Button, SelectionList from textual.widgets.selection_list import Selection from textual.screen import ModalScreen # Operating system commands are hardcoded OS_COMMANDS = { "LSHW": ["lshw", "-json", "-sanitize", "-notime", "-quiet"], "LSCPU": ["lscpu", "--all", "--extended", "--json"], "LSMEM": ["lsmem", "--json", "--all", "--output-all"], "NUMASTAT": ["numastat", "-z"] }
class LogScreen(ModalScreen): # ... Code of the full separate screen omitted, will be explained next def __init__(self, name = None, ident = None, classes = None, selections = None): super().__init__(name, ident, classes) pass
class OsApp(App): BINDINGS = [ ("q", "quit_app", "Quit"), ] CSS_PATH = "os_app.tcss" ENABLE_COMMAND_PALETTE = False # Do not need the command palette
def action_quit_app(self): self.exit(0)
def compose(self) -> ComposeResult: # Create a list of commands, valid commands are assumed to be on the PATH variable. selections = [Selection(name.title(), ' '.join(cmd), True) for name, cmd in OS_COMMANDS.items() if shutil.which(cmd[0].strip())] yield Header(show_clock=False) sel_list = SelectionList(*selections, id='cmds') sel_list.tooltip = "Select one more more command to execute" yield sel_list yield Button(f"Execute {len(selections)} commands", id="exec", variant="primary") yield Footer()
import asyncio from typing import List from textual import on, work from textual.reactive import reactive from textual.screen import ModalScreen from textual.widgets import Button, Label, Log from textual.worker import Worker from textual.app import ComposeResult
#!/usr/bin/env python """ Author: Jose Vicente Nunez """ from typing import Any, List
from rich.style import Style from textual import on from textual.app import ComposeResult, App from textual.command import Provider from textual.screen import ModalScreen, Screen from textual.widgets import DataTable, Footer, Header
class DetailScreen(ModalScreen): ENABLE_COMMAND_PALETTE = False CSS_PATH = "details_screen.tcss"
def __init__( self, name: str | None = None, ident: str | None = None, classes: str | None = None, row: List[Any] | None = None, ): super().__init__(name, ident, classes) # Rest of screen code will be show later
class CustomCommand(Provider):
def __init__(self, screen: Screen[Any], match_style: Style | None = None): super().__init__(screen, match_style) self.table = None # Rest of provider code will be show later
class CompetitorsApp(App): BINDINGS = [ ("q", "quit_app", "Quit"), ] CSS_PATH = "competitors_app.tcss" # Enable the command palette, to add our custom filter commands ENABLE_COMMAND_PALETTE = True # Add the default commands and the TablePopulateProvider to get a row directly by name COMMANDS = App.COMMANDS | {CustomCommand}
def on_mount(self) -> None: table = self.get_widget_by_id(f'competitors_table', expect_type=DataTable) columns = [x.title() for x in MY_DATA[0]] table.add_columns(*columns) table.add_rows(MY_DATA[1:]) table.loading = False table.tooltip = "Select a row to get more details"
from typing import Any, List from textual import on from textual.app import ComposeResult from textual.screen import ModalScreen from textual.widgets import Button, MarkdownViewer
from functools import partial from typing import Any, List from rich.style import Style from textual.command import Provider, Hit from textual.screen import ModalScreen, Screen from textual.widgets import DataTable from textual.app import App
my_app.log.info(f"Got query: {query}") for row_key in self.table.rows: row = self.table.get_row(row_key) my_app.log.info(f"Searching {row}") searchable = row[1] score = matcher.match(searchable) if score > 0: runner_detail = DetailScreen(row=row) yield Hit( score, matcher.highlight(f"{searchable}"), partial(my_app.show_detail, runner_detail), help=f"Show details about {searchable}" )
class DetailScreen(ModalScreen): def __init__( self, name: str | None = None, ident: str | None = None, classes: str | None = None, row: List[Any] | None = None, ): super().__init__(name, ident, classes) # Code of this class explained on the previous section
class CompetitorsApp(App): # Add the default commands and the TablePopulateProvider to get a row directly by name COMMANDS = App.COMMANDS | {CustomCommand} # Most of the code shown before, only displaying relevant code def show_detail(self, detailScreen: DetailScreen): self.push_screen(detailScreen)
. ~/virtualenv/Textualize/bin/activate textual run --dev ./kodegeek_textualize/log_scroller.py
在运行控制台的终端中,你可以看到实时的事件和消息输出:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
▌Textual Development Console v0.46.0 ▌Run a Textual app with textual run --dev my_app.py to connect. ▌Press Ctrl+C to quit. ─────────────────────────────────────────────────────────────────────────────── Client '127.0.0.1' connected ──────────────────────────────────────────────────────────────────────────────── [20:29:43] SYSTEM app.py:2188 Connected to devtools ( ws://127.0.0.1:8081 ) [20:29:43] SYSTEM app.py:2192 --- [20:29:43] SYSTEM app.py:2194 driver=<class 'textual.drivers.linux_driver.LinuxDriver'> [20:29:43] SYSTEM app.py:2195 loop=<_UnixSelectorEventLoop running=True closed=False debug=False> [20:29:43] SYSTEM app.py:2196 features=frozenset({'debug', 'devtools'}) [20:29:43] SYSTEM app.py:2228 STARTED FileMonitor({PosixPath('/home/josevnz/TextualizeTutorial/docs/Textualize/kodegeek_textualize/os_app.tcss')}) [20:29:43] EVENT
import unittest from textual.widgets import Log, Button from kodegeek_textualize.log_scroller import OsApp
class LogScrollerTestCase(unittest.IsolatedAsyncioTestCase): async def test_log_scroller(self): app = OsApp() self.assertIsNotNone(app) async with app.run_test() as pilot: # Execute the default commands await pilot.click(Button) await pilot.pause() event_log = app.screen.query(Log).first() # We pushed the screen, query nodes from there self.assertTrue(event_log.lines) await pilot.click("#close") # Close the new screen, pop the original one await pilot.press("q") # Quit the app by pressing q
if __name__ == '__main__': unittest.main()
现在让我们详细看看 test_log_scroller 方法中的操作步骤:
通过 app.run_test() 获取一个 Pilot 实例。然后点击主按钮,运行包含默认指令的查询,随后等待所有事件的处理。
import unittest from textual.widgets import DataTable, MarkdownViewer from kodegeek_textualize.table_with_detail_screen import CompetitorsApp
class TableWithDetailTestCase(unittest.IsolatedAsyncioTestCase): async def test_app(self): app = CompetitorsApp() self.assertIsNotNone(app) async with app.run_test() as pilot:
""" Test the command palette """ await pilot.press("ctrl+\\") for char in "manuela".split(): await pilot.press(char) await pilot.press("enter") markdown_viewer = app.screen.query(MarkdownViewer).first() self.assertTrue(markdown_viewer.document) await pilot.click("#close") # Close the new screen, pop the original one
""" Test the table """ table = app.screen.query(DataTable).first() coordinate = table.cursor_coordinate self.assertTrue(table.is_valid_coordinate(coordinate)) await pilot.press("enter") await pilot.pause() markdown_viewer = app.screen.query(MarkdownViewer).first() self.assertTrue(markdown_viewer) # Quit the app by pressing q await pilot.press("q")
if __name__ == '__main__': unittest.main()
如果你运行所有的测试,你将看到如下类似的输出:
1 2 3 4 5 6 7
(Textualize) [josevnz@dmaf5 Textualize]$ python -m unittest tests/*.py .. ---------------------------------------------------------------------- Ran 2 tests in 2.065s