diff --git a/src/ttotp/TinyProgress.py b/src/ttotp/TinyProgress.py new file mode 100644 index 0000000..1da578d --- /dev/null +++ b/src/ttotp/TinyProgress.py @@ -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) + ) diff --git a/src/ttotp/__main__.py b/src/ttotp/__main__.py index 906d305..5d20266 100755 --- a/src/ttotp/__main__.py +++ b/src/ttotp/__main__.py @@ -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,7 +527,8 @@ 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.") - config_data.update(profile_data) + else: + config_data.update(profile_data) otp_command = config_data.get("otp-command") if otp_command is None: