Package: textology
TextologyThe study of making interactive UIs with text.Why should GUIs have all the fun? Textology extends the amazing functionality ofTextualandRich,
to help create TUIs with popular UI design patterns and principals from web and mobile frameworks.
Refer toTop Featuresfor a summary of the extensions provided.Additional BackgroundCommonly known as Text (or Terminal) User Interfaces, the goal of a TUI (Tooey) is to provide as close
as possible to a traditional GUI experience straight from a terminal. Why? Not all environments allow full graphical
library access, web access, etc., but almost all provide access from a terminal. Yes, even terminals can provide
mouse support, sophisticated layouts, animations, and a wide range of colors!Like Textual, Textology is inspired by modern web development. Textology extends Textual by bringing together, and
expanding upon, designs from other frameworks such as Dash/FastAPI/Flask, including their use of routing, annotations,
and observation patterns. Why? To increase developer velocity based on existing experience. Textology also receives
inspiration from other UI frameworks external to Python, such as iOS, Android, and Web frameworks. Most importantly
however, Textology is an extension of Textual: it does not replace Textual, but rather provides additional options
on top of the core Textual framework.Before using Textology, be comfortable with Textual. For tutorials, guides, etc., refer to theTextual Documentation. Textology is NOT a replacement for Textual, it is an
extension. Callbacks, widgets, screens, event lifecycles, etc., from Textual still apply to Textology extended
widgets and applications. For other advanced features, familiarity with Dash/FastAPI/Flask principles will help.
Examples for Textology extensions, such as callback based applications, are included in this documentation.Table Of ContentsTop FeaturesCompatibilityGetting StartedInstallationExtended ApplicationsExtended WidgetsExtended TestingTop FeaturesMultiple theme supportSwap CSS themes liveApply multiple CSS themes simultaneouslyExtended callbacksDeclare Widget callbacks/event handling without subclassingDeclare Apps with "event driven architecture/observation pattern" to detect changes and automatically update UIListen to reactive attribute changes.Listen to events/messages/errors.Extended native widgets, including (but not limited to):All widgets: ability to disable event messages and declare styles without subclassingListItems with data objectsButtons with automatic tracking of click countsNew widgets, including (but not limited to):ListItemHeaders (non-interactive ListItems)HorizontalMenus (walkable list of ListViews with peeking at following lists)Enhanced testing supportParallel tests via python-xdistCustom testing arguments, such as updating snapshots on failuresCompatibilityTextology followsTextual Compatibilityguidelines with one
exception: Python3.10 minimum requirement.Getting StartedInstallationInstall Textology via pip:pipinstalltextologyFor development of applications based on Textual/Textology (but not development of Textology itself), use the[dev]package. This installs extra Textual development tools, and requirements for Textology testing extensions.pipinstalltextology[dev]For full development of Textology itself, refer toContributing. This installs Textual development tools,
requirements for Textology testing extensions, and full QA requirements to meet commit standards. This version
has the highest library requirements, in order to match the versions used by Textology itself for testing. Required if
developing Textology itself, or recommended if looking to match/exceed the level of QA testing used by Textology.Extended WidgetsNative Textual widgets can be directly swapped out with Textology extended equivalents. They can then be used as is
(standard Textual usage), or with extensions (via extra keyword arguments).Basic swap (no extensions):# Replace:fromtextual.widgetsimportButton# With:fromtextology.widgetsimportButtonInstance callback extension (avoid global watchers, name/ID checks in the event watchers, and event chaining):fromtextology.widgetsimportButtonbutton=Button(callbacks={"on_button_pressed":lambdaevent:print("Don't press my buttons...")},)Instance style extension (set styles directly at instantiation based on logic):fromtextology.widgetsimportButtonfeeling_blue=Truebutton=Button(styles={"background":"blue"iffeeling_blueelse"green",},)Instance message disable extension (avoid unused event chains, such as in large ListViews):fromtextualimporteventsfromtextology.widgetsimportListItemitem=ListItem(disabled_messages=[events.Mount,events.Show],)Extended ApplicationsTextology App classes, such asWidgetApp, can replace any regular Textual App, and be used as is without any
extensions turned on. Here are examples of the most commonly used application subclasses,WidgetAppandExtendedApp, and their primary extended functionality being used. More detailed examples of applications based
around routes, callbacks, and standard Textual applications can be found inExamples.Basic App without subclassing:fromtextology.appsimportWidgetAppfromtextology.widgetsimportButton,Container,Labelapp=WidgetApp(Container(Button("Ping",callbacks={"on_button_pressed":lambdaevent:app.query_one('#label').update("Ping")}),Button("Pong",callbacks={"on_button_pressed":lambdaevent:app.query_one('#label').update("Pong")}),Button("Sing-a-long",callbacks={"on_button_pressed":lambdaevent:app.query_one('#label').update("Sing-a-long")}),Label(id="label")))app.run()Observer/callback application (automatic attribute monitoring and updates by element IDs without manual queries):fromtextology.appsimportExtendedAppfromtextology.observersimportModified,Select,Updatefromtextology.widgetsimportButton,Container,Labelapp=ExtendedApp(child=Container(Button("Ping",id="ping-btn"),Button("Pong",id="pong-btn"),Button("Sing-a-long",id="sing-btn"),Container(id="content",),))@app.when(Modified("ping-btn","n_clicks"),Update("content","children"),)defping(clicks):returnLabel(f"Ping pressed{clicks}")@app.when(Modified("pong-btn","n_clicks"),Update("content","children"),)defpong(clicks):returnLabel(f"Pong pressed{clicks}")@app.when(Modified("sing-btn","n_clicks"),Select("ping-btn","n_clicks"),Select("pong-btn","n_clicks"),Update("content","children"),)defsong(song_clicks,ping_clicks,pong_clicks):ifnotping_clicksornotpong_clicks:returnLabel(f"Press Ping and Pong first to complete the song!")returnLabel(f"Ping, pong, sing-a-long song pressed{song_clicks}")app.run()Callbacks can also be async:@app.when(Modified("pong-btn","n_clicks"),Update("content","children"),)asyncdefdelayed_pong(clicks):awaitasyncio.sleep(3)returnLabel(f"Pong pressed{clicks}and updated 3 seconds later")Callbacks can also catch Exceptions from other callbacks:@app.when(Raised(Exception),)deferror_notification(error):app.notify(f"An unknown error occurred:{error}",title="Error")Callbacks can also listen for stateless events, not just stateful attribute updatesfromtextology.appsimportExtendedAppfromtextology.observersimportPublished,Select,Updatefromtextology.widgetsimportButton,Container,Labelapp=ExtendedApp(child=Container(Button("Ping",id="ping-btn"),Button("Pong",id="pong-btn"),Button("Sing-a-long",id="sing-btn"),Container(id="content",),))@app.when(Published("ping-btn",Button.Pressed),Update("content","children"),)defping(event):returnLabel(f"Ping pressed{event.button.n_clicks}")@app.when(Published("pong-btn",Button.Pressed),Update("content","children"),)defpong(event):returnLabel(f"Pong pressed{event.button.n_clicks}")@app.when(Published("sing-btn",Button.Pressed),Select("ping-btn","n_clicks"),Select("pong-btn","n_clicks"),Update("content","children"),)defsong(event,ping_clicks,pong_clicks):ifnotping_clicksornotpong_clicks:returnLabel(f"Press Ping and Pong first to complete the song!")returnLabel(f"Ping, pong, sing-a-long song pressed{event.button.n_clicks}")app.run()Callbacks can also be registered on methods, to share across all application instancesfromtextology.appsimportExtendedAppfromtextology.observersimportPublished,Select,Update,whenfromtextology.widgetsimportButton,Container,LabelclassPage(Container):defcompose(self):yieldButton("Ping",id="ping-btn")yieldButton("Pong",id="pong-btn")yieldButton("Sing-a-long",id="sing-btn")yieldContainer(id="content",)@when(Published("ping-btn",Button.Pressed),Update("content","children"),)defping(self,event):returnLabel(f"Ping pressed{event.button.n_clicks}")@when(Published("pong-btn",Button.Pressed),Update("content","children"),)defpong(self,event):returnLabel(f"Pong pressed{event.button.n_clicks}")@when(Published("sing-btn",Button.Pressed),Select("ping-btn","n_clicks"),Select("pong-btn","n_clicks"),Update("content","children"),)defsong(self,event,ping_clicks,pong_clicks):ifnotping_clicksornotpong_clicks:returnLabel(f"Press Ping and Pong first to complete the song!")returnLabel(f"Ping, pong, sing-a-long song pressed{event.button.n_clicks}")app=ExtendedApp(child=Page())app.run()Callbacks can also use Dash code style (Same as others, but with Dash compatibility object and calls)fromtextology.dash_compatimportDashCompatApp,Input,Output,Statefromtextology.widgetsimportButton,Container,Labelapp=DashCompatApp(layout=Container(Button("Ping",id="ping-btn"),Button("Pong",id="pong-btn"),Button("Sing-a-long",id="sing-btn"),Container(id="content",),))@app.callback(Input("ping-btn","n_clicks"),Output("content","children"),)defping(clicks):returnLabel(f"Ping pressed{clicks}")@app.callback(Input("pong-btn","n_clicks"),Output("content","children"),)defpong(clicks):returnLabel(f"Pong pressed{clicks}")@app.callback(Input("sing-btn","n_clicks"),State("ping-btn","n_clicks"),State("pong-btn","n_clicks"),Output("content","children"),)defsong(song_clicks,ping_clicks,pong_clicks):ifnotping_clicksornotpong_clicks:returnLabel(f"Press Ping and Pong first to complete the song!")returnLabel(f"Ping, pong, sing-a-long song pressed{song_clicks}")app.run()Extended TestingDon't want to serialize your pytests? Looking for the ability to quickly visualize differences when UIs change?
You came to the right place. Textology extends Textual SVG snapshot capabilities to add support for parallel processing
during tests (python-xdist), and custom options such as auto updating SVG snapshots on failures. In order to use the
pytest extensions automagically, add the following to aconftest.pyin the root of the project. This will enable
usage of thecompare_snapshotsfixture, and HTML report generation on failure, automatically.pytest_plugins=("textology.pytest_utils",)Basic snapshot test:importpytestfromtextualimportAppfromtextual.widgetsimportButtonclassBasicApp(App):defcompose(self):yieldButton("Click me!")@pytest.mark.asyncioasyncdeftest_snapshot_with_app(compare_snapshots):assertawaitcompare_snapshots(BasicApp())Other advanced testing features include:Ability to pass an App, App Pilot, or a module containing an instantiated App or Pilot, to fixturesCustom snapshot paths, including reusing the same snapshot across multiple testsAutomatic SVG updates withpytest --txtology-snap-updateView all options by runningpytest -hand referring toCustom options:section.