2025-10-15 00:19:24 +07:00
|
|
|
|
package main
|
|
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
|
"bufio"
|
feat(app): pivot from chat UI to order console with menu/order protocol
Client (main.go):
- Replace chat viewport/textarea with a simple order console UI using Bubble Tea + Huh forms
- Add types and messages for connection, menu loading, order submission, and status
- Implement connectCmd with short greeting drain; status updates on connect
- Implement fetchMenuCmd: send "MENU", parse single-line JSON into []menuItem
- Implement submitOrderCmd: send "ORDER <json>", read single-line ack
- Build interactive form (name, menu select, quantity, confirm) with validation
- Add model state for host, status/loading/error, lastOrder, and form fields
- Keyboard controls: n (new order), r (reconnect), q/esc/ctrl+c (quit)
- Simplify program start (remove pre-connect path); alt-screen only
- Remove chat colorization/regex, viewport, and textarea logic
Server (server.go):
- Define menuItem and order structs; add a defaultMenu
- Extend protocol:
- "MENU" returns single-line JSON array of menu items
- "ORDER <json>" validates name/item/quantity (with lenient quantity parsing)
- Acknowledge with "OK" or send "[error] ..." lines
- Optionally broadcast "[order] ..." to chat for visibility
- Keep existing chat behavior; integrate new commands alongside it
Notes:
- Server logs ORDER parsing and continues to support chat commands
- Error handling surfaces server-side errors to the client status/messages
2025-10-15 22:02:41 +07:00
|
|
|
|
"encoding/json"
|
2025-10-15 00:19:24 +07:00
|
|
|
|
"fmt"
|
|
|
|
|
|
"log"
|
|
|
|
|
|
"net"
|
feat(app): pivot from chat UI to order console with menu/order protocol
Client (main.go):
- Replace chat viewport/textarea with a simple order console UI using Bubble Tea + Huh forms
- Add types and messages for connection, menu loading, order submission, and status
- Implement connectCmd with short greeting drain; status updates on connect
- Implement fetchMenuCmd: send "MENU", parse single-line JSON into []menuItem
- Implement submitOrderCmd: send "ORDER <json>", read single-line ack
- Build interactive form (name, menu select, quantity, confirm) with validation
- Add model state for host, status/loading/error, lastOrder, and form fields
- Keyboard controls: n (new order), r (reconnect), q/esc/ctrl+c (quit)
- Simplify program start (remove pre-connect path); alt-screen only
- Remove chat colorization/regex, viewport, and textarea logic
Server (server.go):
- Define menuItem and order structs; add a defaultMenu
- Extend protocol:
- "MENU" returns single-line JSON array of menu items
- "ORDER <json>" validates name/item/quantity (with lenient quantity parsing)
- Acknowledge with "OK" or send "[error] ..." lines
- Optionally broadcast "[order] ..." to chat for visibility
- Keep existing chat behavior; integrate new commands alongside it
Notes:
- Server logs ORDER parsing and continues to support chat commands
- Error handling surfaces server-side errors to the client status/messages
2025-10-15 22:02:41 +07:00
|
|
|
|
"strconv"
|
2025-10-15 00:19:24 +07:00
|
|
|
|
"strings"
|
|
|
|
|
|
"sync"
|
2025-10-15 10:40:55 +07:00
|
|
|
|
|
|
|
|
|
|
gonanoid "github.com/matoous/go-nanoid/v2"
|
2025-10-15 00:19:24 +07:00
|
|
|
|
)
|
|
|
|
|
|
|
feat(app): pivot from chat UI to order console with menu/order protocol
Client (main.go):
- Replace chat viewport/textarea with a simple order console UI using Bubble Tea + Huh forms
- Add types and messages for connection, menu loading, order submission, and status
- Implement connectCmd with short greeting drain; status updates on connect
- Implement fetchMenuCmd: send "MENU", parse single-line JSON into []menuItem
- Implement submitOrderCmd: send "ORDER <json>", read single-line ack
- Build interactive form (name, menu select, quantity, confirm) with validation
- Add model state for host, status/loading/error, lastOrder, and form fields
- Keyboard controls: n (new order), r (reconnect), q/esc/ctrl+c (quit)
- Simplify program start (remove pre-connect path); alt-screen only
- Remove chat colorization/regex, viewport, and textarea logic
Server (server.go):
- Define menuItem and order structs; add a defaultMenu
- Extend protocol:
- "MENU" returns single-line JSON array of menu items
- "ORDER <json>" validates name/item/quantity (with lenient quantity parsing)
- Acknowledge with "OK" or send "[error] ..." lines
- Optionally broadcast "[order] ..." to chat for visibility
- Keep existing chat behavior; integrate new commands alongside it
Notes:
- Server logs ORDER parsing and continues to support chat commands
- Error handling surfaces server-side errors to the client status/messages
2025-10-15 22:02:41 +07:00
|
|
|
|
var defaultMenu = []menuItem{
|
2025-10-15 22:28:03 +07:00
|
|
|
|
{ID: "latte", Name: "Caffè Latte", Price: 4.50},
|
|
|
|
|
|
{ID: "cap", Name: "Cappuccino", Price: 4.00},
|
|
|
|
|
|
{ID: "esp", Name: "Espresso", Price: 3.00},
|
feat(app): pivot from chat UI to order console with menu/order protocol
Client (main.go):
- Replace chat viewport/textarea with a simple order console UI using Bubble Tea + Huh forms
- Add types and messages for connection, menu loading, order submission, and status
- Implement connectCmd with short greeting drain; status updates on connect
- Implement fetchMenuCmd: send "MENU", parse single-line JSON into []menuItem
- Implement submitOrderCmd: send "ORDER <json>", read single-line ack
- Build interactive form (name, menu select, quantity, confirm) with validation
- Add model state for host, status/loading/error, lastOrder, and form fields
- Keyboard controls: n (new order), r (reconnect), q/esc/ctrl+c (quit)
- Simplify program start (remove pre-connect path); alt-screen only
- Remove chat colorization/regex, viewport, and textarea logic
Server (server.go):
- Define menuItem and order structs; add a defaultMenu
- Extend protocol:
- "MENU" returns single-line JSON array of menu items
- "ORDER <json>" validates name/item/quantity (with lenient quantity parsing)
- Acknowledge with "OK" or send "[error] ..." lines
- Optionally broadcast "[order] ..." to chat for visibility
- Keep existing chat behavior; integrate new commands alongside it
Notes:
- Server logs ORDER parsing and continues to support chat commands
- Error handling surfaces server-side errors to the client status/messages
2025-10-15 22:02:41 +07:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-16 18:10:17 +07:00
|
|
|
|
var serverMenu []menuItem
|
|
|
|
|
|
|
feat(app): pivot from chat UI to order console with menu/order protocol
Client (main.go):
- Replace chat viewport/textarea with a simple order console UI using Bubble Tea + Huh forms
- Add types and messages for connection, menu loading, order submission, and status
- Implement connectCmd with short greeting drain; status updates on connect
- Implement fetchMenuCmd: send "MENU", parse single-line JSON into []menuItem
- Implement submitOrderCmd: send "ORDER <json>", read single-line ack
- Build interactive form (name, menu select, quantity, confirm) with validation
- Add model state for host, status/loading/error, lastOrder, and form fields
- Keyboard controls: n (new order), r (reconnect), q/esc/ctrl+c (quit)
- Simplify program start (remove pre-connect path); alt-screen only
- Remove chat colorization/regex, viewport, and textarea logic
Server (server.go):
- Define menuItem and order structs; add a defaultMenu
- Extend protocol:
- "MENU" returns single-line JSON array of menu items
- "ORDER <json>" validates name/item/quantity (with lenient quantity parsing)
- Acknowledge with "OK" or send "[error] ..." lines
- Optionally broadcast "[order] ..." to chat for visibility
- Keep existing chat behavior; integrate new commands alongside it
Notes:
- Server logs ORDER parsing and continues to support chat commands
- Error handling surfaces server-side errors to the client status/messages
2025-10-15 22:02:41 +07:00
|
|
|
|
// order is the structure the server expects for ORDER.
|
|
|
|
|
|
type order struct {
|
|
|
|
|
|
Name string `json:"name"`
|
|
|
|
|
|
ItemID string `json:"itemId"`
|
|
|
|
|
|
Quantity int `json:"quantity"`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-15 10:40:55 +07:00
|
|
|
|
// broadcast represents a line to send to all connections with the ability
|
|
|
|
|
|
// to exclude a single connection (e.g., exclude self on join).
|
2025-10-15 09:51:35 +07:00
|
|
|
|
type broadcast struct {
|
|
|
|
|
|
text string
|
|
|
|
|
|
exclude net.Conn
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-15 10:40:55 +07:00
|
|
|
|
// Hub manages the set of connected clients and fan-out of messages.
|
2025-10-15 00:19:24 +07:00
|
|
|
|
type Hub struct {
|
|
|
|
|
|
mu sync.Mutex
|
|
|
|
|
|
conns map[net.Conn]struct{}
|
|
|
|
|
|
joinCh chan net.Conn
|
|
|
|
|
|
leaveCh chan net.Conn
|
2025-10-15 09:51:35 +07:00
|
|
|
|
msgCh chan broadcast
|
2025-10-15 00:19:24 +07:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func NewHub() *Hub {
|
|
|
|
|
|
return &Hub{
|
|
|
|
|
|
conns: make(map[net.Conn]struct{}),
|
|
|
|
|
|
joinCh: make(chan net.Conn),
|
|
|
|
|
|
leaveCh: make(chan net.Conn),
|
2025-10-15 09:51:35 +07:00
|
|
|
|
msgCh: make(chan broadcast, 128),
|
2025-10-15 00:19:24 +07:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (h *Hub) Run() {
|
|
|
|
|
|
for {
|
|
|
|
|
|
select {
|
|
|
|
|
|
case c := <-h.joinCh:
|
|
|
|
|
|
h.mu.Lock()
|
|
|
|
|
|
h.conns[c] = struct{}{}
|
|
|
|
|
|
h.mu.Unlock()
|
|
|
|
|
|
case c := <-h.leaveCh:
|
|
|
|
|
|
h.mu.Lock()
|
|
|
|
|
|
if _, ok := h.conns[c]; ok {
|
|
|
|
|
|
delete(h.conns, c)
|
|
|
|
|
|
_ = c.Close()
|
|
|
|
|
|
}
|
|
|
|
|
|
h.mu.Unlock()
|
|
|
|
|
|
case msg := <-h.msgCh:
|
|
|
|
|
|
h.mu.Lock()
|
|
|
|
|
|
for c := range h.conns {
|
2025-10-15 09:51:35 +07:00
|
|
|
|
if msg.exclude != nil && c == msg.exclude {
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
2025-10-15 10:40:55 +07:00
|
|
|
|
// Newline-delimited messages
|
2025-10-15 09:51:35 +07:00
|
|
|
|
fmt.Fprintln(c, msg.text)
|
2025-10-15 00:19:24 +07:00
|
|
|
|
}
|
|
|
|
|
|
h.mu.Unlock()
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-15 10:40:55 +07:00
|
|
|
|
// sanitizeUsername enforces server rules on allowed usernames.
|
|
|
|
|
|
// - letters, digits, '_', '-', '.' allowed
|
|
|
|
|
|
// - spaces converted to '_'
|
|
|
|
|
|
// - trimmed of leading/trailing '.', '_' or '-'
|
|
|
|
|
|
// - empty after sanitization is invalid
|
|
|
|
|
|
// - max length limited
|
|
|
|
|
|
func sanitizeUsername(s string) string {
|
|
|
|
|
|
s = strings.TrimSpace(s)
|
|
|
|
|
|
if s == "" {
|
|
|
|
|
|
return ""
|
|
|
|
|
|
}
|
|
|
|
|
|
const maxLen = 12
|
|
|
|
|
|
var out []rune
|
|
|
|
|
|
for _, r := range s {
|
|
|
|
|
|
switch {
|
|
|
|
|
|
case r >= 'a' && r <= 'z',
|
|
|
|
|
|
r >= 'A' && r <= 'Z',
|
|
|
|
|
|
r >= '0' && r <= '9',
|
|
|
|
|
|
r == '_', r == '-', r == '.':
|
|
|
|
|
|
out = append(out, r)
|
|
|
|
|
|
case r == ' ':
|
|
|
|
|
|
out = append(out, '_')
|
|
|
|
|
|
default:
|
|
|
|
|
|
// skip everything else
|
|
|
|
|
|
}
|
|
|
|
|
|
if len(out) >= maxLen {
|
|
|
|
|
|
break
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
res := strings.Trim(string(out), "._-")
|
|
|
|
|
|
return res
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-15 00:19:24 +07:00
|
|
|
|
func handleConn(h *Hub, c net.Conn) {
|
|
|
|
|
|
defer func() { h.leaveCh <- c }()
|
|
|
|
|
|
h.joinCh <- c
|
|
|
|
|
|
|
2025-10-15 10:40:55 +07:00
|
|
|
|
// Generate per-connection ID
|
2025-10-15 10:55:06 +07:00
|
|
|
|
id, err := gonanoid.Generate("abcdef0123456789", 6)
|
2025-10-15 10:40:55 +07:00
|
|
|
|
if err != nil || id == "" {
|
|
|
|
|
|
// Fallback to remote addr if generation fails
|
|
|
|
|
|
id = c.RemoteAddr().String()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Default username is server-controlled; not necessarily unique
|
2025-10-15 10:55:06 +07:00
|
|
|
|
defaultName := "user_" + id
|
2025-10-15 10:40:55 +07:00
|
|
|
|
username := defaultName
|
|
|
|
|
|
|
|
|
|
|
|
// Greet client and instruct on setting username
|
|
|
|
|
|
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 _)")
|
|
|
|
|
|
// Announce join to others, exclude self
|
2025-10-15 11:19:20 +07:00
|
|
|
|
log.Printf("join: user=%s id=%s remote=%s", username, id, c.RemoteAddr())
|
2025-10-15 10:40:55 +07:00
|
|
|
|
h.msgCh <- broadcast{text: fmt.Sprintf("[join] %s (%s)", username, id), exclude: c}
|
2025-10-15 00:19:24 +07:00
|
|
|
|
|
|
|
|
|
|
scanner := bufio.NewScanner(c)
|
2025-10-15 10:40:55 +07:00
|
|
|
|
// Allow reasonably large lines
|
2025-10-15 00:19:24 +07:00
|
|
|
|
scanner.Buffer(make([]byte, 0, 1024), 64*1024)
|
2025-10-15 10:40:55 +07:00
|
|
|
|
|
2025-10-15 00:19:24 +07:00
|
|
|
|
for scanner.Scan() {
|
|
|
|
|
|
line := strings.TrimSpace(scanner.Text())
|
|
|
|
|
|
if line == "" {
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
2025-10-15 10:40:55 +07:00
|
|
|
|
|
feat(app): pivot from chat UI to order console with menu/order protocol
Client (main.go):
- Replace chat viewport/textarea with a simple order console UI using Bubble Tea + Huh forms
- Add types and messages for connection, menu loading, order submission, and status
- Implement connectCmd with short greeting drain; status updates on connect
- Implement fetchMenuCmd: send "MENU", parse single-line JSON into []menuItem
- Implement submitOrderCmd: send "ORDER <json>", read single-line ack
- Build interactive form (name, menu select, quantity, confirm) with validation
- Add model state for host, status/loading/error, lastOrder, and form fields
- Keyboard controls: n (new order), r (reconnect), q/esc/ctrl+c (quit)
- Simplify program start (remove pre-connect path); alt-screen only
- Remove chat colorization/regex, viewport, and textarea logic
Server (server.go):
- Define menuItem and order structs; add a defaultMenu
- Extend protocol:
- "MENU" returns single-line JSON array of menu items
- "ORDER <json>" validates name/item/quantity (with lenient quantity parsing)
- Acknowledge with "OK" or send "[error] ..." lines
- Optionally broadcast "[order] ..." to chat for visibility
- Keep existing chat behavior; integrate new commands alongside it
Notes:
- Server logs ORDER parsing and continues to support chat commands
- Error handling surfaces server-side errors to the client status/messages
2025-10-15 22:02:41 +07:00
|
|
|
|
// New protocol commands:
|
|
|
|
|
|
// MENU -> server returns single-line JSON array of menuItem
|
|
|
|
|
|
if strings.EqualFold(line, "MENU") {
|
2025-10-16 18:10:17 +07:00
|
|
|
|
b, err := json.Marshal(serverMenu)
|
feat(app): pivot from chat UI to order console with menu/order protocol
Client (main.go):
- Replace chat viewport/textarea with a simple order console UI using Bubble Tea + Huh forms
- Add types and messages for connection, menu loading, order submission, and status
- Implement connectCmd with short greeting drain; status updates on connect
- Implement fetchMenuCmd: send "MENU", parse single-line JSON into []menuItem
- Implement submitOrderCmd: send "ORDER <json>", read single-line ack
- Build interactive form (name, menu select, quantity, confirm) with validation
- Add model state for host, status/loading/error, lastOrder, and form fields
- Keyboard controls: n (new order), r (reconnect), q/esc/ctrl+c (quit)
- Simplify program start (remove pre-connect path); alt-screen only
- Remove chat colorization/regex, viewport, and textarea logic
Server (server.go):
- Define menuItem and order structs; add a defaultMenu
- Extend protocol:
- "MENU" returns single-line JSON array of menu items
- "ORDER <json>" validates name/item/quantity (with lenient quantity parsing)
- Acknowledge with "OK" or send "[error] ..." lines
- Optionally broadcast "[order] ..." to chat for visibility
- Keep existing chat behavior; integrate new commands alongside it
Notes:
- Server logs ORDER parsing and continues to support chat commands
- Error handling surfaces server-side errors to the client status/messages
2025-10-15 22:02:41 +07:00
|
|
|
|
if err != nil {
|
|
|
|
|
|
fmt.Fprintln(c, `[error] failed to encode menu`)
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
fmt.Fprintln(c, string(b))
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ORDER <json> -> server validates and replies with a single-line ack
|
|
|
|
|
|
if strings.HasPrefix(line, "ORDER") {
|
|
|
|
|
|
raw := strings.TrimSpace(line[len("ORDER"):])
|
|
|
|
|
|
var ord order
|
|
|
|
|
|
if err := json.Unmarshal([]byte(raw), &ord); err != nil {
|
|
|
|
|
|
fmt.Fprintln(c, "[error] invalid order json")
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
ord.Name = strings.TrimSpace(ord.Name)
|
|
|
|
|
|
log.Printf("ORDER parsed: name=%q itemId=%q qty=%d", ord.Name, ord.ItemID, ord.Quantity)
|
|
|
|
|
|
if ord.Name == "" {
|
|
|
|
|
|
fmt.Fprintln(c, "[error] missing name")
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
// Fallback handling: accept numeric strings or floats for quantity
|
|
|
|
|
|
if ord.Quantity <= 0 {
|
|
|
|
|
|
var generic map[string]any
|
|
|
|
|
|
if err := json.Unmarshal([]byte(raw), &generic); err == nil {
|
|
|
|
|
|
if v, ok := generic["quantity"]; ok {
|
|
|
|
|
|
switch t := v.(type) {
|
|
|
|
|
|
case string:
|
|
|
|
|
|
if n, err := strconv.Atoi(strings.TrimSpace(t)); err == nil {
|
|
|
|
|
|
ord.Quantity = n
|
|
|
|
|
|
}
|
|
|
|
|
|
case float64:
|
|
|
|
|
|
ord.Quantity = int(t)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if ord.Quantity <= 0 {
|
|
|
|
|
|
fmt.Fprintln(c, "[error] invalid quantity")
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
var chosen *menuItem
|
2025-10-16 18:10:17 +07:00
|
|
|
|
for i := range serverMenu {
|
|
|
|
|
|
if serverMenu[i].ID == ord.ItemID {
|
|
|
|
|
|
chosen = &serverMenu[i]
|
feat(app): pivot from chat UI to order console with menu/order protocol
Client (main.go):
- Replace chat viewport/textarea with a simple order console UI using Bubble Tea + Huh forms
- Add types and messages for connection, menu loading, order submission, and status
- Implement connectCmd with short greeting drain; status updates on connect
- Implement fetchMenuCmd: send "MENU", parse single-line JSON into []menuItem
- Implement submitOrderCmd: send "ORDER <json>", read single-line ack
- Build interactive form (name, menu select, quantity, confirm) with validation
- Add model state for host, status/loading/error, lastOrder, and form fields
- Keyboard controls: n (new order), r (reconnect), q/esc/ctrl+c (quit)
- Simplify program start (remove pre-connect path); alt-screen only
- Remove chat colorization/regex, viewport, and textarea logic
Server (server.go):
- Define menuItem and order structs; add a defaultMenu
- Extend protocol:
- "MENU" returns single-line JSON array of menu items
- "ORDER <json>" validates name/item/quantity (with lenient quantity parsing)
- Acknowledge with "OK" or send "[error] ..." lines
- Optionally broadcast "[order] ..." to chat for visibility
- Keep existing chat behavior; integrate new commands alongside it
Notes:
- Server logs ORDER parsing and continues to support chat commands
- Error handling surfaces server-side errors to the client status/messages
2025-10-15 22:02:41 +07:00
|
|
|
|
break
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if chosen == nil {
|
|
|
|
|
|
fmt.Fprintln(c, "[error] unknown item")
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-15 22:28:03 +07:00
|
|
|
|
total := float64(ord.Quantity) * chosen.Price
|
|
|
|
|
|
|
feat(app): pivot from chat UI to order console with menu/order protocol
Client (main.go):
- Replace chat viewport/textarea with a simple order console UI using Bubble Tea + Huh forms
- Add types and messages for connection, menu loading, order submission, and status
- Implement connectCmd with short greeting drain; status updates on connect
- Implement fetchMenuCmd: send "MENU", parse single-line JSON into []menuItem
- Implement submitOrderCmd: send "ORDER <json>", read single-line ack
- Build interactive form (name, menu select, quantity, confirm) with validation
- Add model state for host, status/loading/error, lastOrder, and form fields
- Keyboard controls: n (new order), r (reconnect), q/esc/ctrl+c (quit)
- Simplify program start (remove pre-connect path); alt-screen only
- Remove chat colorization/regex, viewport, and textarea logic
Server (server.go):
- Define menuItem and order structs; add a defaultMenu
- Extend protocol:
- "MENU" returns single-line JSON array of menu items
- "ORDER <json>" validates name/item/quantity (with lenient quantity parsing)
- Acknowledge with "OK" or send "[error] ..." lines
- Optionally broadcast "[order] ..." to chat for visibility
- Keep existing chat behavior; integrate new commands alongside it
Notes:
- Server logs ORDER parsing and continues to support chat commands
- Error handling surfaces server-side errors to the client status/messages
2025-10-15 22:02:41 +07:00
|
|
|
|
h.msgCh <- broadcast{
|
2025-10-15 23:14:03 +07:00
|
|
|
|
text: fmt.Sprintf("[order] %s ordered %d × %s ($%.2f)", ord.Name, ord.Quantity, chosen.Name, total),
|
feat(app): pivot from chat UI to order console with menu/order protocol
Client (main.go):
- Replace chat viewport/textarea with a simple order console UI using Bubble Tea + Huh forms
- Add types and messages for connection, menu loading, order submission, and status
- Implement connectCmd with short greeting drain; status updates on connect
- Implement fetchMenuCmd: send "MENU", parse single-line JSON into []menuItem
- Implement submitOrderCmd: send "ORDER <json>", read single-line ack
- Build interactive form (name, menu select, quantity, confirm) with validation
- Add model state for host, status/loading/error, lastOrder, and form fields
- Keyboard controls: n (new order), r (reconnect), q/esc/ctrl+c (quit)
- Simplify program start (remove pre-connect path); alt-screen only
- Remove chat colorization/regex, viewport, and textarea logic
Server (server.go):
- Define menuItem and order structs; add a defaultMenu
- Extend protocol:
- "MENU" returns single-line JSON array of menu items
- "ORDER <json>" validates name/item/quantity (with lenient quantity parsing)
- Acknowledge with "OK" or send "[error] ..." lines
- Optionally broadcast "[order] ..." to chat for visibility
- Keep existing chat behavior; integrate new commands alongside it
Notes:
- Server logs ORDER parsing and continues to support chat commands
- Error handling surfaces server-side errors to the client status/messages
2025-10-15 22:02:41 +07:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-15 22:28:03 +07:00
|
|
|
|
fmt.Fprintf(c, "OK|%.2f\n", total)
|
feat(app): pivot from chat UI to order console with menu/order protocol
Client (main.go):
- Replace chat viewport/textarea with a simple order console UI using Bubble Tea + Huh forms
- Add types and messages for connection, menu loading, order submission, and status
- Implement connectCmd with short greeting drain; status updates on connect
- Implement fetchMenuCmd: send "MENU", parse single-line JSON into []menuItem
- Implement submitOrderCmd: send "ORDER <json>", read single-line ack
- Build interactive form (name, menu select, quantity, confirm) with validation
- Add model state for host, status/loading/error, lastOrder, and form fields
- Keyboard controls: n (new order), r (reconnect), q/esc/ctrl+c (quit)
- Simplify program start (remove pre-connect path); alt-screen only
- Remove chat colorization/regex, viewport, and textarea logic
Server (server.go):
- Define menuItem and order structs; add a defaultMenu
- Extend protocol:
- "MENU" returns single-line JSON array of menu items
- "ORDER <json>" validates name/item/quantity (with lenient quantity parsing)
- Acknowledge with "OK" or send "[error] ..." lines
- Optionally broadcast "[order] ..." to chat for visibility
- Keep existing chat behavior; integrate new commands alongside it
Notes:
- Server logs ORDER parsing and continues to support chat commands
- Error handling surfaces server-side errors to the client status/messages
2025-10-15 22:02:41 +07:00
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Chat commands
|
2025-10-15 00:19:24 +07:00
|
|
|
|
if line == "/quit" {
|
2025-10-15 10:40:55 +07:00
|
|
|
|
break // unified leave handling below
|
|
|
|
|
|
}
|
2025-10-15 10:55:06 +07:00
|
|
|
|
if desired, ok := strings.CutPrefix(line, "/name "); ok {
|
2025-10-15 10:40:55 +07:00
|
|
|
|
newName := sanitizeUsername(desired)
|
|
|
|
|
|
if newName == "" {
|
|
|
|
|
|
fmt.Fprintln(c, "[error] invalid username")
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
if newName == username {
|
|
|
|
|
|
// No change
|
|
|
|
|
|
fmt.Fprintf(c, "[info] username unchanged: %s\n", username)
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
old := username
|
|
|
|
|
|
username = newName
|
|
|
|
|
|
// Broadcast rename to everyone (including the renamer)
|
2025-10-15 11:19:20 +07:00
|
|
|
|
log.Printf("rename: user=%s id=%s remote=%s", username, id, c.RemoteAddr())
|
2025-10-15 10:40:55 +07:00
|
|
|
|
h.msgCh <- broadcast{text: fmt.Sprintf("[rename] %s (%s) -> %s", old, id, username)}
|
|
|
|
|
|
continue
|
2025-10-15 00:19:24 +07:00
|
|
|
|
}
|
2025-10-15 10:40:55 +07:00
|
|
|
|
|
|
|
|
|
|
// Regular chat message
|
|
|
|
|
|
h.msgCh <- broadcast{text: fmt.Sprintf("%s (%s): %s", username, id, line)}
|
2025-10-15 00:19:24 +07:00
|
|
|
|
}
|
|
|
|
|
|
if err := scanner.Err(); err != nil {
|
2025-10-15 10:40:55 +07:00
|
|
|
|
log.Printf("read err from %s (%s): %v", username, id, err)
|
2025-10-15 00:19:24 +07:00
|
|
|
|
}
|
2025-10-15 10:40:55 +07:00
|
|
|
|
|
|
|
|
|
|
// Single, consistent leave announcement
|
2025-10-15 11:19:20 +07:00
|
|
|
|
log.Printf("leave: user=%s id=%s remote=%s", username, id, c.RemoteAddr())
|
2025-10-15 10:40:55 +07:00
|
|
|
|
h.msgCh <- broadcast{text: fmt.Sprintf("[leave] %s (%s)", username, id)}
|
2025-10-15 00:19:24 +07:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-15 10:40:55 +07:00
|
|
|
|
// startTCPServer starts a TCP chat server and never returns unless an error occurs.
|
2025-10-16 18:10:17 +07:00
|
|
|
|
func startTCPServer(addr string, menu []menuItem) error {
|
|
|
|
|
|
if len(menu) == 0 {
|
|
|
|
|
|
menu = defaultMenu
|
|
|
|
|
|
}
|
|
|
|
|
|
serverMenu = menu
|
|
|
|
|
|
|
2025-10-15 00:19:24 +07:00
|
|
|
|
ln, err := net.Listen("tcp", addr)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
|
|
|
|
|
log.Printf("TCP chat server listening on %s", ln.Addr())
|
2025-10-16 18:10:17 +07:00
|
|
|
|
log.Printf("Menu items: %d", len(serverMenu))
|
2025-10-15 00:19:24 +07:00
|
|
|
|
|
|
|
|
|
|
hub := NewHub()
|
|
|
|
|
|
go hub.Run()
|
|
|
|
|
|
|
|
|
|
|
|
for {
|
|
|
|
|
|
c, err := ln.Accept()
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
log.Printf("accept error: %v", err)
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
go handleConn(hub, c)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|