mirror of
https://github.com/NCBM/plyngent.git
synced 2026-11-07 07:17:54 +08:00
core/cli: drop the go-to-terminal button and ring the bell when a popup goes out
"Go to terminal" promised something no desktop can do — raise another application's window — and the popup's own close button does as much, so it was a third button that answered nothing. `REVIEW` leaves the answer vocabulary; a click on the body (the platform's default action) or a close still withdraws the notification and leaves the prompt waiting. The bell now rings as the notification is shown. A prompt is only announced after `grace_seconds` of nobody answering it, so the human is at some other window — possibly another virtual desktop, where a popup is easy to miss — and the terminal's bell is the only portable cue that says where to look. It rings once per announcement; withdrawing the popup afterwards stays silent.
This commit is contained in:
+31
-30
@@ -4,19 +4,21 @@ A prompt blocks the turn, so someone who switched to another window learns about
|
|||||||
it only when they come back. The notifier announces a prompt once it has waited
|
it only when they come back. The notifier announces a prompt once it has waited
|
||||||
``grace_seconds`` — answering at the terminal stays silent — and, where the
|
``grace_seconds`` — answering at the terminal stays silent — and, where the
|
||||||
platform allows it, offers the answer itself: the prompt's own answers as
|
platform allows it, offers the answer itself: the prompt's own answers as
|
||||||
buttons (allow / deny / review for a confirmation), a reply field for a
|
buttons (allow / deny for a confirmation), a reply field for a question. It says
|
||||||
question. It says what is asked: a question travels in the body where it is read
|
what is asked: a question travels in the body where it is read whole, the object
|
||||||
whole, the object of a confirmation (a program or a file name) rides along with
|
of a confirmation (a program or a file name) rides along with it, a prompt that
|
||||||
it, and a prompt that takes a default answer says when not answering gives it. A
|
takes a default answer says when not answering gives it, and the workspace it
|
||||||
prompt that can be answered from the notification reads from both surfaces from
|
belongs to comes last. A prompt that can be answered from the notification reads
|
||||||
its first poll, since the announcement only appears once the grace period is
|
from both surfaces from its first poll, since the announcement only appears once
|
||||||
over. A prompt that follows an answer given on the notification is announced at
|
the grace period is over. A prompt that follows an answer given on the
|
||||||
once: the human is already looking at it.
|
notification is announced at once: the human is already looking at it.
|
||||||
|
|
||||||
Two channels. The desktop one is the optional ``desktop-notifier`` extra
|
Two channels. The desktop one is the optional ``desktop-notifier`` extra
|
||||||
(``pip install plyngent[notify]``); the terminal one writes an OSC escape
|
(``pip install plyngent[notify]``); the terminal one writes an OSC escape
|
||||||
sequence to the controlling terminal, which every modern terminal turns into a
|
sequence to the controlling terminal, which every modern terminal turns into a
|
||||||
native notification without any dependency at all.
|
native notification without any dependency at all. A notification that went to
|
||||||
|
the desktop also rings the terminal's bell: the popup may be on another virtual
|
||||||
|
desktop, and no desktop lets a program raise another application's window.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
@@ -44,7 +46,6 @@ if TYPE_CHECKING:
|
|||||||
|
|
||||||
ALLOW = "allow"
|
ALLOW = "allow"
|
||||||
DENY = "deny"
|
DENY = "deny"
|
||||||
REVIEW = "review"
|
|
||||||
|
|
||||||
#: Kinds whose notification carries an answer, and the capability it needs: a
|
#: Kinds whose notification carries an answer, and the capability it needs: a
|
||||||
#: confirmation gets buttons, a question gets a reply field.
|
#: confirmation gets buttons, a question gets a reply field.
|
||||||
@@ -58,10 +59,10 @@ _ANSWER_CAPABILITY: dict[str, str] = {
|
|||||||
"ask": "reply",
|
"ask": "reply",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#: The answers a confirmation offers when the prompt names none of its own.
|
||||||
_BUTTONS: tuple[tuple[str, str], ...] = (
|
_BUTTONS: tuple[tuple[str, str], ...] = (
|
||||||
(ALLOW, "Allow once"),
|
(ALLOW, "Allow once"),
|
||||||
(DENY, "Deny"),
|
(DENY, "Deny"),
|
||||||
(REVIEW, "Go to terminal"),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
_MAX_TITLE = 80
|
_MAX_TITLE = 80
|
||||||
@@ -451,9 +452,6 @@ class WaitHandle:
|
|||||||
"""Record one answer pressed on the notification (a button)."""
|
"""Record one answer pressed on the notification (a button)."""
|
||||||
if self._closed:
|
if self._closed:
|
||||||
return
|
return
|
||||||
if key == REVIEW:
|
|
||||||
self.review()
|
|
||||||
return
|
|
||||||
# The human is at the notification now, so a prompt that follows this
|
# The human is at the notification now, so a prompt that follows this
|
||||||
# answer is announced at once instead of waiting out the grace period.
|
# answer is announced at once instead of waiting out the grace period.
|
||||||
self.notifier.handoff()
|
self.notifier.handoff()
|
||||||
@@ -466,15 +464,15 @@ class WaitHandle:
|
|||||||
self.notifier.handoff()
|
self.notifier.handoff()
|
||||||
self._answers.put(text)
|
self._answers.put(text)
|
||||||
|
|
||||||
def review(self) -> None:
|
def withdraw(self) -> None:
|
||||||
"""Take the human back to the terminal: withdraw, and ring the bell.
|
"""Take the notification down; the prompt keeps waiting at the terminal.
|
||||||
|
|
||||||
Nothing here can raise another application's window — no desktop
|
This is what a click on the body does, and the popup's own close button
|
||||||
exposes that portably — so the terminal rings instead: the prompt is
|
means the same. Nothing can raise another application's window (no
|
||||||
still waiting there, and the bell says which window holds it.
|
desktop exposes that portably), which is why the bell — rung as the
|
||||||
|
notification went out — is what said where the prompt is.
|
||||||
"""
|
"""
|
||||||
self.notifier.dismiss(self)
|
self.notifier.dismiss(self)
|
||||||
self.notifier.ring()
|
|
||||||
|
|
||||||
def __enter__(self) -> Self:
|
def __enter__(self) -> Self:
|
||||||
delay = 0.0 if self.handoff else self.notifier.grace_seconds
|
delay = 0.0 if self.handoff else self.notifier.grace_seconds
|
||||||
@@ -499,8 +497,13 @@ class WaitHandle:
|
|||||||
def _notify(self) -> None:
|
def _notify(self) -> None:
|
||||||
if self._closed or not self.notifier.may_notify(self):
|
if self._closed or not self.notifier.may_notify(self):
|
||||||
return
|
return
|
||||||
if not self.notifier.notify_desktop(self):
|
if self.notifier.notify_desktop(self):
|
||||||
self.notifier.notify_terminal(self.wait)
|
# The popup went to the desktop, so the human is looking at some
|
||||||
|
# other window — possibly another virtual desktop, where a popup is
|
||||||
|
# easy to miss. The terminal rings to say which window waits.
|
||||||
|
self.notifier.ring()
|
||||||
|
return
|
||||||
|
self.notifier.notify_terminal(self.wait)
|
||||||
|
|
||||||
|
|
||||||
class WaitNotifier:
|
class WaitNotifier:
|
||||||
@@ -618,14 +621,12 @@ class WaitNotifier:
|
|||||||
)
|
)
|
||||||
|
|
||||||
def _buttons(self, wait: PromptWait, capability: str) -> tuple[tuple[str, str], ...]:
|
def _buttons(self, wait: PromptWait, capability: str) -> tuple[tuple[str, str], ...]:
|
||||||
"""The answers *wait* accepts, and always a way back to the terminal."""
|
"""The answers *wait* accepts; a reply prompt has none to offer."""
|
||||||
if capability == "reply":
|
if capability == "reply":
|
||||||
# A reply field carries the answer, so the only button left is the
|
# The reply field carries the answer; a button beside it would only
|
||||||
# way out of the notification.
|
# be a way out, and the popup's own close is that.
|
||||||
return ((REVIEW, "Go to terminal"),)
|
return ()
|
||||||
if not wait.choices:
|
return wait.choices or _BUTTONS
|
||||||
return _BUTTONS
|
|
||||||
return (*wait.choices, (REVIEW, "Go to terminal"))
|
|
||||||
|
|
||||||
def desktop_channel(self) -> DesktopChannel | None:
|
def desktop_channel(self) -> DesktopChannel | None:
|
||||||
"""The desktop surface, resolved once (importing the extra on demand)."""
|
"""The desktop surface, resolved once (importing the extra on demand)."""
|
||||||
@@ -681,7 +682,7 @@ class WaitNotifier:
|
|||||||
reply_field=capability == "reply",
|
reply_field=capability == "reply",
|
||||||
on_answer=handle.answer,
|
on_answer=handle.answer,
|
||||||
on_reply=handle.reply,
|
on_reply=handle.reply,
|
||||||
on_click=handle.review,
|
on_click=handle.withdraw,
|
||||||
timeout=_expiry(wait),
|
timeout=_expiry(wait),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user