Switch to a "tiny" progress bar

this saves horizontal space for when the screen is small.
This commit is contained in:
Jeff Epler 2025-08-17 10:05:35 -05:00
parent 3cff298395
commit a1eb7e10d7
2 changed files with 91 additions and 5 deletions

85
src/ttotp/TinyProgress.py Normal file
View file

@ -0,0 +1,85 @@
#!/usr/bin/python3
# SPDX-FileCopyrightText: 2025 Jeff Epler
# SPDX-FileCopyrightText: 2021 Will McGugan
#
# SPDX-License-Identifier: MIT
from textual.app import ComposeResult, RenderResult
from textual.widgets._progress_bar import ProgressBar, Bar
from rich.text import Text
class OneCellBar(Bar):
"""The bar portion of the tiny progress bar."""
BARS = "▁▂▃▄▅▆▇"
SHADES = "█▓▒░▒▓"
DEFAULT_CSS = """
OneCellBar {
width: 1;
height: 1;
&> .bar--bar {
color: $primary;
background: $surface;
}
&> .bar--indeterminate {
color: $error;
background: $surface;
}
&> .bar--complete {
color: $success;
background: $surface;
}
}
"""
def render(self) -> RenderResult:
if self.percentage is None:
return self.render_indeterminate()
else:
return self.render_determinate(self.percentage)
def render_determinate(self, percentage: float) -> RenderResult:
bar_style = (
self.get_component_rich_style("bar--bar")
if percentage < 1
else self.get_component_rich_style("bar--complete")
)
i = self.percentage_to_index(percentage)
return Text(self.BARS[i], style=bar_style)
def watch_percentage(self, percentage: float | None) -> None:
"""Manage the timer that enables the indeterminate bar animation."""
if percentage is not None:
self.auto_refresh = None
else:
self.auto_refresh = 1 # every second
def render_indeterminate(self) -> RenderResult:
bar_style = self.get_component_rich_style("bar--indeterminate")
phase = round(self._clock.time) % len(self.SHADES)
i = self.SHADES[phase]
return Text(i, style=bar_style)
def percentage_to_index(self, percentage: float) -> int:
p = max(0, min(1, percentage))
i = round(p * (len(self.BARS) - 1))
return i
def _validate_percentage(self, percentage: float | None) -> float | None:
if percentage is None:
return None
return self.percentage_to_index(percentage) / (len(self.BARS) - 1)
class TinyProgress(ProgressBar):
def compose(self) -> ComposeResult:
if self.show_bar:
yield (
OneCellBar(id="bar", clock=self._clock)
.data_bind(ProgressBar.percentage)
.data_bind(ProgressBar.gradient)
)

View file

@ -20,7 +20,7 @@ from textual.fuzzy import Matcher
from textual.app import App, ComposeResult
from textual.events import Key, MouseDown, MouseUp, MouseScrollDown, MouseScrollUp
from textual.widget import Widget
from textual.widgets import Label, Footer, ProgressBar, Button, Input
from textual.widgets import Label, Footer, Button, Input
from textual.binding import Binding
from textual.containers import VerticalScroll, Horizontal
from textual.css.query import DOMQuery
@ -31,6 +31,8 @@ import pyotp
import platformdirs
import tomllib
from .TinyProgress import TinyProgress as ProgressBar
from typing import TypeGuard # use `typing_extensions` for Python 3.9 and below
# workaround for pyperclip being un-typed
@ -328,15 +330,13 @@ def search_preprocess(s: str) -> str:
class TTOTP(App[None]):
CSS = """
VerticalScroll { min-height: 1; }
.otp-progress { width: 12; }
.otp-value { width: 9; }
.otp-hidden { display: none; }
.otp-name { text-wrap: nowrap; text-overflow: ellipsis; }
.otp-name:focus { background: red; }
TOTPLabel { width: 1fr; height: 1; padding: 0 1; }
Horizontal:focus-within { background: $primary-background; }
Bar > .bar--bar { color: $success; }
Bar { width: 1fr; }
OneCellBar > .bar--bar { color: $success; }
Button { border: none; height: 1; width: 3; min-width: 4 }
Horizontal { height: 1; }
Input { border: none; height: 1; width: 1fr; }
@ -527,6 +527,7 @@ multiple profiles as configuration file sections, and select one with
profile_data = config_data.get(profile, None)
if profile_data is None:
config_hint(f"The profile {profile!r} file does not exist.")
else:
config_data.update(profile_data)
otp_command = config_data.get("otp-command")