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:
1 parent
c018f5fd29
commit
0e50de4c76
1047 files changed
+58364
-63747
No files matched your search
@@ -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
|
||||
}
|
||||
Reference in new issue
Block a user