264 lines
8.2 KiB
Python
264 lines
8.2 KiB
Python
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
import asyncio
|
||
|
|
from importlib.metadata import version
|
||
|
|
|
||
|
|
try:
|
||
|
|
import httpx
|
||
|
|
|
||
|
|
HTTPX_AVAILABLE = True
|
||
|
|
except ImportError:
|
||
|
|
HTTPX_AVAILABLE = False
|
||
|
|
|
||
|
|
from textual import work
|
||
|
|
from textual.app import ComposeResult
|
||
|
|
from textual.containers import Horizontal, Vertical, VerticalScroll
|
||
|
|
from textual.demo.page import PageScreen
|
||
|
|
from textual.reactive import reactive
|
||
|
|
from textual.widgets import Collapsible, Digits, Footer, Label, Markdown
|
||
|
|
|
||
|
|
WHAT_IS_TEXTUAL_MD = """\
|
||
|
|
# What is Textual?
|
||
|
|
|
||
|
|
Snappy, keyboard-centric, applications that run in the terminal and [the web](https://github.com/Textualize/textual-web).
|
||
|
|
|
||
|
|
🐍 All you need is Python!
|
||
|
|
|
||
|
|
"""
|
||
|
|
|
||
|
|
WELCOME_MD = """\
|
||
|
|
## Welcome keyboard warriors!
|
||
|
|
|
||
|
|
This is a Textual app. Here's what you need to know:
|
||
|
|
|
||
|
|
* **enter** `toggle this collapsible widget`
|
||
|
|
* **tab** `focus the next widget`
|
||
|
|
* **shift+tab** `focus the previous widget`
|
||
|
|
* **ctrl+p** `summon the command palette`
|
||
|
|
|
||
|
|
|
||
|
|
👇 Also see the footer below.
|
||
|
|
|
||
|
|
`Or… click away with the mouse (no judgement).`
|
||
|
|
|
||
|
|
"""
|
||
|
|
|
||
|
|
ABOUT_MD = """\
|
||
|
|
The retro look is not just an aesthetic choice! Textual apps have some unique properties that make them preferable for many tasks.
|
||
|
|
|
||
|
|
## Textual interfaces are *snappy*
|
||
|
|
Even the most modern of web apps can leave the user waiting hundreds of milliseconds or more for a response.
|
||
|
|
Given their low graphical requirements, Textual interfaces can be far more responsive — no waiting required.
|
||
|
|
|
||
|
|
## Reward repeated use
|
||
|
|
Use the mouse to explore, but Textual apps are keyboard-centric and reward repeated use.
|
||
|
|
An experienced user can operate a Textual app far faster than their web / GUI counterparts.
|
||
|
|
|
||
|
|
## Command palette
|
||
|
|
A builtin command palette with fuzzy searching puts powerful commands at your fingertips.
|
||
|
|
|
||
|
|
**Try it:** Press **ctrl+p** now.
|
||
|
|
|
||
|
|
"""
|
||
|
|
|
||
|
|
API_MD = """\
|
||
|
|
A modern Python API from the developer of [Rich](https://github.com/Textualize/rich).
|
||
|
|
|
||
|
|
```python
|
||
|
|
# Start building!
|
||
|
|
from textual.app import App, ComposeResult
|
||
|
|
from textual.widgets import Label
|
||
|
|
|
||
|
|
class MyApp(App):
|
||
|
|
def compose(self) -> ComposeResult:
|
||
|
|
yield Label("Hello, World!")
|
||
|
|
|
||
|
|
MyApp().run()
|
||
|
|
```
|
||
|
|
|
||
|
|
* Intuitive, batteries-included, API.
|
||
|
|
* Well documented: See the [tutorial](https://textual.textualize.io/tutorial/), [guide](https://textual.textualize.io/guide/app/), and [reference](https://textual.textualize.io/reference/).
|
||
|
|
* Fully typed, with modern type annotations.
|
||
|
|
* Accessible to Python developers of all skill levels.
|
||
|
|
|
||
|
|
**Hint:** press **C** to view the code for this page.
|
||
|
|
|
||
|
|
## Built on Rich
|
||
|
|
|
||
|
|
With over 3.1 *billion* downloads, Rich is the most popular terminal library out there.
|
||
|
|
Textual builds on Rich to add interactivity, and is fully-compatible with Rich renderables.
|
||
|
|
|
||
|
|
## Re-usable widgets
|
||
|
|
|
||
|
|
Textual's widgets are self-contained and re-usable across projects.
|
||
|
|
Virtually all aspects of a widget's look and feel can be customized to your requirements.
|
||
|
|
|
||
|
|
## Builtin widgets
|
||
|
|
|
||
|
|
A large [library of builtin widgets](https://textual.textualize.io/widget_gallery/), and a growing ecosystem of third party widgets on PyPI
|
||
|
|
(this content is generated by the builtin [Markdown](https://textual.textualize.io/widget_gallery/#markdown) widget).
|
||
|
|
|
||
|
|
## Reactive variables
|
||
|
|
|
||
|
|
[Reactivity](https://textual.textualize.io/guide/reactivity/) using Python idioms, keeps your logic separate from display code.
|
||
|
|
|
||
|
|
## Async support
|
||
|
|
|
||
|
|
Built on asyncio, you can easily integrate async libraries while keeping your UI responsive.
|
||
|
|
|
||
|
|
## Concurrency
|
||
|
|
|
||
|
|
Textual's [Workers](https://textual.textualize.io/guide/workers/) provide a far-less error prone interface to
|
||
|
|
concurrency: both async and threads.
|
||
|
|
|
||
|
|
## Testing
|
||
|
|
|
||
|
|
With a comprehensive [testing framework](https://textual.textualize.io/guide/testing/), you can release reliable software, that can be maintained indefinitely.
|
||
|
|
|
||
|
|
## Docs
|
||
|
|
|
||
|
|
Textual has [amazing docs](https://textual.textualize.io/)!
|
||
|
|
|
||
|
|
"""
|
||
|
|
|
||
|
|
DEPLOY_MD = """\
|
||
|
|
Textual apps have extremely low system requirements, and will run on virtually any OS and hardware; locally or remotely via SSH.
|
||
|
|
|
||
|
|
There are a number of ways to deploy and share Textual apps.
|
||
|
|
|
||
|
|
## As a Python library
|
||
|
|
|
||
|
|
Textual apps may be pip installed, via tools such as `pipx` or `uvx`, and other package managers.
|
||
|
|
|
||
|
|
## As a web application
|
||
|
|
|
||
|
|
It takes two lines of code to [serve your Textual app](https://github.com/Textualize/textual-serve) as a web application.
|
||
|
|
|
||
|
|
## Managed web application
|
||
|
|
|
||
|
|
With [Textual web](https://github.com/Textualize/textual-web) you can serve multiple Textual apps on the web,
|
||
|
|
with zero configuration. Even behind a firewall.
|
||
|
|
"""
|
||
|
|
|
||
|
|
|
||
|
|
class StarCount(Vertical):
|
||
|
|
"""Widget to get and display GitHub star count."""
|
||
|
|
|
||
|
|
DEFAULT_CSS = """
|
||
|
|
StarCount {
|
||
|
|
dock: top;
|
||
|
|
height: 6;
|
||
|
|
border-bottom: hkey $background;
|
||
|
|
border-top: hkey $background;
|
||
|
|
layout: horizontal;
|
||
|
|
background: $boost;
|
||
|
|
padding: 0 1;
|
||
|
|
color: $text-warning;
|
||
|
|
#stars { align: center top; }
|
||
|
|
#forks { align: right top; }
|
||
|
|
Label { text-style: bold; color: $foreground; }
|
||
|
|
LoadingIndicator { background: transparent !important; }
|
||
|
|
Digits { width: auto; margin-right: 1; }
|
||
|
|
Label { margin-right: 1; }
|
||
|
|
align: center top;
|
||
|
|
&>Horizontal { max-width: 100;}
|
||
|
|
}
|
||
|
|
"""
|
||
|
|
stars = reactive(25251, recompose=True)
|
||
|
|
forks = reactive(776, recompose=True)
|
||
|
|
|
||
|
|
@work
|
||
|
|
async def get_stars(self):
|
||
|
|
"""Worker to get stars from GitHub API."""
|
||
|
|
if not HTTPX_AVAILABLE:
|
||
|
|
self.notify(
|
||
|
|
"Install httpx to update stars from the GitHub API.\n\n$ [b]pip install httpx[/b]",
|
||
|
|
title="GitHub Stars",
|
||
|
|
)
|
||
|
|
return
|
||
|
|
self.loading = True
|
||
|
|
try:
|
||
|
|
await asyncio.sleep(1) # Time to admire the loading indicator
|
||
|
|
async with httpx.AsyncClient() as client:
|
||
|
|
repository_json = (
|
||
|
|
await client.get("https://api.github.com/repos/textualize/textual")
|
||
|
|
).json()
|
||
|
|
self.stars = repository_json["stargazers_count"]
|
||
|
|
self.forks = repository_json["forks"]
|
||
|
|
except Exception:
|
||
|
|
self.notify(
|
||
|
|
"Unable to update star count (maybe rate-limited)",
|
||
|
|
title="GitHub stars",
|
||
|
|
severity="error",
|
||
|
|
)
|
||
|
|
self.loading = False
|
||
|
|
|
||
|
|
def compose(self) -> ComposeResult:
|
||
|
|
with Horizontal():
|
||
|
|
with Vertical(id="version"):
|
||
|
|
yield Label("Version")
|
||
|
|
yield Digits(version("textual"))
|
||
|
|
with Vertical(id="stars"):
|
||
|
|
yield Label("GitHub ★")
|
||
|
|
stars = f"{self.stars / 1000:.1f}K"
|
||
|
|
yield Digits(stars).with_tooltip(f"{self.stars} GitHub stars")
|
||
|
|
with Vertical(id="forks"):
|
||
|
|
yield Label("Forks")
|
||
|
|
yield Digits(str(self.forks)).with_tooltip(f"{self.forks} Forks")
|
||
|
|
|
||
|
|
def on_mount(self) -> None:
|
||
|
|
self.tooltip = "Click to refresh"
|
||
|
|
self.get_stars()
|
||
|
|
|
||
|
|
def on_click(self) -> None:
|
||
|
|
self.get_stars()
|
||
|
|
|
||
|
|
|
||
|
|
class Content(VerticalScroll, can_focus=False):
|
||
|
|
"""Non focusable vertical scroll."""
|
||
|
|
|
||
|
|
|
||
|
|
class HomeScreen(PageScreen):
|
||
|
|
DEFAULT_CSS = """
|
||
|
|
HomeScreen {
|
||
|
|
|
||
|
|
Content {
|
||
|
|
align-horizontal: center;
|
||
|
|
& > * {
|
||
|
|
max-width: 100;
|
||
|
|
}
|
||
|
|
margin: 0 1;
|
||
|
|
overflow-y: auto;
|
||
|
|
height: 1fr;
|
||
|
|
scrollbar-gutter: stable;
|
||
|
|
MarkdownFence {
|
||
|
|
height: auto;
|
||
|
|
max-height: initial;
|
||
|
|
}
|
||
|
|
Collapsible {
|
||
|
|
padding-right: 0;
|
||
|
|
&.-collapsed { padding-bottom: 1; }
|
||
|
|
}
|
||
|
|
Markdown {
|
||
|
|
margin-right: 1;
|
||
|
|
padding-right: 1;
|
||
|
|
background: transparent;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
"""
|
||
|
|
|
||
|
|
def compose(self) -> ComposeResult:
|
||
|
|
yield StarCount()
|
||
|
|
with Content():
|
||
|
|
yield Markdown(WHAT_IS_TEXTUAL_MD)
|
||
|
|
with Collapsible(title="Welcome", collapsed=False):
|
||
|
|
yield Markdown(WELCOME_MD)
|
||
|
|
with Collapsible(title="Textual Interfaces"):
|
||
|
|
yield Markdown(ABOUT_MD)
|
||
|
|
with Collapsible(title="Textual API"):
|
||
|
|
yield Markdown(API_MD)
|
||
|
|
with Collapsible(title="Deploying Textual apps"):
|
||
|
|
yield Markdown(DEPLOY_MD)
|
||
|
|
yield Footer()
|