Fix chat redraw and input history

This commit is contained in:
Storm Dragon
2026-07-14 19:08:38 -04:00
parent 9bc514e74f
commit 7fed25e8d0
5 changed files with 240 additions and 6 deletions
+1
View File
@@ -162,6 +162,7 @@ You can see the below keystrokes in your config file.
Pressing tab inside the main window switches between the user/channel tree view and the message input box.
When in the message input box:
* left and right arrow keys move by character
* up and down arrow keys move through previously submitted input
* home/end moves to the beginning/end of the text respectively
* enter submits the entered message
+72 -1
View File
@@ -17,6 +17,9 @@ type Textbox struct {
active bool
x0, y0, x1, y1 int
pos int
history []string
historyIndex int
historyDraft string
}
func (t *Textbox) uiInitialize(ui *Ui) {
@@ -100,14 +103,20 @@ func (t *Textbox) uiKeyEvent(key Key) {
case KeyCtrlC:
t.Text = ""
t.pos = 0
t.resetHistoryNavigation()
redraw = true
case KeyEnter:
text := t.Text
if t.Input != nil {
t.Input(t.ui, t, t.Text)
t.Input(t.ui, t, text)
}
t.addHistory(text)
t.Text = ""
t.pos = 0
t.resetHistoryNavigation()
redraw = true
case KeyArrowUp, KeyAltArrowUp, KeyArrowDown, KeyAltArrowDown:
redraw = t.handleHistoryKey(key)
case KeySpace:
t.uiCharacterEvent(' ')
case KeyBackspace:
@@ -136,3 +145,65 @@ func (t *Textbox) uiCharacterEvent(chr rune) {
t.pos += len(s)
t.uiDraw()
}
func (t *Textbox) setText(text string) {
t.Text = text
t.pos = len(text)
}
func (t *Textbox) addHistory(text string) {
if strings.TrimSpace(text) == "" {
return
}
if len(t.history) > 0 && t.history[len(t.history)-1] == text {
t.historyIndex = len(t.history)
return
}
t.history = append(t.history, text)
t.historyIndex = len(t.history)
}
func (t *Textbox) handleHistoryKey(key Key) bool {
switch key {
case KeyArrowUp, KeyAltArrowUp:
return t.previousHistory()
case KeyArrowDown, KeyAltArrowDown:
return t.nextHistory()
default:
return false
}
}
func (t *Textbox) previousHistory() bool {
if len(t.history) == 0 {
return false
}
if t.historyIndex == len(t.history) {
t.historyDraft = t.Text
}
if t.historyIndex > 0 {
t.historyIndex--
}
t.setText(t.history[t.historyIndex])
return true
}
func (t *Textbox) nextHistory() bool {
if len(t.history) == 0 || t.historyIndex == len(t.history) {
return false
}
if t.historyIndex < len(t.history)-1 {
t.historyIndex++
t.setText(t.history[t.historyIndex])
return true
}
t.historyIndex = len(t.history)
t.setText(t.historyDraft)
t.historyDraft = ""
return true
}
func (t *Textbox) resetHistoryNavigation() {
t.historyIndex = len(t.history)
t.historyDraft = ""
}
+93
View File
@@ -0,0 +1,93 @@
package uiterm
import "testing"
func TestTextboxHistoryNavigatesSubmittedText(t *testing.T) {
t.Parallel()
textbox := Textbox{}
textbox.addHistory("first")
textbox.addHistory("second")
if !textbox.previousHistory() {
t.Fatal("expected previous history item")
}
if textbox.Text != "second" {
t.Fatalf("expected second history item, got %q", textbox.Text)
}
if !textbox.previousHistory() {
t.Fatal("expected older history item")
}
if textbox.Text != "first" {
t.Fatalf("expected first history item, got %q", textbox.Text)
}
if !textbox.nextHistory() {
t.Fatal("expected newer history item")
}
if textbox.Text != "second" {
t.Fatalf("expected second history item after next, got %q", textbox.Text)
}
}
func TestTextboxHistoryNavigatesAltArrowKeys(t *testing.T) {
t.Parallel()
textbox := Textbox{}
textbox.addHistory("first")
textbox.addHistory("second")
if redraw := textbox.handleHistoryKey(KeyAltArrowUp); !redraw {
t.Fatal("expected alt-up to recall history")
}
if textbox.Text != "second" {
t.Fatalf("expected second history item, got %q", textbox.Text)
}
if redraw := textbox.handleHistoryKey(KeyAltArrowDown); !redraw {
t.Fatal("expected alt-down to move forward through history")
}
if textbox.Text != "" {
t.Fatalf("expected alt-down to restore empty draft, got %q", textbox.Text)
}
}
func TestTextboxHistoryRestoresDraft(t *testing.T) {
t.Parallel()
textbox := Textbox{Text: "draft"}
textbox.addHistory("previous")
textbox.setText("draft")
if !textbox.previousHistory() {
t.Fatal("expected previous history item")
}
if textbox.Text != "previous" {
t.Fatalf("expected previous history item, got %q", textbox.Text)
}
if !textbox.nextHistory() {
t.Fatal("expected history next to restore draft")
}
if textbox.Text != "draft" {
t.Fatalf("expected draft to be restored, got %q", textbox.Text)
}
}
func TestTextboxHistorySkipsBlankAndConsecutiveDuplicateEntries(t *testing.T) {
t.Parallel()
textbox := Textbox{}
textbox.addHistory("")
textbox.addHistory(" ")
textbox.addHistory("repeat")
textbox.addHistory("repeat")
if len(textbox.history) != 1 {
t.Fatalf("expected one history item, got %d", len(textbox.history))
}
if textbox.history[0] != "repeat" {
t.Fatalf("expected repeat history item, got %q", textbox.history[0])
}
}
+28 -5
View File
@@ -77,6 +77,7 @@ func (t *Textview) updateParsedLines() {
if t.Lines == nil || width <= 0 {
t.parsedLines = nil
t.CurrentLine = 0
return
}
@@ -108,6 +109,7 @@ func (t *Textview) updateParsedLines() {
}
}
t.parsedLines = parsed
t.clampCurrentLine()
}
func (t *Textview) AddLine(line string) {
@@ -129,12 +131,9 @@ func (t *Textview) uiDraw() {
var reader *strings.Reader
writeableLines := t.y1 - t.y0
lineNum := 0
if writeableLines < len(t.parsedLines) {
lineNum = len(t.parsedLines) - writeableLines
}
lineNum := t.visibleStartLine(writeableLines)
//Beep()
for y := t.y0; y < writeableLines; y++ {
for y := t.y0; y < t.y1; y++ {
if lineNum < len(t.parsedLines) {
reader = strings.NewReader(t.parsedLines[lineNum])
} else {
@@ -153,6 +152,30 @@ func (t *Textview) uiDraw() {
} //each y
} //func
func (t *Textview) visibleStartLine(writeableLines int) int {
if writeableLines <= 0 || len(t.parsedLines) == 0 {
return 0
}
bottomStart := len(t.parsedLines) - writeableLines
if bottomStart < 0 {
bottomStart = 0
}
start := bottomStart - t.CurrentLine
if start < 0 {
return 0
}
return start
}
func (t *Textview) clampCurrentLine() {
if len(t.parsedLines) == 0 {
t.CurrentLine = 0
return
}
t.CurrentLine = bounded(t.CurrentLine, 0, len(t.parsedLines)-1)
}
func (t *Textview) uiKeyEvent(key Key) {
}
+46
View File
@@ -0,0 +1,46 @@
package uiterm
import "testing"
func TestTextviewVisibleStartLineShowsBottomWhenNotScrolled(t *testing.T) {
t.Parallel()
textview := Textview{
parsedLines: []string{"one", "two", "three", "four"},
}
if got := textview.visibleStartLine(3); got != 1 {
t.Fatalf("expected visible lines to start at 1, got %d", got)
}
}
func TestTextviewVisibleStartLineHonorsScrollOffset(t *testing.T) {
t.Parallel()
textview := Textview{
CurrentLine: 2,
parsedLines: []string{"one", "two", "three", "four", "five"},
}
if got := textview.visibleStartLine(3); got != 0 {
t.Fatalf("expected scrolled view to start at 0, got %d", got)
}
}
func TestTextviewUpdateParsedLinesClampsScrollOffset(t *testing.T) {
t.Parallel()
textview := Textview{
CurrentLine: 10,
Lines: []string{"one", "two"},
showTimestamps: true,
x0: 0,
x1: 20,
}
textview.updateParsedLines()
if textview.CurrentLine != 1 {
t.Fatalf("expected current line to clamp to 1, got %d", textview.CurrentLine)
}
}