feat(chat): colorize usernames by ID; refine IDs/usernames and /name parsing

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: "<name> (<id>): <msg>"
  - 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
This commit is contained in:
Syahdan 2025-10-15 10:55:06 +07:00
parent 2c884d030a
commit 52d4602e33
2 changed files with 44 additions and 5 deletions

42
main.go
View File

@ -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") {

View File

@ -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")