// 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 clipboard owns text history and asynchronous clipboard transfers. package clipboard import ( "fmt" "unicode/utf8" ) // MaxBytes bounds individual entries and file/helper messages. const MaxBytes = 4 << 20 // History belongs to the application loop. New entries select the newest item; // navigation wraps, matching Python Fenrir. Clear removes the entire history. type History struct { entries []string index int limit int } func New_history(limit int) *History { return &History{limit: max(1, limit)} } func Validate_text(text string) error { if len(text) > MaxBytes || !utf8.ValidString(text) { return fmt.Errorf("clipboard text must be valid UTF-8 and at most 4 MiB") } return nil } func (h *History) Add(text string) error { if err := Validate_text(text); err != nil { return err } h.entries = append([]string{text}, h.entries[:min(len(h.entries), h.limit-1)]...) h.index = 0 return nil } func (h *History) Current() (string, bool) { if len(h.entries) == 0 { return "", false } return h.entries[h.index], true } func (h *History) Move(delta int) { if len(h.entries) > 0 { h.index = (h.index + delta + len(h.entries)) % len(h.entries) } } func (h *History) First() { h.index = 0 } func (h *History) Last() { h.index = max(0, len(h.entries)-1) } func (h *History) At_first() bool { return len(h.entries) > 0 && h.index == 0 } func (h *History) At_last() bool { return len(h.entries) > 0 && h.index == len(h.entries)-1 } func (h *History) Clear() { h.entries, h.index = nil, 0 } // Refresh adopts shared entries while retaining this reader's selected copy. // Count duplicates from the oldest end so a new identical copy stays distinct. func (h *History) Refresh(state Snapshot) error { text, selected := h.Current() occurrence := 0 if selected { for _, entry := range h.entries[h.index:] { if entry == text { occurrence++ } } } if err := h.Restore(state); err != nil { return err } if selected { for i := len(h.entries) - 1; i >= 0; i-- { if h.entries[i] == text { occurrence-- if occurrence == 0 { h.index = i break } } } } return nil }