241 lines
5.9 KiB
Go
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()
|
|
}
|
|
}
|
|
}
|