// Copyright (c) 2026 Storm Dragon // Authors: Storm Dragon, Angus Kola // SPDX-License-Identifier: GPL-3.0-or-later package presentation import ( "fmt" "slices" "strings" "git.stormux.org/storm/cthulhu/internal/atspi" ) func calcColumnLabel(column int) string { label := "" for column++; column > 0; column = (column - 1) / 26 { label = string(rune('A'+(column-1)%26)) + label } return label } func calcCellLabel(cell atspi.LibreOfficeCell) string { return fmt.Sprintf("%s%d", calcColumnLabel(cell.Column), cell.Row+1) } // LibreOfficeSelectionText formats copied metadata only. A large selection's // endpoints never imply a filled rectangle. Small disjoint selections retain // their gaps; at most eight rectangles are spoken before using a count. func LibreOfficeSelectionText(selection atspi.LibreOfficeSelection, action string) string { if selection.Kind == "all" { return "All cells " + action + "." } if selection.Count == 0 { return "" } if !selection.Complete || len(selection.Cells) > 64 { return libreOfficeSelectionCount(selection, action) } cells := slices.Clone(selection.Cells) slices.SortFunc(cells, func(a, b atspi.LibreOfficeCell) int { if a.Row != b.Row { return a.Row - b.Row } return a.Column - b.Column }) cells = slices.Compact(cells) type rectangle struct{ first, last atspi.LibreOfficeCell } var ranges []rectangle for index := 0; index < len(cells); { first, last := cells[index], cells[index] index++ for index < len(cells) && (selection.Kind == "rows" && cells[index].Row == last.Row+1 || selection.Kind != "rows" && cells[index].Row == last.Row && cells[index].Column == last.Column+1) { last = cells[index] index++ } merged := false if selection.Kind == "cells" { for i := range ranges { previous := &ranges[i] if previous.first.Column == first.Column && previous.last.Column == last.Column && previous.last.Row+1 == first.Row { previous.last.Row = last.Row merged = true break } } } if !merged { ranges = append(ranges, rectangle{first, last}) } } if len(ranges) > 8 { return libreOfficeSelectionCount(selection, action) } var messages []string for _, span := range ranges { first, last := calcCellLabel(span.first), calcCellLabel(span.last) singular, plural := "Cell", "Cells" if selection.Kind == "rows" { first, last = fmt.Sprint(span.first.Row+1), fmt.Sprint(span.last.Row+1) singular, plural = "Row", "Rows" } else if selection.Kind == "columns" { first, last = calcColumnLabel(span.first.Column), calcColumnLabel(span.last.Column) singular, plural = "Column", "Columns" } if first == last { messages = append(messages, fmt.Sprintf("%s %s %s.", singular, first, action)) } else { messages = append(messages, fmt.Sprintf("%s %s to %s %s.", plural, first, last, action)) } } return strings.Join(messages, " ") } func libreOfficeSelectionCount(selection atspi.LibreOfficeSelection, action string) string { prefix := "" if selection.CountLimited { prefix = "At least " } return fmt.Sprintf("%s%d %s %s.", prefix, selection.Count, selection.Kind, action) }