ai-agent/internal/tui/accessibility.go
admin 8dc496b626
Some checks failed
CI / test (push) Has been cancelled
Release / release (push) Failing after 4m36s
first commit
2026-03-08 15:40:34 +07:00

241 lines
5.9 KiB
Go

package tui
import (
"fmt"
"strings"
"charm.land/lipgloss/v2"
)
// AccessibilityHelper provides accessibility features like screen reader support.
type AccessibilityHelper struct {
isDark bool
styles AccessibilityStyles
speakFunc func(string) // Function to speak text (for screen readers)
announceFunc func(string) // Function to announce changes
}
// AccessibilityStyles holds styling.
type AccessibilityStyles struct {
Announce lipgloss.Style
}
// DefaultAccessibilityStyles returns default styles.
func DefaultAccessibilityStyles(isDark bool) AccessibilityStyles {
return AccessibilityStyles{
Announce: lipgloss.NewStyle().Foreground(lipgloss.Color("#88c0d0")),
}
}
// NewAccessibilityHelper creates a new accessibility helper.
func NewAccessibilityHelper(isDark bool) *AccessibilityHelper {
return &AccessibilityHelper{
isDark: isDark,
styles: DefaultAccessibilityStyles(isDark),
}
}
// SetDark updates theme.
func (ah *AccessibilityHelper) SetDark(isDark bool) {
ah.isDark = isDark
ah.styles = DefaultAccessibilityStyles(isDark)
}
// SetSpeakFunc sets the function to speak text.
func (ah *AccessibilityHelper) SetSpeakFunc(f func(string)) {
ah.speakFunc = f
}
// SetAnnounceFunc sets the function to announce changes.
func (ah *AccessibilityHelper) SetAnnounceFunc(f func(string)) {
ah.announceFunc = f
}
// Announce announces a message to the user.
func (ah *AccessibilityHelper) Announce(format string, args ...string) {
if ah.announceFunc != nil {
msg := format
if len(args) > 0 {
msg = fmt.Sprintf(format, args)
}
ah.announceFunc(msg)
}
}
// Speak speaks text directly.
func (ah *AccessibilityHelper) Speak(text string) {
if ah.speakFunc != nil {
ah.speakFunc(text)
}
}
// DescribeEntry creates an accessibility description for a chat entry.
func (ah *AccessibilityHelper) DescribeEntry(entry ChatEntry, index int, toolCount int) string {
var desc strings.Builder
switch entry.Kind {
case "user":
desc.WriteString("User message")
case "assistant":
desc.WriteString("Assistant response")
if entry.ThinkingContent != "" {
desc.WriteString(", has thinking")
}
case "tool_group":
desc.WriteString("Tool execution")
if index >= 0 && index < toolCount {
desc.WriteString(", tool result")
}
case "system":
desc.WriteString("System message")
case "error":
desc.WriteString("Error")
}
// Add content preview
if entry.Content != "" {
preview := truncateStr(entry.Content, 50)
desc.WriteString(": ")
desc.WriteString(preview)
}
return desc.String()
}
// DescribeState creates an accessibility description of the current state.
func (ah *AccessibilityHelper) DescribeState(state State, model, mode string) string {
var desc string
switch state {
case StateIdle:
desc = "Ready"
case StateWaiting:
desc = "Waiting for response"
case StateStreaming:
desc = "Receiving response"
}
if model != "" {
desc += ", model: " + model
}
if mode != "" {
desc += ", mode: " + mode
}
return desc
}
// DescribeOverlay creates an accessibility description of the current overlay.
func (ah *AccessibilityHelper) DescribeOverlay(overlay OverlayKind) string {
switch overlay {
case OverlayNone:
return ""
case OverlayHelp:
return "Help overlay open"
case OverlayCompletion:
return "Completion menu open"
case OverlayModelPicker:
return "Model picker open"
case OverlayPlanForm:
return "Plan form open"
case OverlaySessionsPicker:
return "Sessions picker open"
default:
return "Overlay open"
}
}
// DescribeTools creates an accessibility description of tool status.
func (ah *AccessibilityHelper) DescribeTools(pending, total int) string {
if pending == 0 && total == 0 {
return "No tools running"
}
if pending > 0 {
return fmt.Sprintf("%d tool running", pending)
}
return fmt.Sprintf("%d tools completed", total)
}
// truncate truncates a string to maxLength.
func truncateStr(s string, maxLength int) string {
if len(s) <= maxLength {
return s
}
return s[:maxLength-3] + "..."
}
// AccessibilityLabel returns an accessibility label for a view element.
func AccessibilityLabel(role, name string, props ...string) string {
var b strings.Builder
b.WriteString(role)
b.WriteString(": ")
b.WriteString(name)
for _, p := range props {
b.WriteString(", ")
b.WriteString(p)
}
return b.String()
}
// FocusOrder represents the focus order for keyboard navigation.
type FocusOrder struct {
Current int
Items []Focusable
}
// Focusable is an interface for focusable elements.
type Focusable interface {
Focus() error
Blur() error
IsFocused() bool
}
// NewFocusOrder creates a new focus order.
func NewFocusOrder(items []Focusable) *FocusOrder {
return &FocusOrder{
Current: 0,
Items: items,
}
}
// Next moves focus to the next item.
func (fo *FocusOrder) Next() {
if len(fo.Items) == 0 {
return
}
fo.Current = (fo.Current + 1) % len(fo.Items)
fo.focusCurrent()
}
// Prev moves focus to the previous item.
func (fo *FocusOrder) Prev() {
if len(fo.Items) == 0 {
return
}
fo.Current--
if fo.Current < 0 {
fo.Current = len(fo.Items) - 1
}
fo.focusCurrent()
}
// Current returns the currently focused item.
func (fo *FocusOrder) CurrentItem() Focusable {
if fo.Current >= 0 && fo.Current < len(fo.Items) {
return fo.Items[fo.Current]
}
return nil
}
func (fo *FocusOrder) focusCurrent() {
for i, item := range fo.Items {
if i == fo.Current {
item.Focus()
} else {
item.Blur()
}
}
}