Fix high prio recommendations, add error / health screen

This commit is contained in:
2026-07-28 15:40:11 +02:00
parent 948f3e1a79
commit 7511311297
19 changed files with 1885 additions and 148 deletions

View File

@@ -3,10 +3,12 @@ package main
import (
"strings"
"unicode"
"unicode/utf8"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"github.com/charmbracelet/x/ansi"
"github.com/rivo/uniseg"
)
type textEditorMode string
@@ -69,7 +71,7 @@ func (e *textEditor) handleKeyAtWidth(key tea.KeyMsg, multiline bool, wrapWidth
e.clearPending()
start, _ := editorLineBounds(e.Text, e.Cursor, wrapWidth)
if e.Cursor > start {
e.Cursor--
e.Cursor = max(start, previousGraphemeBoundary(e.Text, e.Cursor))
}
return true
}
@@ -92,10 +94,10 @@ func (e *textEditor) handleStandardKey(key tea.KeyMsg, multiline bool, wrapWidth
switch {
case key.Type != tea.KeyRunes && key.Type != tea.KeySpace &&
keyMatches(k, e.keys.Navigation.Left):
e.Cursor = max(0, e.Cursor-1)
e.Cursor = previousGraphemeBoundary(e.Text, e.Cursor)
case key.Type != tea.KeyRunes && key.Type != tea.KeySpace &&
keyMatches(k, e.keys.Navigation.Right):
e.Cursor = min(len([]rune(e.Text)), e.Cursor+1)
e.Cursor = nextGraphemeBoundary(e.Text, e.Cursor)
case key.Type != tea.KeyRunes && key.Type != tea.KeySpace &&
keyMatches(k, e.keys.Navigation.Up):
if !multiline {
@@ -130,6 +132,7 @@ func (e *textEditor) handleStandardKey(key tea.KeyMsg, multiline bool, wrapWidth
return false
}
}
e.Cursor = previousOrCurrentGraphemeBoundary(e.Text, e.Cursor)
return true
}
@@ -166,7 +169,7 @@ func (e *textEditor) handleNormalKey(key tea.KeyMsg, multiline bool, wrapWidth i
e.Mode = textEditorInsert
case keyMatches(k, e.keys.Vim.Append):
_, end := editorLineBounds(e.Text, e.Cursor, wrapWidth)
e.Cursor = min(end, e.Cursor+1)
e.Cursor = min(end, nextGraphemeBoundary(e.Text, e.Cursor))
e.Mode = textEditorInsert
case keyMatches(k, e.keys.Vim.InsertLineStart):
e.Cursor = firstNonBlankAtWidth(e.Text, e.Cursor, wrapWidth)
@@ -192,9 +195,12 @@ func (e *textEditor) handleNormalKey(key tea.KeyMsg, multiline bool, wrapWidth i
e.Mode = textEditorInsert
case keyMatches(k, e.keys.Navigation.Left):
start, _ := editorLineBounds(e.Text, e.Cursor, wrapWidth)
e.Cursor = max(start, e.Cursor-1)
e.Cursor = max(start, previousGraphemeBoundary(e.Text, e.Cursor))
case keyMatches(k, e.keys.Navigation.Right):
e.Cursor = min(normalEditorLineLast(e.Text, e.Cursor, wrapWidth), e.Cursor+1)
e.Cursor = min(
normalEditorLineLast(e.Text, e.Cursor, wrapWidth),
nextGraphemeBoundary(e.Text, e.Cursor),
)
case keyMatches(k, e.keys.Navigation.Down):
if multiline {
e.Cursor = moveEditorCursorLine(e.Text, e.Cursor, 1, wrapWidth, true)
@@ -266,6 +272,7 @@ func (e *textEditor) handleNormalKey(key tea.KeyMsg, multiline bool, wrapWidth i
default:
return false
}
e.Cursor = previousOrCurrentGraphemeBoundary(e.Text, e.Cursor)
return true
}
@@ -347,7 +354,9 @@ func (e textEditor) selectionBounds(wrapWidth int) (int, int, bool) {
anchor := clamp(e.visualAnchor, 0, len(runes))
cursor := clamp(e.Cursor, 0, len(runes))
if !e.visualLine {
start, end := min(anchor, cursor), min(len(runes), max(anchor, cursor)+1)
start := previousOrCurrentGraphemeBoundary(e.Text, min(anchor, cursor))
end := nextGraphemeBoundary(e.Text, max(anchor, cursor))
end = min(len(runes), end)
return start, end, end > start
}
anchorStart, anchorEnd := editorLineBounds(e.Text, anchor, wrapWidth)
@@ -386,7 +395,7 @@ func (e *textEditor) deleteSelection(wrapWidth int) {
e.Text = string(append(runes[:start], runes[end:]...))
e.Cursor = min(start, len([]rune(e.Text)))
if e.Cursor == len([]rune(e.Text)) && e.Cursor > 0 {
e.Cursor--
e.Cursor = previousGraphemeBoundary(e.Text, e.Cursor)
}
e.stopVisual()
}
@@ -411,11 +420,11 @@ func (e *textEditor) pasteClipboard(replaceSelection bool, wrapWidth int) {
e.stopVisual()
} else {
_, end := editorLineBounds(e.Text, e.Cursor, 0)
e.Cursor = min(end, e.Cursor+1)
e.Cursor = min(end, nextGraphemeBoundary(e.Text, e.Cursor))
}
e.insert(value)
if e.Cursor > 0 {
e.Cursor--
e.Cursor = previousGraphemeBoundary(e.Text, e.Cursor)
}
}
@@ -442,8 +451,9 @@ func (e *textEditor) deleteBefore() {
if e.Cursor == 0 {
return
}
value = append(value[:e.Cursor-1], value[e.Cursor:]...)
e.Cursor--
start := previousGraphemeBoundary(e.Text, e.Cursor)
value = append(value[:start], value[e.Cursor:]...)
e.Cursor = start
e.Text = string(value)
}
@@ -453,10 +463,56 @@ func (e *textEditor) deleteAt() {
if e.Cursor == len(value) {
return
}
value = append(value[:e.Cursor], value[e.Cursor+1:]...)
end := nextGraphemeBoundary(e.Text, e.Cursor)
value = append(value[:e.Cursor], value[end:]...)
e.Text = string(value)
}
func graphemeBoundaries(value string) []int {
boundaries := []int{0}
graphemes := uniseg.NewGraphemes(value)
offset := 0
for graphemes.Next() {
offset += utf8.RuneCountInString(graphemes.Str())
boundaries = append(boundaries, offset)
}
return boundaries
}
func previousGraphemeBoundary(value string, cursor int) int {
cursor = clamp(cursor, 0, len([]rune(value)))
previous := 0
for _, boundary := range graphemeBoundaries(value) {
if boundary >= cursor {
return previous
}
previous = boundary
}
return previous
}
func previousOrCurrentGraphemeBoundary(value string, cursor int) int {
cursor = clamp(cursor, 0, len([]rune(value)))
previous := 0
for _, boundary := range graphemeBoundaries(value) {
if boundary > cursor {
return previous
}
previous = boundary
}
return previous
}
func nextGraphemeBoundary(value string, cursor int) int {
cursor = clamp(cursor, 0, len([]rune(value)))
for _, boundary := range graphemeBoundaries(value) {
if boundary > cursor {
return boundary
}
}
return len([]rune(value))
}
func (e *textEditor) performFind(command, target rune, remember bool, wrapWidth int) {
runes := []rune(e.Text)
cursor := clamp(e.Cursor, 0, len(runes))