refactor(net): modelled connection lifecycle with typed msgs; switch to line-by-line reader

- Introduce typed msgs: connectedMsg, disconnectedMsg, errorMsg, and netMsg
- connectCmd now returns connectedMsg on success instead of “[connected]”
- Add readLineCmd to read a single line via bufio.Reader and emit netMsg
  - Handles io.EOF as disconnectedMsg; reports other errors as “[error] read: …”
- Update loop:
  - On connectedMsg: store conn, append “[connected]”, schedule first readLineCmd
  - On netMsg: append message, schedule next readLineCmd unless it’s a read error
  - On disconnectedMsg: append “[disconnected]”, close and clear conn
- UI tweaks: textarea placeholder uses "..." and height reduced from 3 to 2
- Window sizing: set viewport width; removed old netMsg branch now superseded by typed msgs

This removes the scanner goroutine pattern in favor of Cmd-driven incremental reads.
This commit is contained in:
Syahdan 2025-10-15 01:03:35 +07:00
parent cae82f5d55
commit 676b45cd23

79
main.go
View File

@ -2,7 +2,9 @@ package main
import ( import (
"bufio" "bufio"
"errors"
"fmt" "fmt"
"io"
"net" "net"
"strings" "strings"
"time" "time"
@ -14,6 +16,9 @@ import (
) )
type netMsg string type netMsg string
type connectedMsg struct{ conn net.Conn }
type disconnectedMsg struct{}
type errorMsg struct{ err error }
type model struct { type model struct {
vp viewport.Model vp viewport.Model
@ -31,11 +36,11 @@ func initialModel(serverAddr string) model {
ta := textarea.New() ta := textarea.New()
ta.FocusedStyle.CursorLine = lipgloss.NewStyle() ta.FocusedStyle.CursorLine = lipgloss.NewStyle()
ta.Placeholder = "Type message and press Enter" ta.Placeholder = "Type message and press Enter..."
ta.Focus() ta.Focus()
ta.Prompt = "┃ " ta.Prompt = "┃ "
ta.CharLimit = 0 ta.CharLimit = 0
ta.SetHeight(3) ta.SetHeight(2)
ta.ShowLineNumbers = false ta.ShowLineNumbers = false
return model{ return model{
@ -57,19 +62,26 @@ func connectCmd(addr string) tea.Cmd {
return netMsg(fmt.Sprintf("[error] connect: %v", err)) return netMsg(fmt.Sprintf("[error] connect: %v", err))
} }
go func(conn net.Conn, ch chan<- tea.Msg) { return connectedMsg{conn: conn}
scanner := bufio.NewScanner(conn) }
scanner.Buffer(make([]byte, 0, 1024), 64*1024) }
for scanner.Scan() {
ch <- netMsg(scanner.Text())
}
if err := scanner.Err(); err != nil {
ch <- netMsg(fmt.Sprintf("[error] read: %v", err))
}
ch <- netMsg("[disconnected]")
}(conn, make(chan tea.Msg, 1))
return netMsg("[connected]") func readLineCmd(conn net.Conn) tea.Cmd {
return func() tea.Msg {
if conn == nil {
return disconnectedMsg{}
}
reader := bufio.NewReader(conn)
line, err := reader.ReadString('\n')
if err != nil {
if errors.Is(err, io.EOF) {
return disconnectedMsg{}
}
return netMsg(fmt.Sprintf("[error] read: %v", err))
}
return netMsg(strings.TrimRight(line, "\r\n"))
} }
} }
@ -91,6 +103,31 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var cmds []tea.Cmd var cmds []tea.Cmd
switch msg := msg.(type) { switch msg := msg.(type) {
case connectedMsg:
m.conn = msg.conn
m.messages = append(m.messages, "[connected]")
m.refreshViewport()
return m, readLineCmd(m.conn)
case disconnectedMsg:
m.messages = append(m.messages, "[disconnected]")
if m.conn != nil {
_ = m.conn.Close()
m.conn = nil
}
m.refreshViewport()
return m, nil
case netMsg:
m.messages = append(m.messages, string(msg))
m.refreshViewport()
if m.conn != nil && !strings.HasPrefix(string(msg), "[error] read") {
cmds = append(cmds, readLineCmd(m.conn))
}
case tea.KeyMsg: case tea.KeyMsg:
switch msg.Type { switch msg.Type {
case tea.KeyCtrlC, tea.KeyEsc: case tea.KeyCtrlC, tea.KeyEsc:
@ -107,25 +144,13 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
} }
return m, tea.Batch(cmds...) return m, tea.Batch(cmds...)
} }
case tea.WindowSizeMsg: case tea.WindowSizeMsg:
// Allocate space: viewport above, input below // Allocate space: viewport above, input below
m.vp.Width = msg.Width m.vp.Width = msg.Width
m.vp.Height = msg.Height - 4 m.vp.Height = msg.Height - 4
m.input.SetWidth(msg.Width - 2) m.input.SetWidth(msg.Width - 2)
m.refreshViewport() m.refreshViewport()
case netMsg:
s := string(msg)
if s == "[connected]" {
m.messages = append(m.messages, s)
m.refreshViewport()
return m, nil
}
m.messages = append(m.messages, s)
m.refreshViewport()
return m, nil
} }
// Let textarea handle remaining keys // Let textarea handle remaining keys