Add native i3 desktop announcements
This commit is contained in:
1 parent
49eeae89f4
commit
bf1f155018
22 files changed
+1835
-1
No files matched your search
@@ -0,0 +1,153 @@
|
||||
// Copyright (c) 2026 Storm Dragon
|
||||
// Authors: Storm Dragon, Angus Kola
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/xml"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"git.stormux.org/storm/cthulhu/internal/config"
|
||||
"git.stormux.org/storm/cthulhu/internal/i3ipc"
|
||||
"git.stormux.org/storm/cthulhu/internal/lifecycle"
|
||||
"git.stormux.org/storm/cthulhu/internal/platform/xorg"
|
||||
)
|
||||
|
||||
// i3Announcements owns presentation policy, never focus or accessibility state.
|
||||
// The backend owns its connection, baseline, caches and reconnect lifetime.
|
||||
type i3Announcements struct {
|
||||
live *liveSettings
|
||||
output speechEnqueuer
|
||||
}
|
||||
|
||||
func i3ModeName(name string, markup bool) string {
|
||||
if !markup {
|
||||
return strings.TrimSpace(name)
|
||||
}
|
||||
decoder := xml.NewDecoder(strings.NewReader("<mode>" + name + "</mode>"))
|
||||
var text strings.Builder
|
||||
for {
|
||||
token, err := decoder.Token()
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
if data, ok := token.(xml.CharData); ok {
|
||||
text.Write(data)
|
||||
}
|
||||
}
|
||||
return strings.TrimSpace(text.String())
|
||||
}
|
||||
|
||||
func i3Message(event i3ipc.Event, settings config.ReadingSettings) string {
|
||||
var text string
|
||||
switch event.Kind {
|
||||
case i3ipc.Mode:
|
||||
if !settings.I3Modes {
|
||||
return ""
|
||||
}
|
||||
name := i3ModeName(event.Name, event.Markup)
|
||||
if name == "" {
|
||||
return ""
|
||||
}
|
||||
if name == "default" {
|
||||
return "Default mode"
|
||||
}
|
||||
return name + " mode"
|
||||
case i3ipc.Workspace:
|
||||
if settings.I3Workspaces && event.Name != "" {
|
||||
return "Workspace " + event.Name
|
||||
}
|
||||
return ""
|
||||
case i3ipc.Fullscreen:
|
||||
if !settings.I3WindowStates {
|
||||
return ""
|
||||
}
|
||||
text = "Fullscreen off"
|
||||
if event.Enabled {
|
||||
text = "Fullscreen on"
|
||||
if event.FullscreenMode == 2 {
|
||||
text = "Global fullscreen on"
|
||||
}
|
||||
}
|
||||
case i3ipc.Floating:
|
||||
if !settings.I3WindowStates {
|
||||
return ""
|
||||
}
|
||||
text = "Floating off"
|
||||
if event.Enabled {
|
||||
text = "Floating on"
|
||||
}
|
||||
case i3ipc.Urgent:
|
||||
if !settings.I3Urgency {
|
||||
return ""
|
||||
}
|
||||
text = "Urgent"
|
||||
if event.Window != "" {
|
||||
text += ": " + event.Window
|
||||
}
|
||||
if event.Workspace != "" {
|
||||
text += ", workspace " + event.Workspace
|
||||
}
|
||||
return text
|
||||
case i3ipc.MarkAdded, i3ipc.MarkRemoved:
|
||||
if !settings.I3Marks {
|
||||
return ""
|
||||
}
|
||||
text = "Mark added: " + event.Name
|
||||
if event.Kind == i3ipc.MarkRemoved {
|
||||
text = "Mark removed: " + event.Name
|
||||
}
|
||||
case i3ipc.WorkspaceMove:
|
||||
if !settings.I3WorkspaceMoves || event.Workspace == "" {
|
||||
return ""
|
||||
}
|
||||
text = "Moved to workspace " + event.Workspace
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
if !event.Focused {
|
||||
name := event.Window
|
||||
if name == "" {
|
||||
name = "Window"
|
||||
}
|
||||
text = name + ": " + text
|
||||
}
|
||||
return text
|
||||
}
|
||||
|
||||
func (controller i3Announcements) present(event i3ipc.Event) {
|
||||
settings := controller.live.Load()
|
||||
text := i3Message(event, settings.Reading)
|
||||
if text == "" {
|
||||
return
|
||||
}
|
||||
controller.output.Enqueue(speechRequest{Text: text, Role: "system", Cause: "i3-desktop", Append: true, BypassFocusGate: true})
|
||||
}
|
||||
|
||||
func i3Service(controller i3Announcements) lifecycle.Service {
|
||||
var cancel context.CancelFunc
|
||||
var done chan struct{}
|
||||
return lifecycle.Service{Name: "i3 desktop", Start: func(ctx context.Context) error {
|
||||
ctx, cancel = context.WithCancel(ctx)
|
||||
done = make(chan struct{})
|
||||
go func() { defer close(done); i3ipc.Run(ctx, xorg.I3Socket, controller.present) }()
|
||||
return nil
|
||||
}, Stop: func(ctx context.Context) error {
|
||||
if cancel == nil {
|
||||
return nil
|
||||
}
|
||||
cancel()
|
||||
select {
|
||||
case <-done:
|
||||
return nil
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
}
|
||||
}}
|
||||
}
|
||||
Reference in new issue
Block a user