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"] }
classLogScreen(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
classOsApp(App): BINDINGS = [ ("q", "quit_app", "Quit"), ] CSS_PATH = "os_app.tcss" ENABLE_COMMAND_PALETTE = False# Do not need the command palette
defaction_quit_app(self): self.exit(0)
defcompose(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 importList 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 importAny, 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
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
classCustomCommand(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
classCompetitorsApp(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}
defon_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 importAny, 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 importAny, 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
classDetailScreen(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
classCompetitorsApp(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 defshow_detail(self, detailScreen: DetailScreen): self.push_screen(detailScreen)
▌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
classLogScrollerTestCase(unittest.IsolatedAsyncioTestCase): asyncdeftest_log_scroller(self): app = OsApp() self.assertIsNotNone(app) asyncwith 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
classTableWithDetailTestCase(unittest.IsolatedAsyncioTestCase): asyncdeftest_app(self): app = CompetitorsApp() self.assertIsNotNone(app) asyncwith 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