Switch to a "tiny" progress bar
this saves horizontal space for when the screen is small.
This commit is contained in:
parent
3cff298395
commit
a1eb7e10d7
2 changed files with 91 additions and 5 deletions
85
src/ttotp/TinyProgress.py
Normal file
85
src/ttotp/TinyProgress.py
Normal 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)
|
||||||
|
)
|
||||||
|
|
@ -20,7 +20,7 @@ from textual.fuzzy import Matcher
|
||||||
from textual.app import App, ComposeResult
|
from textual.app import App, ComposeResult
|
||||||
from textual.events import Key, MouseDown, MouseUp, MouseScrollDown, MouseScrollUp
|
from textual.events import Key, MouseDown, MouseUp, MouseScrollDown, MouseScrollUp
|
||||||
from textual.widget import Widget
|
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.binding import Binding
|
||||||
from textual.containers import VerticalScroll, Horizontal
|
from textual.containers import VerticalScroll, Horizontal
|
||||||
from textual.css.query import DOMQuery
|
from textual.css.query import DOMQuery
|
||||||
|
|
@ -31,6 +31,8 @@ import pyotp
|
||||||
import platformdirs
|
import platformdirs
|
||||||
import tomllib
|
import tomllib
|
||||||
|
|
||||||
|
from .TinyProgress import TinyProgress as ProgressBar
|
||||||
|
|
||||||
from typing import TypeGuard # use `typing_extensions` for Python 3.9 and below
|
from typing import TypeGuard # use `typing_extensions` for Python 3.9 and below
|
||||||
|
|
||||||
# workaround for pyperclip being un-typed
|
# workaround for pyperclip being un-typed
|
||||||
|
|
@ -328,15 +330,13 @@ def search_preprocess(s: str) -> str:
|
||||||
class TTOTP(App[None]):
|
class TTOTP(App[None]):
|
||||||
CSS = """
|
CSS = """
|
||||||
VerticalScroll { min-height: 1; }
|
VerticalScroll { min-height: 1; }
|
||||||
.otp-progress { width: 12; }
|
|
||||||
.otp-value { width: 9; }
|
.otp-value { width: 9; }
|
||||||
.otp-hidden { display: none; }
|
.otp-hidden { display: none; }
|
||||||
.otp-name { text-wrap: nowrap; text-overflow: ellipsis; }
|
.otp-name { text-wrap: nowrap; text-overflow: ellipsis; }
|
||||||
.otp-name:focus { background: red; }
|
.otp-name:focus { background: red; }
|
||||||
TOTPLabel { width: 1fr; height: 1; padding: 0 1; }
|
TOTPLabel { width: 1fr; height: 1; padding: 0 1; }
|
||||||
Horizontal:focus-within { background: $primary-background; }
|
Horizontal:focus-within { background: $primary-background; }
|
||||||
Bar > .bar--bar { color: $success; }
|
OneCellBar > .bar--bar { color: $success; }
|
||||||
Bar { width: 1fr; }
|
|
||||||
Button { border: none; height: 1; width: 3; min-width: 4 }
|
Button { border: none; height: 1; width: 3; min-width: 4 }
|
||||||
Horizontal { height: 1; }
|
Horizontal { height: 1; }
|
||||||
Input { border: none; height: 1; width: 1fr; }
|
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)
|
profile_data = config_data.get(profile, None)
|
||||||
if profile_data is None:
|
if profile_data is None:
|
||||||
config_hint(f"The profile {profile!r} file does not exist.")
|
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")
|
otp_command = config_data.get("otp-command")
|
||||||
if otp_command is None:
|
if otp_command is None:
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue