// Fenrir console screen reader. // Copyright (c) 2026 Storm Dragon // SPDX-License-Identifier: GPL-3.0-or-later // // This program is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by the Free // Software Foundation, either version 3 of the License, or (at your option) // any later version. // // This program is distributed in the hope that it will be useful, but WITHOUT // ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or // FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for // more details. // // You should have received a copy of the GNU General Public License along with // this program. If not, see . package internalui import ( "strconv" "strings" "unicode/utf8" ) func (s *Session) Open() string { return s.messages.T(s.form.Title) + ". " + s.Describe() } func (s *Session) describe() string { if s.closed { return "" } tab := s.form.Tabs[s.tab] if s.focus == -1 { return s.messages.Format("{0} tab", s.messages.T(tab.Label)) } control := tab.Controls[s.focus] label := s.messages.T(control.Label) switch control.Kind { case CheckBox: state := "unchecked" if control.Checked { state = "checked" } return label + " " + s.messages.T(state) case Number: return label + " " + strconv.FormatFloat(control.Number, 'f', -1, 64) case Entry: return s.Presentation().Speech case List: if control.Selected < 0 { return label } return label + " " + s.list_labels(control)[control.Selected] case Grid: return s.messages.Format("{0} grid. {1}", label, s.describe_grid(control)) case ComboBox: return s.messages.Format("{0} combo box {1}", label, s.describe_choice(control)) case Button: return s.messages.Format("{0} button", label) } return label } func (s *Session) describe_choice(control Control) string { choice := control.Choices[control.Selected] text := s.choice_label(choice) if choice.Unavailable != "" { text += ". " + s.messages.T("unavailable") + ": " + choice.Unavailable } return text } func (s *Session) choice_label(choice Choice) string { if choice.Literal { return choice.Label } return s.messages.T(choice.Label) } func (s *Session) focused_group() string { if s.focus < 0 { return "" } return s.form.Tabs[s.tab].Controls[s.focus].Group } func (s *Session) describe_focus(previousGroup string) string { if group := s.focused_group(); group != "" && group != previousGroup { return s.messages.T(group) + ". " + s.Describe() } return s.Describe() } func (s *Session) Describe() string { return s.Presentation().Speech } func (s *Session) Presentation() Result { if s.closed { return Result{} } if s.focus < 0 { text := s.messages.Format("{0} tab", s.messages.T(s.form.Tabs[s.tab].Label)) return Result{Speech: text, Braille: text, Cursor: -1} } c := s.form.Tabs[s.tab].Controls[s.focus] label := s.messages.T(c.Label) if c.Kind == Entry { template := "{0}, edit: {1}" if !s.form.Detailed { template = "{0} entry {1}" } value := c.Text if c.Secret { template = "{0}, password edit: {1}" value = strings.Repeat("*", utf8.RuneCountInString(c.Text)) } text := s.messages.Format(template, label, value) marker := s.messages.Format(template, label, "\x00") offset := strings.IndexByte(marker, 0) cursor := -1 if offset >= 0 { cursor = len([]rune(marker[:offset])) + s.cursor } if s.cursor != s.anchor { n := s.cursor - s.anchor if n < 0 { n = -n } text += s.messages.Format(" (selected {0} characters)", strconv.Itoa(n)) } speech := text if c.Secret { speech = s.messages.Format("{0}, password edit, {1}", label, s.characterCount(utf8.RuneCountInString(c.Text))) } return Result{Speech: speech, Braille: text, Cursor: cursor} } text := s.describe() speech := text if s.form.Detailed { switch c.Kind { case List: if c.Selected >= 0 { speech = s.list_labels(c)[c.Selected] text = s.messages.Format("{0}: {1}, {2} of {3}", label, speech, strconv.Itoa(c.Selected+1), strconv.Itoa(len(s.list_labels(c)))) } case CheckBox: state := "unchecked" if c.Checked { state = "checked" } text = label + ", " + s.messages.T(state) speech = text case Button: text = s.messages.Format("{0}, button", label) speech = text case Grid: text = s.describe_grid(c) speech = text } } return Result{Speech: speech, Braille: text, Cursor: -1} }