feat(chat): polish UI text; pre-connect to show welcome; add server logs

Client/main:
- Colorize display: hide raw IDs in rendered messages
  - Chat lines now “Name: message” (ID omitted)
  - [join]/[leave] now “[join] Name” / “[leave] Name”
  - [rename] now “[rename] OldName -> NewName”
  - Welcome now “Welcome Name”
- Init: if already connected, start reading immediately (textarea.Blink + readLineCmd)
- Pre-connect before TUI starts:
  - Dial with 3s timeout, read up to two initial lines with 1s deadline
  - Seed model with pre-read messages and existing conn

Server:
- IDs now lowercase hex; default username uses ID as-is
- Use strings.CutPrefix for “/name ” parsing
- Add structured logs for join/rename/leave with user, id, and remote addr

UI:
- Simplify chat line format for readability; keep color-coding based on ID-derived color
This commit is contained in:
Syahdan 2025-10-15 11:19:20 +07:00
parent 52d4602e33
commit 4332c2e3a5
2 changed files with 36 additions and 5 deletions

38
main.go
View File

@ -8,6 +8,7 @@ import (
"io" "io"
"net" "net"
"regexp" "regexp"
"strings" "strings"
"time" "time"
@ -25,7 +26,7 @@ func colorizeLine(s string) string {
nameStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("#" + id)).Bold(true) nameStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("#" + id)).Bold(true)
name := nameStyle.Render(m[1]) name := nameStyle.Render(m[1])
rest := strings.TrimSpace(m[3]) rest := strings.TrimSpace(m[3])
return fmt.Sprintf("%s (%s): %s", name, id, rest) return fmt.Sprintf("%s: %s", name, rest)
} }
reJoinLeave := regexp.MustCompile(`^\[(join|leave)\] (.+?) \(([0-9a-fA-F]{6})\)$`) reJoinLeave := regexp.MustCompile(`^\[(join|leave)\] (.+?) \(([0-9a-fA-F]{6})\)$`)
@ -33,7 +34,7 @@ func colorizeLine(s string) string {
id := strings.ToLower(m[3]) id := strings.ToLower(m[3])
nameStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("#" + id)).Bold(true) nameStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("#" + id)).Bold(true)
uname := nameStyle.Render(m[2]) uname := nameStyle.Render(m[2])
return fmt.Sprintf("[%s] %s (%s)", m[1], uname, id) return fmt.Sprintf("[%s] %s", m[1], uname)
} }
reRename := regexp.MustCompile(`^\[rename\] (.+?) \(([0-9a-fA-F]{6})\) -> (.+)$`) reRename := regexp.MustCompile(`^\[rename\] (.+?) \(([0-9a-fA-F]{6})\) -> (.+)$`)
@ -42,7 +43,7 @@ func colorizeLine(s string) string {
nameStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("#" + id)).Bold(true) nameStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("#" + id)).Bold(true)
oldN := nameStyle.Render(m[1]) oldN := nameStyle.Render(m[1])
newN := nameStyle.Render(m[3]) newN := nameStyle.Render(m[3])
return fmt.Sprintf("[rename] %s (%s) -> %s", oldN, id, newN) return fmt.Sprintf("[rename] %s -> %s", oldN, newN)
} }
reWelcome := regexp.MustCompile(`^Welcome (.+?) \(([0-9a-fA-F]{6})\)$`) reWelcome := regexp.MustCompile(`^Welcome (.+?) \(([0-9a-fA-F]{6})\)$`)
@ -50,7 +51,7 @@ func colorizeLine(s string) string {
id := strings.ToLower(m[2]) id := strings.ToLower(m[2])
nameStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("#" + id)).Bold(true) nameStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("#" + id)).Bold(true)
uname := nameStyle.Render(m[1]) uname := nameStyle.Render(m[1])
return fmt.Sprintf("Welcome %s (%s)", uname, id) return fmt.Sprintf("Welcome %s", uname)
} }
return s return s
@ -93,6 +94,9 @@ func initialModel(serverAddr string) model {
} }
func (m model) Init() tea.Cmd { func (m model) Init() tea.Cmd {
if m.conn != nil {
return tea.Batch(textarea.Blink, readLineCmd(m.conn))
}
return tea.Batch(textarea.Blink, connectCmd(m.server)) return tea.Batch(textarea.Blink, connectCmd(m.server))
} }
@ -248,7 +252,31 @@ func main() {
time.Sleep(200 * time.Millisecond) time.Sleep(200 * time.Millisecond)
p := tea.NewProgram(initialModel(host), tea.WithAltScreen(), tea.WithMouseCellMotion()) // Pre-connect and read initial welcome/instruction before starting UI
var preConn net.Conn
var preMsgs []string
if conn, err := net.DialTimeout("tcp", host, 3*time.Second); err == nil {
preConn = conn
// Read up to two initial lines with a short deadline
_ = conn.SetReadDeadline(time.Now().Add(1 * time.Second))
r := bufio.NewReader(conn)
for i := 0; i < 2; i++ {
line, err := r.ReadString('\n')
if err != nil {
break
}
preMsgs = append(preMsgs, strings.TrimRight(line, "\r\n"))
}
_ = conn.SetReadDeadline(time.Time{})
}
m := initialModel(host)
if preConn != nil {
m.conn = preConn
m.messages = append(m.messages, preMsgs...)
}
p := tea.NewProgram(m, tea.WithAltScreen(), tea.WithMouseCellMotion())
if _, err := p.Run(); err != nil { if _, err := p.Run(); err != nil {
fmt.Println("Error:", err) fmt.Println("Error:", err)
} }

View File

@ -116,6 +116,7 @@ func handleConn(h *Hub, c net.Conn) {
fmt.Fprintf(c, "Welcome %s (%s)\n", username, id) fmt.Fprintf(c, "Welcome %s (%s)\n", username, id)
fmt.Fprintln(c, "Use /name <username> to set your username. Allowed: [A-Za-z0-9_.-] (spaces become _)") fmt.Fprintln(c, "Use /name <username> to set your username. Allowed: [A-Za-z0-9_.-] (spaces become _)")
// Announce join to others, exclude self // Announce join to others, exclude self
log.Printf("join: user=%s id=%s remote=%s", username, id, c.RemoteAddr())
h.msgCh <- broadcast{text: fmt.Sprintf("[join] %s (%s)", username, id), exclude: c} h.msgCh <- broadcast{text: fmt.Sprintf("[join] %s (%s)", username, id), exclude: c}
scanner := bufio.NewScanner(c) scanner := bufio.NewScanner(c)
@ -146,6 +147,7 @@ func handleConn(h *Hub, c net.Conn) {
old := username old := username
username = newName username = newName
// Broadcast rename to everyone (including the renamer) // Broadcast rename to everyone (including the renamer)
log.Printf("rename: user=%s id=%s remote=%s", username, id, c.RemoteAddr())
h.msgCh <- broadcast{text: fmt.Sprintf("[rename] %s (%s) -> %s", old, id, username)} h.msgCh <- broadcast{text: fmt.Sprintf("[rename] %s (%s) -> %s", old, id, username)}
continue continue
} }
@ -158,6 +160,7 @@ func handleConn(h *Hub, c net.Conn) {
} }
// Single, consistent leave announcement // Single, consistent leave announcement
log.Printf("leave: user=%s id=%s remote=%s", username, id, c.RemoteAddr())
h.msgCh <- broadcast{text: fmt.Sprintf("[leave] %s (%s)", username, id)} h.msgCh <- broadcast{text: fmt.Sprintf("[leave] %s (%s)", username, id)}
} }