Show square brackets in the neoirc-cli message window as written (closes #233)
check / check (push) Waiting to run
check / check (push) Waiting to run
The message window has colour tags on, so tview read square-bracket text in nicks, channel names, reasons, topics and message text as tags. It dropped that text or changed the colour of the rest of the line. A new escape in internal/cli/ui.go writes each "[" as "[[:]", and app.go applies it to every part of a line that comes from the user or the server. tview.Escape is not used: it misses bracket pairs holding symbols such as "|", and tags with a URL. Lines for other server commands now show the command, such as "[MODE]", which tview had read as a tag. Model: opus-5-5
This commit is contained in:
@@ -8,6 +8,7 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/gdamore/tcell/v2"
|
||||
api "sneak.berlin/go/neoirc/internal/cli/api"
|
||||
@@ -845,6 +846,109 @@ func TestOwnEventFromOtherClient(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestBracketsInMessageWindow checks that each kind of line drawn
|
||||
// from a server event shows nicks, texts, reasons and topics
|
||||
// holding square brackets as written.
|
||||
func TestBracketsInMessageWindow(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Every event is sent to the channel and has a body of one line.
|
||||
tests := []struct {
|
||||
name string
|
||||
command string
|
||||
from string
|
||||
body string
|
||||
// Whether the line goes to the channel's window rather than
|
||||
// the status window, which is the one shown at start.
|
||||
inChannel bool
|
||||
// How the line ends, after the timestamps.
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "message", command: irc.CmdPrivmsg, inChannel: true,
|
||||
from: "bob[|[]", body: "x[^[] [:::y|z] [red]",
|
||||
want: "<bob[|[]> x[^[] [:::y|z] [red]",
|
||||
},
|
||||
{
|
||||
name: "joined", command: irc.CmdJoin, from: "x[^[]", inChannel: true,
|
||||
want: "*** x[^[] has joined #chan",
|
||||
},
|
||||
{
|
||||
name: "left", command: irc.CmdPart, inChannel: true,
|
||||
from: "erin[away]", body: "[:::y|z]",
|
||||
want: "*** erin[away] has left #chan ([:::y|z])",
|
||||
},
|
||||
{
|
||||
name: "topic", command: irc.CmdTopic, inChannel: true,
|
||||
from: "dave[afk]", body: "[red]news",
|
||||
want: "*** dave[afk] set topic: [red]news",
|
||||
},
|
||||
{
|
||||
name: "quit", command: irc.CmdQuit, from: "carol[x]", body: "bye [red]",
|
||||
want: "*** carol[x] has quit (bye [red])",
|
||||
},
|
||||
{
|
||||
name: "nick change", command: irc.CmdNick, from: "erin[m]", body: "erin[x]",
|
||||
want: "*** erin[m] is now known as erin[x]",
|
||||
},
|
||||
{
|
||||
name: "notice", command: irc.CmdNotice, from: "bob[]", body: "[::b]note",
|
||||
want: "--bob[]-- [::b]note",
|
||||
},
|
||||
{
|
||||
name: "other command", command: irc.CmdMode, body: "+v dave[afk]",
|
||||
want: "[MODE] +v dave[afk]",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
app := &App{ui: NewUI(), nick: testNick}
|
||||
screen := runUI(t, app.ui)
|
||||
|
||||
if test.inChannel {
|
||||
app.ui.SwitchToBuffer(testChannel)
|
||||
}
|
||||
|
||||
app.handleServerMessage(&api.Message{
|
||||
Command: test.command, From: test.from, To: testChannel,
|
||||
Body: []string{test.body},
|
||||
})
|
||||
|
||||
got, _ := shownLine(app.ui, screen)
|
||||
if !strings.HasSuffix(got, test.want) {
|
||||
t.Fatalf("message window shows %q, want it to end %q", got, test.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestColourTagInMessage checks that a colour tag in a message
|
||||
// shows as written and leaves the message text white, as the
|
||||
// client draws it.
|
||||
func TestColourTagInMessage(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const want = "<bob> [red]x"
|
||||
|
||||
app := &App{ui: NewUI(), nick: testNick}
|
||||
screen := runUI(t, app.ui)
|
||||
app.ui.SwitchToBuffer(testChannel)
|
||||
|
||||
app.handleServerMessage(&api.Message{
|
||||
Command: irc.CmdPrivmsg, From: testOtherNick, To: testChannel,
|
||||
Body: []string{"[red]x"},
|
||||
})
|
||||
|
||||
got, colour := shownLine(app.ui, screen)
|
||||
if !strings.HasSuffix(got, want) || colour != tcell.ColorWhite {
|
||||
t.Fatalf("message window shows %q, its last character in %v; "+
|
||||
"want it to end %q, in white", got, colour, want)
|
||||
}
|
||||
}
|
||||
|
||||
// bufferState returns the number of lines in the named
|
||||
// buffer and how many of them are unread.
|
||||
func bufferState(ui *UI, name string) (int, int) {
|
||||
@@ -872,6 +976,39 @@ func statusBarText(ui *UI) string {
|
||||
return text
|
||||
}
|
||||
|
||||
// shownLine returns the first line of the message window as the
|
||||
// screen shows it, without the blanks after it, and the colour of
|
||||
// its last character.
|
||||
func shownLine(
|
||||
ui *UI, screen tcell.SimulationScreen,
|
||||
) (string, tcell.Color) {
|
||||
var (
|
||||
text string
|
||||
colour tcell.Color
|
||||
)
|
||||
|
||||
ui.app.QueueUpdate(func() {
|
||||
cells, width, _ := screen.GetContents()
|
||||
|
||||
var row strings.Builder
|
||||
|
||||
for _, cell := range cells[:width] {
|
||||
row.WriteString(string(cell.Runes))
|
||||
}
|
||||
|
||||
text = strings.TrimRight(row.String(), " ")
|
||||
if text == "" {
|
||||
return
|
||||
}
|
||||
|
||||
// Each character in these tests takes one cell.
|
||||
last := cells[utf8.RuneCountInString(text)-1]
|
||||
colour, _, _ = last.Style.Decompose()
|
||||
})
|
||||
|
||||
return text, colour
|
||||
}
|
||||
|
||||
// handledLines makes app.ui pass each typed line to
|
||||
// app.handleInput, as Run does, and returns a channel that
|
||||
// receives each line once it has been handled.
|
||||
@@ -964,11 +1101,15 @@ func waitPoll(t *testing.T, polled <-chan struct{}, server string) {
|
||||
}
|
||||
}
|
||||
|
||||
// runUI runs ui on a simulated screen until the test ends.
|
||||
func runUI(t *testing.T, ui *UI) {
|
||||
// runUI runs ui on a simulated screen until the test ends, and
|
||||
// returns the screen.
|
||||
//
|
||||
//nolint:ireturn // tcell has no exported type for the screen
|
||||
func runUI(t *testing.T, ui *UI) tcell.SimulationScreen {
|
||||
t.Helper()
|
||||
|
||||
ui.app.SetScreen(tcell.NewSimulationScreen(""))
|
||||
screen := tcell.NewSimulationScreen("")
|
||||
ui.app.SetScreen(screen)
|
||||
|
||||
// One slot, so that Run's goroutine can end even after the
|
||||
// cleanup below has stopped waiting for it.
|
||||
@@ -988,4 +1129,6 @@ func runUI(t *testing.T, ui *UI) {
|
||||
t.Error("the UI did not stop: its event loop stopped responding")
|
||||
}
|
||||
})
|
||||
|
||||
return screen
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user