From 52d4602e332d027c62011e38b16883eb4380d1f7 Mon Sep 17 00:00:00 2001 From: Syahdan Date: Wed, 15 Oct 2025 10:55:06 +0700 Subject: [PATCH] feat(chat): colorize usernames by ID; refine IDs/usernames and /name parsing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Server: - Generate 6-char IDs from lowercase hex alphabet "abcdef0123456789" - Default username now uses the ID as-is (no extra ToLower) - Use strings.CutPrefix for robust “/name ” command parsing Client: - Add colorizeLine to apply ANSI colors based on 6-hex-digit ID - Colorize chat lines: " (): " - Colorize [join]/[leave] notices - Colorize [rename] events - Colorize Welcome banner - Apply colorizeLine to incoming netMsg before appending Notes: - ID regex patterns accept 6 hex digits (case-insensitive), rendered in lowercase - Keeps non-matching lines unchanged --- main.go | 42 +++++++++++++++++++++++++++++++++++++++++- server.go | 7 +++---- 2 files changed, 44 insertions(+), 5 deletions(-) diff --git a/main.go b/main.go index 1bc92cb..f77c025 100644 --- a/main.go +++ b/main.go @@ -7,6 +7,7 @@ import ( "fmt" "io" "net" + "regexp" "strings" "time" @@ -16,6 +17,45 @@ import ( "github.com/charmbracelet/lipgloss" ) +// colorizeLine applies ANSI styling to usernames based on 6-hex-digit id from the server. +func colorizeLine(s string) string { + reChat := regexp.MustCompile(`^(.+?) \(([0-9a-fA-F]{6})\):[ \t]*(.*)$`) + if m := reChat.FindStringSubmatch(s); m != nil { + id := strings.ToLower(m[2]) + nameStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("#" + id)).Bold(true) + name := nameStyle.Render(m[1]) + rest := strings.TrimSpace(m[3]) + return fmt.Sprintf("%s (%s): %s", name, id, rest) + } + + reJoinLeave := regexp.MustCompile(`^\[(join|leave)\] (.+?) \(([0-9a-fA-F]{6})\)$`) + if m := reJoinLeave.FindStringSubmatch(s); m != nil { + id := strings.ToLower(m[3]) + nameStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("#" + id)).Bold(true) + uname := nameStyle.Render(m[2]) + return fmt.Sprintf("[%s] %s (%s)", m[1], uname, id) + } + + reRename := regexp.MustCompile(`^\[rename\] (.+?) \(([0-9a-fA-F]{6})\) -> (.+)$`) + if m := reRename.FindStringSubmatch(s); m != nil { + id := strings.ToLower(m[2]) + nameStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("#" + id)).Bold(true) + oldN := nameStyle.Render(m[1]) + newN := nameStyle.Render(m[3]) + return fmt.Sprintf("[rename] %s (%s) -> %s", oldN, id, newN) + } + + reWelcome := regexp.MustCompile(`^Welcome (.+?) \(([0-9a-fA-F]{6})\)$`) + if m := reWelcome.FindStringSubmatch(s); m != nil { + id := strings.ToLower(m[2]) + nameStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("#" + id)).Bold(true) + uname := nameStyle.Render(m[1]) + return fmt.Sprintf("Welcome %s (%s)", uname, id) + } + + return s +} + type netMsg string type connectedMsg struct{ conn net.Conn } type disconnectedMsg struct{} @@ -122,7 +162,7 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, nil case netMsg: - m.messages = append(m.messages, string(msg)) + m.messages = append(m.messages, colorizeLine(string(msg))) m.refreshViewport() if m.conn != nil && !strings.HasPrefix(string(msg), "[error] read") { diff --git a/server.go b/server.go index adf4f75..e1132fd 100644 --- a/server.go +++ b/server.go @@ -102,14 +102,14 @@ func handleConn(h *Hub, c net.Conn) { h.joinCh <- c // Generate per-connection ID - id, err := gonanoid.Generate("ABCDEF0123456789", 6) + id, err := gonanoid.Generate("abcdef0123456789", 6) if err != nil || id == "" { // Fallback to remote addr if generation fails id = c.RemoteAddr().String() } // Default username is server-controlled; not necessarily unique - defaultName := "user_" + strings.ToLower(id) + defaultName := "user_" + id username := defaultName // Greet client and instruct on setting username @@ -132,8 +132,7 @@ func handleConn(h *Hub, c net.Conn) { if line == "/quit" { break // unified leave handling below } - if strings.HasPrefix(line, "/name ") { - desired := strings.TrimSpace(strings.TrimPrefix(line, "/name ")) + if desired, ok := strings.CutPrefix(line, "/name "); ok { newName := sanitizeUsername(desired) if newName == "" { fmt.Fprintln(c, "[error] invalid username")