Show the neoirc-cli screen without waiting at start-up (closes #226)
check / check (push) Canceled after 0s

Run added the start-up status bar and welcome lines through
SetStatus and AddStatus before it started the tview event loop. Both
wait for that loop, so neoirc-cli stopped before it showed its
screen. They now run on a goroutine, as typed lines do.

Run now passes a new UI to run, which holds the start-up path and
returns its error. TestStartUp calls run with a simulation screen and
fails if the status window does not get the welcome lines within five
seconds.

Model: opus-5-5
This commit is contained in:
2026-11-02 00:20:25 +00:00
parent fe734af375
commit 837275fa92
2 changed files with 97 additions and 19 deletions
+32 -18
View File
@@ -34,30 +34,44 @@ type App struct {
// Run creates and runs the CLI application.
func Run() {
app := &App{ //nolint:exhaustruct
ui: NewUI(),
nick: "guest",
}
app.ui.OnInput(app.handleInput)
app.ui.SetStatus(app.nick, "", "disconnected")
app.ui.AddStatus(
"Welcome to neoirc-cli — an IRC-style client",
)
app.ui.AddStatus(
"Type [yellow]/connect <server-url>" +
"[white] to begin, " +
"or [yellow]/help[white] for commands",
)
err := app.ui.Run()
err := run(NewUI())
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
}
// run starts the client on ui and returns when ui stops. Tests
// call it with ui on a simulation screen.
func run(ui *UI) error {
app := &App{ //nolint:exhaustruct
ui: ui,
nick: "guest",
}
app.ui.OnInput(app.handleInput)
// SetStatus and AddStatus wait for the event loop, which
// app.ui.Run starts below, so they run on a goroutine. The
// nick is read first, as a typed /nick may change app.nick.
nick := app.nick
go func() {
app.ui.SetStatus(nick, "", "disconnected")
app.ui.AddStatus(
"Welcome to neoirc-cli — an IRC-style client",
)
app.ui.AddStatus(
"Type [yellow]/connect <server-url>" +
"[white] to begin, " +
"or [yellow]/help[white] for commands",
)
}()
return app.ui.Run()
}
func (a *App) handleInput(text string) {
if strings.HasPrefix(text, "/") {
a.handleCommand(text)
+65 -1
View File
@@ -22,13 +22,22 @@ const (
)
// eventLoopTimeout is how long a test waits for a typed line to
// be handled, or for the UI to stop, before it fails.
// be handled, for the welcome lines, or for the UI to stop, before
// it fails.
const eventLoopTimeout = 5 * time.Second
// holdTime is how long TestLinesKeepOrder holds the first line,
// which gives the second line time to start too early.
const holdTime = 200 * time.Millisecond
// welcomeLines is the number of lines run adds to the status
// window when the client starts.
const welcomeLines = 2
// checkInterval is how often TestStartUp reads the status window
// while it waits for the welcome lines.
const checkInterval = 10 * time.Millisecond
func TestMemberPrefix(t *testing.T) {
t.Parallel()
@@ -348,6 +357,61 @@ func TestLinesKeepOrder(t *testing.T) {
waitHandled(t, handled, secondLine)
}
// TestStartUp runs the client's start-up path on a simulation
// screen. It fails unless the event loop starts and the status
// window gets the welcome lines.
func TestStartUp(t *testing.T) {
t.Parallel()
ui := NewUI()
ui.app.SetScreen(tcell.NewSimulationScreen(""))
// One slot, so that run's goroutine can end even after the
// cleanup below has stopped waiting for it.
done := make(chan error, 1)
go func() { done <- run(ui) }()
t.Cleanup(func() {
ui.Stop()
select {
case err := <-done:
if err != nil {
t.Errorf("run: %v", err)
}
case <-time.After(eventLoopTimeout):
t.Error("run did not return after the UI was stopped")
}
})
// run adds the welcome lines on a goroutine, so the status
// window is read until they are there. Each read waits for
// the event loop, so the reads run on a goroutine too, which
// this test stops waiting for after eventLoopTimeout.
shown := make(chan struct{})
go func() {
for {
lines, _ := bufferState(ui, "(status)")
if lines == welcomeLines {
close(shown)
return
}
time.Sleep(checkInterval)
}
}()
select {
case <-shown:
case <-time.After(eventLoopTimeout):
t.Fatalf("the status window did not get the welcome lines within %v",
eventLoopTimeout)
}
}
// TestOwnEventFromOtherClient checks that a message, notice,
// join or part that the user's session makes from another
// client shows as one line in the buffer it belongs to.