After commit c018f5fd29 Fenrir is being switched over to go. If you do not want to be part of the testing process, please switch to master.

This commit is contained in:
Storm Dragon committed 2026-10-27 20:30:55 -04:00
1 parent c018f5fd29
commit 0e50de4c76
1047 files changed
+58364 -63747

No files matched your search

+230
View File
@@ -0,0 +1,230 @@
// 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 <https://www.gnu.org/licenses/gpl-3.0.html>.
package review
import (
"strconv"
"strings"
"unicode"
"git.stormux.org/storm/fenrir/internal/command"
"git.stormux.org/storm/fenrir/internal/i18n"
"git.stormux.org/storm/fenrir/internal/presentation"
)
type TableCell struct {
start, end int // trimmed, half-open Unicode cell bounds; empty cells retained
}
// Match Python's delimiter preference and two-or-more-space aligned columns.
// Bounds come directly from screen positions, never byte offsets or searches
// for cell text (which can repeat within a row).
func table_cells(line []rune) []TableCell {
end := len(line)
for end > 0 && (line[end-1] == ' ' || line[end-1] == 0) {
end--
}
if end == 0 {
return nil
}
var separator rune
for _, candidate := range []rune{',', '|', ';', '\t'} {
if strings.ContainsRune(string(line[:end]), candidate) {
separator = candidate
break
}
}
var cells []TableCell
appendCell := func(start, end int) {
for start < end && unicode.IsSpace(line[start]) {
start++
}
for end > start && unicode.IsSpace(line[end-1]) {
end--
}
cells = append(cells, TableCell{start: start, end: end})
}
start := 0
for index := 0; index < end; index++ {
if separator != 0 {
if line[index] == separator {
appendCell(start, index)
start = index + 1
}
continue
}
if line[index] != ' ' {
continue
}
gap := index
for index+1 < end && line[index+1] == ' ' {
index++
}
if index-gap >= 1 {
// Leading indentation is not an empty aligned column.
if gap > start {
appendCell(start, gap)
}
start = index + 1
}
}
appendCell(start, end)
return cells
}
func (r *Review) Set_table_mode(enabled bool) {
if r.tableMode == enabled {
return
}
r.tableMode = enabled
r.tableHeaders = nil
if enabled && r.ready {
r.sync_table_column()
}
}
func (r *Review) sync_table_column() {
cells := table_cells(r.screen.Line(r.y))
r.tableColumn = 0
for index, cell := range cells {
if r.x < cell.start {
break
}
r.tableColumn = index
}
}
func (r *Review) column_header() string {
if r.tableColumn < len(r.tableHeaders) && r.tableHeaders[r.tableColumn] != "" {
return r.tableHeaders[r.tableColumn]
}
// Python's generic column label is not a gettext message ID.
return "Column " + strconv.Itoa(r.tableColumn+1)
}
func (r *Review) table_cell_text(cells []TableCell) string {
cell := cells[r.tableColumn]
text := string(r.screen.Line(r.y)[cell.start:cell.end])
if text == "" {
text = i18n.T("blank")
}
return text + " " + r.column_header()
}
func (r *Review) set_table_headers() presentation.Message {
cells := table_cells(r.screen.Line(r.y))
if len(cells) == 0 {
return presentation.Message{Text: i18n.T("could not set header row")}
}
r.tableHeaders = nil
for _, cell := range cells {
r.tableHeaders = append(r.tableHeaders, string(r.screen.Line(r.y)[cell.start:cell.end]))
}
return presentation.Message{Text: i18n.T("header row set"), Cue: presentation.PlaceStartMark}
}
func (r *Review) table_command(id command.ID) (string, bool) {
switch id {
case command.CurrentWord, command.PreviousWord, command.NextWord, command.LineStart, command.LineEnd,
command.CurrentChar, command.PreviousChar, command.NextChar, command.FirstText, command.LastText:
default:
return "", false
}
cells := table_cells(r.screen.Line(r.y))
if len(cells) == 0 {
return i18n.T("no table data"), true
}
if id == command.LineStart {
r.tableColumn = 0
} else if id == command.LineEnd {
r.tableColumn = len(cells) - 1
}
if r.tableColumn >= len(cells) {
return i18n.T("no table data"), true
}
switch id {
case command.CurrentWord:
return r.table_cell_text(cells), true
case command.PreviousWord, command.NextWord, command.LineStart, command.LineEnd:
boundary := ""
switch id {
case command.PreviousWord:
if r.tableColumn == 0 {
boundary = i18n.T("start of line")
r.result.After.Cue = presentation.LineStart
} else {
r.tableColumn--
}
case command.NextWord:
if r.tableColumn == len(cells)-1 {
boundary = i18n.T("end of line")
r.result.After.Cue = presentation.LineEnd
} else {
r.tableColumn++
}
case command.LineStart:
r.tableColumn = 0
case command.LineEnd:
r.tableColumn = len(cells) - 1
}
r.x = min(cells[r.tableColumn].start, r.screen.Cols-1)
text := r.table_cell_text(cells)
if boundary != "" {
r.result.After.Text = boundary
} else if id == command.LineStart {
r.result.After.Text = i18n.T("first cell")
} else if id == command.LineEnd {
r.result.After.Text = i18n.T("last cell")
}
return text, true
}
cell := cells[r.tableColumn]
first, last := min(cell.start, r.screen.Cols-1), max(cell.start, cell.end-1)
last = min(last, r.screen.Cols-1)
if r.x < first || r.x > last {
r.x = first
}
character := func() string {
if cell.start == cell.end {
r.result.Cue = presentation.Blank
return i18n.T("blank")
}
return r.currentCharacter()
}
switch id {
case command.PreviousChar:
if r.x == first {
r.result.After.Text, r.result.After.Cue = i18n.T("start of cell"), presentation.LineStart
return character(), true
}
r.x--
case command.NextChar:
if r.x == last {
r.result.After.Text, r.result.After.Cue = i18n.T("end of cell"), presentation.LineEnd
return character(), true
}
r.x++
case command.FirstText:
r.x = first
return character() + " " + i18n.Format("first character in cell {0}", r.column_header()), true
case command.LastText:
r.x = last
return character() + " " + i18n.Format("last character in cell {0}", r.column_header()), true
}
return character(), true
}