Add auto-exit after inactivity

Closes: #6
This commit is contained in:
Jeff Epler 2023-12-16 13:52:29 -06:00
parent a6aa81882d
commit 377608f78d
No known key found for this signature in database
GPG key ID: D5BF15AB975AB4DE
2 changed files with 58 additions and 4 deletions

View file

@ -35,6 +35,30 @@ otp-command = "echo 'otpauth://totp/example?algorithm=SHA1&digits=6&secret=IHACD
If the command is a string, it is interpreted with the shell; otherwise, the list of arguments is used directly.
## Auto-exit on idle
To auto exit after a specified inactivity period, use the `auto-exit` setting:
```toml
# Exit after 5 minutes (300 seconds) of inactivity
auto-exit = 300
```
Any key event, mouse click, or mouse scroll counts as "activity" and will reset the auto exit timer.
If `auto-exit` is not specified, or it is 0, there is no inactivity timeout.
## Profiles
textual-totp supports multiple profiles. Profiles are organized as sections of the configuration file; if a setting is not specified within a profile section, the global setting is used.
For example, given
```
auto-exit=300
otp-command = ["..."]
[trusted-location]
auto-exit=0
```
textual-totp will normally exit after 5 minutes of inactivity, but when you run `ttotp --profile trusted-location` auto-exit will be disabled.
# Obtaining TOTP URIs
There are a couple of ways to obtain your TOTP URIs, which are strings that begin `otpauth://totp/`.

View file

@ -17,6 +17,7 @@ from typing import TYPE_CHECKING, Any, Sequence, cast
import rich.text
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.binding import Binding
@ -279,12 +280,17 @@ class TTOTP(App[None]):
Binding("ctrl+a", "clear_search", "Show all", show=True),
]
def __init__(self, tokens: Sequence[pyotp.TOTP]) -> None:
def __init__(
self, tokens: Sequence[pyotp.TOTP], timeout: int | float | None
) -> None:
super().__init__()
self.tokens = tokens
self.otp_data: list[TOTPData] = []
self.timer: Timer | None = None
self.clear_clipboard_time: Timer | None = None
self.exit_time: Timer | None = None
self.warn_exit_time: Timer | None = None
self.timeout: int | float | None = timeout
self.copied = ""
def on_mount(self) -> None:
@ -293,6 +299,24 @@ class TTOTP(App[None]):
self.clear_clipboard_timer = self.set_timer(
30, self.clear_clipboard_func, pause=True
)
if self.timeout:
self.exit_time = self.set_timer(self.timeout, self.action_quit)
warn_timeout = max(self.timeout / 2, self.timeout - 10)
self.warn_exit_time = self.set_timer(warn_timeout, self.warn_quit)
def reset_exit_timers(self) -> None:
if self.exit_time:
self.exit_time.reset()
if self.warn_exit_time:
self.warn_exit_time.reset()
async def on_event(self, event: Any) -> None:
if isinstance(event, (Key, MouseDown, MouseUp, MouseScrollDown, MouseScrollUp)):
self.reset_exit_timers()
await super().on_event(event)
def warn_quit(self) -> None:
self.notify("Will exit soon due to inactivity", title="Auto-exit")
def clear_clipboard_func(self) -> None:
if pyperclip_paste() == self.copied:
@ -432,9 +456,10 @@ multiple profiles as configuration file sections, and select one with
config_data = tomllib.load(f)
if profile:
config_data = config_data.get(profile, None)
if config_data is None:
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)
otp_command = config_data.get("otp-command")
if otp_command is None:
@ -447,6 +472,11 @@ multiple profiles as configuration file sections, and select one with
else:
config_hint("The otp-command value must be a string or list of strings.")
timeout = config_data.get("auto-exit", None)
if timeout is not None:
if not isinstance(timeout, (float, int)):
config_hint("If specified, the auto-exit value must be a number.")
tokens: list[pyotp.TOTP] = []
for row in c.strip().split("\n"):
if row.startswith("otpauth://"):
@ -455,7 +485,7 @@ multiple profiles as configuration file sections, and select one with
if not tokens:
config_hint("No tokens were found when running the given command.")
TTOTP(tokens).run()
TTOTP(tokens, timeout).run()
if __name__ == "__main__":