feat(chat): CLI flags, centered header; improve server with selective broadcast and quit handling

Client/main:
- Add flags: -host (default "localhost:9000") and -server (run only server)
- When -server is set, startTCPServer(host) and exit
- Use provided host for client connection
- Center header with width-aware lipgloss styling; adjust padding

Server:
- Introduce broadcast struct {text string, exclude net.Conn}
- Hub.msgCh now chan broadcast; send messages with optional exclusion
- On join: send “[join] …” to others (exclude the joiner)
- Broadcast chat lines without exclusion; newline-delimited as before
- Handle “/quit” by breaking loop (graceful disconnect)
- On leave: broadcast “[leave] …” to all

Refactor notes:
- Remove unconditional embedded server from client startup
- Scanner loop unchanged aside from quit handling and broadcast usage
This commit is contained in:
Syahdan 2025-10-15 09:51:35 +07:00
parent 676b45cd23
commit b80c5190a3
2 changed files with 31 additions and 13 deletions

21
main.go
View File

@ -3,6 +3,7 @@ package main
import ( import (
"bufio" "bufio"
"errors" "errors"
"flag"
"fmt" "fmt"
"io" "io"
"net" "net"
@ -179,7 +180,7 @@ func (m *model) refreshViewport() {
} }
func (m model) View() string { func (m model) View() string {
header := lipgloss.NewStyle().Foreground(lipgloss.Color("241")).Bold(true).Render(m.server) header := lipgloss.NewStyle().Width(m.vp.Width).Padding(0, 1).Align(lipgloss.Center).Foreground(lipgloss.Color("241")).Bold(true).Render(m.server)
return lipgloss.JoinVertical( return lipgloss.JoinVertical(
lipgloss.Left, lipgloss.Left,
header, header,
@ -189,15 +190,25 @@ func (m model) View() string {
} }
func main() { func main() {
go func() { var (
if err := startTCPServer("localhost:9000"); err != nil { host string
serverOnly bool
)
flag.StringVar(&host, "host", "localhost:9000", "host:port to connect to or bind the server on")
flag.BoolVar(&serverOnly, "server", false, "run only the server")
flag.Parse()
if serverOnly {
if err := startTCPServer(host); err != nil {
fmt.Println("Server error:", err) fmt.Println("Server error:", err)
} }
}() return
}
time.Sleep(200 * time.Millisecond) time.Sleep(200 * time.Millisecond)
p := tea.NewProgram(initialModel("127.0.0.1:9000"), tea.WithAltScreen(), tea.WithMouseCellMotion()) p := tea.NewProgram(initialModel(host), 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

@ -9,12 +9,17 @@ import (
"sync" "sync"
) )
type broadcast struct {
text string
exclude net.Conn
}
type Hub struct { type Hub struct {
mu sync.Mutex mu sync.Mutex
conns map[net.Conn]struct{} conns map[net.Conn]struct{}
joinCh chan net.Conn joinCh chan net.Conn
leaveCh chan net.Conn leaveCh chan net.Conn
msgCh chan string msgCh chan broadcast
} }
func NewHub() *Hub { func NewHub() *Hub {
@ -22,7 +27,7 @@ func NewHub() *Hub {
conns: make(map[net.Conn]struct{}), conns: make(map[net.Conn]struct{}),
joinCh: make(chan net.Conn), joinCh: make(chan net.Conn),
leaveCh: make(chan net.Conn), leaveCh: make(chan net.Conn),
msgCh: make(chan string, 128), msgCh: make(chan broadcast, 128),
} }
} }
@ -43,8 +48,10 @@ func (h *Hub) Run() {
case msg := <-h.msgCh: case msg := <-h.msgCh:
h.mu.Lock() h.mu.Lock()
for c := range h.conns { for c := range h.conns {
// newline-delimited messages if msg.exclude != nil && c == msg.exclude {
fmt.Fprintln(c, msg) continue
}
fmt.Fprintln(c, msg.text)
} }
h.mu.Unlock() h.mu.Unlock()
} }
@ -57,7 +64,7 @@ func handleConn(h *Hub, c net.Conn) {
name := c.RemoteAddr().String() name := c.RemoteAddr().String()
fmt.Fprintf(c, "Welcome %s\n", name) fmt.Fprintf(c, "Welcome %s\n", name)
h.msgCh <- fmt.Sprintf("[join] %s", name) h.msgCh <- broadcast{text: fmt.Sprintf("[join] %s", name), exclude: c}
scanner := bufio.NewScanner(c) scanner := bufio.NewScanner(c)
scanner.Buffer(make([]byte, 0, 1024), 64*1024) scanner.Buffer(make([]byte, 0, 1024), 64*1024)
@ -67,14 +74,14 @@ func handleConn(h *Hub, c net.Conn) {
continue continue
} }
if line == "/quit" { if line == "/quit" {
return break
} }
h.msgCh <- fmt.Sprintf("%s: %s", name, line) h.msgCh <- broadcast{text: fmt.Sprintf("%s: %s", name, line)}
} }
if err := scanner.Err(); err != nil { if err := scanner.Err(); err != nil {
log.Printf("read err from %s: %v", name, err) log.Printf("read err from %s: %v", name, err)
} }
h.msgCh <- fmt.Sprintf("[leave] %s", name) h.msgCh <- broadcast{text: fmt.Sprintf("[leave] %s", name)}
} }
func startTCPServer(addr string) error { func startTCPServer(addr string) error {