Skip to content

Instantly share code, notes, and snippets.

@jacobtomlinson
Last active October 28, 2022 11:41
Show Gist options
  • Save jacobtomlinson/b9f02776adc78273aad698a33b98c6a3 to your computer and use it in GitHub Desktop.
Save jacobtomlinson/b9f02776adc78273aad698a33b98c6a3 to your computer and use it in GitHub Desktop.
Recording textual demos with vhs

Install vhs.

$ conda create -n textual-demo python=3.10 -y

$ conda activate textual-demo

$ pip install textual

$ vhs < demo.tape
from time import monotonic
from textual.app import App, ComposeResult
from textual.containers import Container
from textual.reactive import reactive
from textual.widgets import Button, Header, Footer, Static
class TimeDisplay(Static):
"""A widget to display elapsed time."""
start_time = reactive(monotonic)
time = reactive(0.0)
total = reactive(0.0)
def on_mount(self) -> None:
"""Event handler called when widget is added to the app."""
self.update_timer = self.set_interval(1 / 60, self.update_time, pause=True)
def update_time(self) -> None:
"""Method to update time to current."""
self.time = self.total + (monotonic() - self.start_time)
def watch_time(self, time: float) -> None:
"""Called when the time attribute changes."""
minutes, seconds = divmod(time, 60)
hours, minutes = divmod(minutes, 60)
self.update(f"{hours:02,.0f}:{minutes:02.0f}:{seconds:05.2f}")
def start(self) -> None:
"""Method to start (or resume) time updating."""
self.start_time = monotonic()
self.update_timer.resume()
def stop(self):
"""Method to stop the time display updating."""
self.update_timer.pause()
self.total += monotonic() - self.start_time
self.time = self.total
def reset(self):
"""Method to reset the time display to zero."""
self.total = 0
self.time = 0
class Stopwatch(Static):
"""A stopwatch widget."""
def on_button_pressed(self, event: Button.Pressed) -> None:
"""Event handler called when a button is pressed."""
button_id = event.button.id
time_display = self.query_one(TimeDisplay)
if button_id == "start":
time_display.start()
self.add_class("started")
elif button_id == "stop":
time_display.stop()
self.remove_class("started")
elif button_id == "reset":
time_display.reset()
def compose(self) -> ComposeResult:
"""Create child widgets of a stopwatch."""
yield Button("Start", id="start", variant="success")
yield Button("Stop", id="stop", variant="error")
yield Button("Reset", id="reset")
yield TimeDisplay()
class StopwatchApp(App):
"""A Textual app to manage stopwatches."""
CSS_PATH = "stopwatch.css"
BINDINGS = [
("d", "toggle_dark", "Toggle dark mode"),
("a", "add_stopwatch", "Add"),
("r", "remove_stopwatch", "Remove"),
]
def compose(self) -> ComposeResult:
"""Called to add widgets to the app."""
yield Header()
yield Footer()
yield Container(Stopwatch(), Stopwatch(), Stopwatch(), id="timers")
def action_add_stopwatch(self) -> None:
"""An action to add a timer."""
new_stopwatch = Stopwatch()
self.query_one("#timers").mount(new_stopwatch)
new_stopwatch.scroll_visible()
def action_remove_stopwatch(self) -> None:
"""Called to remove a timer."""
timers = self.query("Stopwatch")
if timers:
timers.last().remove()
def action_toggle_dark(self) -> None:
"""An action to toggle dark mode."""
self.dark = not self.dark
if __name__ == "__main__":
app = StopwatchApp()
app.run()
Output demo.gif
Set FontSize 28
Set Width 1200
Set Height 600
Set Padding 10
Type "python demo.py"
Sleep 500ms
Enter
Sleep 2s
Tab
Sleep 500ms
Type "r"
Sleep 500ms
Type "r"
Sleep 500ms
Enter
Sleep 5s
Sleep 500ms
Type "a"
Sleep 2s
Ctrl+c
Sleep 2s
Stopwatch {
layout: horizontal;
background: $boost;
height: 5;
min-width: 50;
margin: 1;
padding: 1;
}
TimeDisplay {
content-align: center middle;
text-opacity: 60%;
height: 3;
}
Button {
width: 16;
}
#start {
dock: left;
}
#stop {
dock: left;
display: none;
}
#reset {
dock: right;
}
.started {
text-style: bold;
background: $success;
color: $text;
}
.started TimeDisplay {
text-opacity: 100%;
}
.started #start {
display: none
}
.started #stop {
display: block
}
.started #reset {
visibility: hidden
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment