101 lines
2.9 KiB
Go
101 lines
2.9 KiB
Go
// 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 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
|
|
}
|