Graphical terminal improvements.
It makes using programs who like to refresh a ton much more tollerable when I'm not in Emacs; I suspect others will like them too.
This commit is contained in:
1 parent
1fcf7e6f87
commit
00dbb1fea7
12 files changed
+608
-14
No files matched your search
@@ -8,6 +8,7 @@ import (
|
||||
"log/slog"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode"
|
||||
|
||||
"git.stormux.org/storm/cthulhu/internal/atspi"
|
||||
"git.stormux.org/storm/cthulhu/internal/input"
|
||||
@@ -46,6 +47,7 @@ type windowTitleController struct {
|
||||
inputSource any
|
||||
lastNative atspi.ForegroundTitle
|
||||
lastTitle string
|
||||
recent map[string]time.Time
|
||||
application string
|
||||
pending *pendingWindowTitle
|
||||
timer *time.Timer
|
||||
@@ -55,6 +57,10 @@ type windowTitleController struct {
|
||||
const (
|
||||
windowTitleChangeDelay = 25 * time.Millisecond
|
||||
windowTitleIntroductionDelay = 250 * time.Millisecond
|
||||
// Animated titles (terminal TUI spinners) cycle through the same set
|
||||
// of frames about once a second. A title already announced within
|
||||
// this window is another cycle, not a new title.
|
||||
windowTitleRepeatWindow = 3 * time.Second
|
||||
)
|
||||
|
||||
func (titles *windowTitleController) clearPending() {
|
||||
@@ -64,6 +70,10 @@ func (titles *windowTitleController) clearPending() {
|
||||
titles.timer, titles.due, titles.pending = nil, nil, nil
|
||||
}
|
||||
|
||||
// The animated-title repeat history (titles.recent) deliberately
|
||||
// survives reset and activation: it expires by time on its own, and a
|
||||
// workspace round-trip away from a window with an animated title must
|
||||
// not restart its announcement cycle on return.
|
||||
func (titles *windowTitleController) reset() {
|
||||
titles.clearPending()
|
||||
titles.lastTitle, titles.application = "", ""
|
||||
@@ -140,6 +150,11 @@ func (titles *windowTitleController) accessible(event atspi.Event) bool {
|
||||
titles.state.owner.changeTitle(event)
|
||||
titles.clearPending()
|
||||
titles.lastTitle = ""
|
||||
// Keep the repeat history: animated title cycles include a
|
||||
// blank frame every cycle (observed from terminal TUIs), and
|
||||
// clearing here would restart the cycle announcement each
|
||||
// second. A title restored after a genuine removal announces
|
||||
// again once the repeat window expires.
|
||||
return true
|
||||
}
|
||||
previousTitle := titles.state.owner.title
|
||||
@@ -171,6 +186,16 @@ func (titles *windowTitleController) nativeChanged() {
|
||||
accessibleActivation := titles.state.owner.activated && !wine.IsReviewID(titles.state.owner.window)
|
||||
if switched {
|
||||
titles.clearPending()
|
||||
// Keep the repeat history: a native sampler can re-report the
|
||||
// same client as a switch (new epoch or title-window id) on
|
||||
// every title sample, and clearing here would defeat animated
|
||||
// title suppression. Genuine window switches clear it through
|
||||
// AT-SPI activation instead.
|
||||
if titles.logger != nil {
|
||||
titles.logger.Debug("native window title switch",
|
||||
"epoch", latest.Epoch, "window", latest.Title.Window,
|
||||
"title_window", latest.Title.TitleWindow)
|
||||
}
|
||||
// An AT-SPI activation may already have introduced this client.
|
||||
titles.lastTitle = titles.state.owner.title
|
||||
if wine.IsReviewID(titles.state.owner.window) {
|
||||
@@ -200,6 +225,46 @@ func (titles *windowTitleController) nativeChanged() {
|
||||
native: latest.Title, changed: changed})
|
||||
}
|
||||
|
||||
// animatedWindowTitleKey reduces a title to the part that carries meaning
|
||||
// across animation frames. A leading field with no letter or digit is a
|
||||
// spinner frame, not content; a leading count stays, since it can change
|
||||
// meaningfully. Announcement suppression uses this only to detect cycles;
|
||||
// the spoken text remains the original title.
|
||||
func animatedWindowTitleKey(title string) string {
|
||||
fields := strings.Fields(strings.ToLower(title))
|
||||
if len(fields) > 1 && strings.IndexFunc(fields[0], func(r rune) bool {
|
||||
return unicode.IsLetter(r) || unicode.IsDigit(r)
|
||||
}) < 0 {
|
||||
fields = fields[1:]
|
||||
}
|
||||
return strings.Join(fields, " ")
|
||||
}
|
||||
|
||||
// repeatedWithinWindow reports whether this title was already announced in
|
||||
// the repeat window. A suppressed sighting refreshes its entry, so a
|
||||
// running animation stays suppressed instead of breaking through once per
|
||||
// window; it expires shortly after the animation stops.
|
||||
func (titles *windowTitleController) repeatedWithinWindow(text string, at time.Time) bool {
|
||||
key := animatedWindowTitleKey(text)
|
||||
if key == "" {
|
||||
return false
|
||||
}
|
||||
if at.IsZero() {
|
||||
at = time.Now()
|
||||
}
|
||||
for seen, when := range titles.recent {
|
||||
if at.Sub(when) > windowTitleRepeatWindow {
|
||||
delete(titles.recent, seen)
|
||||
}
|
||||
}
|
||||
if titles.recent == nil {
|
||||
titles.recent = make(map[string]time.Time)
|
||||
}
|
||||
_, repeated := titles.recent[key]
|
||||
titles.recent[key] = at
|
||||
return repeated
|
||||
}
|
||||
|
||||
func equivalentWindowTitles(first, second string) bool {
|
||||
normalize := func(value string) string { return strings.Join(strings.Fields(strings.ToLower(value)), " ") }
|
||||
first, second = normalize(first), normalize(second)
|
||||
@@ -327,6 +392,13 @@ func (titles *windowTitleController) flush() {
|
||||
if pending.inputSequence != recentInput(titles.inputSource).TitleInterruptionSequence {
|
||||
return
|
||||
}
|
||||
if pending.changed && titles.repeatedWithinWindow(pending.text, pending.observedAt) {
|
||||
if titles.logger != nil {
|
||||
titles.logger.Debug("suppressed repeated window title",
|
||||
"key", animatedWindowTitleKey(pending.text), "recent", len(titles.recent))
|
||||
}
|
||||
return
|
||||
}
|
||||
if titles.logger != nil {
|
||||
titles.logger.Debug("queued automatic window title", "title_changed", pending.changed,
|
||||
"observed_to_queue_ms", time.Since(pending.observedAt).Milliseconds())
|
||||
|
||||
Reference in new issue
Block a user