parent
a6aa81882d
commit
377608f78d
2 changed files with 58 additions and 4 deletions
24
README.md
24
README.md
|
|
@ -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.
|
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
|
# Obtaining TOTP URIs
|
||||||
|
|
||||||
There are a couple of ways to obtain your TOTP URIs, which are strings that begin `otpauth://totp/`.
|
There are a couple of ways to obtain your TOTP URIs, which are strings that begin `otpauth://totp/`.
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ from typing import TYPE_CHECKING, Any, Sequence, cast
|
||||||
import rich.text
|
import rich.text
|
||||||
from textual.fuzzy import Matcher
|
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.widget import Widget
|
from textual.widget import Widget
|
||||||
from textual.widgets import Label, Footer, ProgressBar, Button, Input
|
from textual.widgets import Label, Footer, ProgressBar, Button, Input
|
||||||
from textual.binding import Binding
|
from textual.binding import Binding
|
||||||
|
|
@ -279,12 +280,17 @@ class TTOTP(App[None]):
|
||||||
Binding("ctrl+a", "clear_search", "Show all", show=True),
|
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__()
|
super().__init__()
|
||||||
self.tokens = tokens
|
self.tokens = tokens
|
||||||
self.otp_data: list[TOTPData] = []
|
self.otp_data: list[TOTPData] = []
|
||||||
self.timer: Timer | None = None
|
self.timer: Timer | None = None
|
||||||
self.clear_clipboard_time: 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 = ""
|
self.copied = ""
|
||||||
|
|
||||||
def on_mount(self) -> None:
|
def on_mount(self) -> None:
|
||||||
|
|
@ -293,6 +299,24 @@ class TTOTP(App[None]):
|
||||||
self.clear_clipboard_timer = self.set_timer(
|
self.clear_clipboard_timer = self.set_timer(
|
||||||
30, self.clear_clipboard_func, pause=True
|
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:
|
def clear_clipboard_func(self) -> None:
|
||||||
if pyperclip_paste() == self.copied:
|
if pyperclip_paste() == self.copied:
|
||||||
|
|
@ -432,9 +456,10 @@ multiple profiles as configuration file sections, and select one with
|
||||||
config_data = tomllib.load(f)
|
config_data = tomllib.load(f)
|
||||||
|
|
||||||
if profile:
|
if profile:
|
||||||
config_data = config_data.get(profile, None)
|
profile_data = config_data.get(profile, None)
|
||||||
if config_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)
|
||||||
|
|
||||||
otp_command = config_data.get("otp-command")
|
otp_command = config_data.get("otp-command")
|
||||||
if otp_command is None:
|
if otp_command is None:
|
||||||
|
|
@ -447,6 +472,11 @@ multiple profiles as configuration file sections, and select one with
|
||||||
else:
|
else:
|
||||||
config_hint("The otp-command value must be a string or list of strings.")
|
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] = []
|
tokens: list[pyotp.TOTP] = []
|
||||||
for row in c.strip().split("\n"):
|
for row in c.strip().split("\n"):
|
||||||
if row.startswith("otpauth://"):
|
if row.startswith("otpauth://"):
|
||||||
|
|
@ -455,7 +485,7 @@ multiple profiles as configuration file sections, and select one with
|
||||||
if not tokens:
|
if not tokens:
|
||||||
config_hint("No tokens were found when running the given command.")
|
config_hint("No tokens were found when running the given command.")
|
||||||
|
|
||||||
TTOTP(tokens).run()
|
TTOTP(tokens, timeout).run()
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue