Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ make build # or: go build -o chat-server ./cmd/ts-chat
# Run the server
make run # builds and runs
./chat-server # run directly
./chat-server --plain-text # run with plain-text mode (Windows telnet compatibility)

# Run tests
make test # runs: go test -v ./internal/chat/
Expand Down
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,23 @@ docker run -e TS_AUTHKEY=tskey-auth-xxxxx chat-tails --tailscale --hostname mych
| `--hostname` | `-H` | "chatroom" | Tailscale hostname (requires `--tailscale`) |
| `--history` | | false | Enable message history for new users |
| `--history-size` | | 50 | Number of messages to keep in history |
| `--plain-text` | | false | Disable ANSI formatting (for Windows telnet) |
| `--version` | `-v` | | Show version information |

## Windows Telnet Compatibility

Windows telnet has limited ANSI escape sequence support. If you see garbled formatting characters when connecting from Windows telnet, start the server with the `--plain-text` flag:

```bash
./chat-server --plain-text
```

This disables all ANSI color codes and cursor control sequences for a better experience on legacy telnet clients.

**Recommended:** For the best experience on Windows, use a modern terminal emulator like:
- Windows Terminal with `telnet` or `ssh`
- PuTTY
- WSL with `nc` or `telnet`

## Tailscale Setup

Expand Down
3 changes: 3 additions & 0 deletions cmd/chat-tails/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ type config struct {
HostName string
EnableHistory bool
HistorySize int
PlainText bool
}

func main() {
Expand Down Expand Up @@ -72,6 +73,7 @@ func main() {
HostName: cfg.HostName,
EnableHistory: cfg.EnableHistory,
HistorySize: cfg.HistorySize,
PlainText: cfg.PlainText,
})
if err != nil {
log.Fatalf("Failed to create server: %v", err)
Expand Down Expand Up @@ -116,6 +118,7 @@ func parseFlags() (config, bool) {
pflag.StringVarP(&cfg.HostName, "hostname", "H", defaultHostname, "Tailscale hostname (only used if --tailscale is enabled)")
pflag.BoolVar(&cfg.EnableHistory, "history", false, "Enable message history for new users")
pflag.IntVar(&cfg.HistorySize, "history-size", defaultHistorySize, "Number of messages to keep in history")
pflag.BoolVar(&cfg.PlainText, "plain-text", false, "Disable ANSI formatting (for Windows telnet compatibility)")
pflag.BoolVarP(&showVersion, "version", "v", false, "Show version information")

// Display help message
Expand Down
78 changes: 63 additions & 15 deletions internal/chat/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,13 @@ func NewClient(conn net.Conn, room *Room) (*Client, error) {
// requestNickname asks the user for a nickname
func (c *Client) requestNickname() error {
// Send welcome message
if err := c.write(ui.FormatTitle("Welcome to Chat Tails") + "\r\n\r\n"); err != nil {
var welcomeTitle string
if c.room.PlainText {
welcomeTitle = ui.FormatTitlePlain("Welcome to Chat Tails")
} else {
welcomeTitle = ui.FormatTitle("Welcome to Chat Tails")
}
if err := c.write(welcomeTitle + "\r\n\r\n"); err != nil {
return fmt.Errorf("failed to write welcome message: %w", err)
}

Expand Down Expand Up @@ -176,13 +182,21 @@ func (c *Client) sendWelcomeMessage() error {
║ ║
╚════════════════════════════════════════════════════════════╝
`
coloredBanner := ui.SystemStyle.Render(banner)
welcomeMsg := ui.FormatWelcomeMessage(c.room.Name, c.Nickname)

var coloredBanner, welcomeMsg string

if c.room.PlainText {
// In plain text mode, skip the banner styling
coloredBanner = banner
welcomeMsg = ui.FormatWelcomeMessagePlain(c.room.Name, c.Nickname)
} else {
coloredBanner = ui.SystemStyle.Render(banner)
welcomeMsg = ui.FormatWelcomeMessage(c.room.Name, c.Nickname)
}

if err := c.write(coloredBanner + "\r\n"); err != nil {
return fmt.Errorf("failed to write banner: %w", err)
}

if err := c.write(welcomeMsg + "\r\n\r\n"); err != nil {
return fmt.Errorf("failed to write welcome message: %w", err)
}
Expand All @@ -197,13 +211,22 @@ func (c *Client) sendHistory() {
return
}

c.write(ui.FormatSystemMessage("--- Recent messages ---") + "\r\n")
var headerMsg, footerMsg string
if c.room.PlainText {
headerMsg = ui.FormatSystemMessagePlain("--- Recent messages ---")
footerMsg = ui.FormatSystemMessagePlain("--- End of history ---")
} else {
headerMsg = ui.FormatSystemMessage("--- Recent messages ---")
footerMsg = ui.FormatSystemMessage("--- End of history ---")
}

c.write(headerMsg + "\r\n")

for _, msg := range history {
c.sendMessage(msg)
}

c.write(ui.FormatSystemMessage("--- End of history ---") + "\r\n\r\n")
c.write(footerMsg + "\r\n\r\n")
}

// Handle handles client interactions
Expand Down Expand Up @@ -300,7 +323,10 @@ func (c *Client) Handle(ctx context.Context) {

// clearInputLine clears the echoed input line
func (c *Client) clearInputLine() {
c.write(cursorUp + clearLine + cursorToStart)
// Skip cursor manipulation in plain text mode
if !c.room.PlainText {
c.write(cursorUp + clearLine + cursorToStart)
}
}

// showPrompt displays the input prompt
Expand Down Expand Up @@ -399,13 +425,23 @@ func (c *Client) handleCommand(cmd string) error {
// showUserList shows the list of users in the room
func (c *Client) showUserList() error {
users := c.room.GetUserList()
msg := ui.FormatUserList(c.room.Name, users, c.room.MaxUsers)
var msg string
if c.room.PlainText {
msg = ui.FormatUserListPlain(c.room.Name, users, c.room.MaxUsers)
} else {
msg = ui.FormatUserList(c.room.Name, users, c.room.MaxUsers)
}
return c.write(msg + "\r\n")
}

// showHelp shows the help message
func (c *Client) showHelp() error {
helpMsg := ui.FormatHelp()
var helpMsg string
if c.room.PlainText {
helpMsg = ui.FormatHelpPlain()
} else {
helpMsg = ui.FormatHelp()
}
return c.write(helpMsg + "\r\n")
}

Expand All @@ -426,12 +462,24 @@ func (c *Client) sendMessage(msg Message) {
var formatted string
timeStr := msg.Timestamp.Format("15:04:05")

if msg.IsSystem {
formatted = ui.FormatSystemMessage(msg.Content) + "\r\n"
} else if msg.IsAction {
formatted = ui.FormatActionMessage(msg.From, msg.Content) + "\r\n"
if c.room.PlainText {
// Use plain text formatters
if msg.IsSystem {
formatted = ui.FormatSystemMessagePlain(msg.Content) + "\r\n"
} else if msg.IsAction {
formatted = ui.FormatActionMessagePlain(msg.From, msg.Content) + "\r\n"
} else {
formatted = ui.FormatUserMessagePlain(msg.From, msg.Content, timeStr) + "\r\n"
}
} else {
formatted = ui.FormatUserMessage(msg.From, msg.Content, timeStr) + "\r\n"
// Use ANSI formatters
if msg.IsSystem {
formatted = ui.FormatSystemMessage(msg.Content) + "\r\n"
} else if msg.IsAction {
formatted = ui.FormatActionMessage(msg.From, msg.Content) + "\r\n"
} else {
formatted = ui.FormatUserMessage(msg.From, msg.Content, timeStr) + "\r\n"
}
}

c.mu.Lock()
Expand Down
4 changes: 3 additions & 1 deletion internal/chat/room.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,11 @@ type Room struct {
historySize int
history []Message
historyMu sync.RWMutex
PlainText bool
}

// NewRoom creates a new chat room
func NewRoom(name string, maxUsers int, enableHistory bool, historySize int) *Room {
func NewRoom(name string, maxUsers int, enableHistory bool, historySize int, plainText bool) *Room {
ctx, cancel := context.WithCancel(context.Background())
room := &Room{
Name: name,
Expand All @@ -50,6 +51,7 @@ func NewRoom(name string, maxUsers int, enableHistory bool, historySize int) *Ro
enableHistory: enableHistory,
historySize: historySize,
history: make([]Message, 0, historySize),
PlainText: plainText,
}

go room.run()
Expand Down
6 changes: 3 additions & 3 deletions internal/chat/room_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import (
)

func TestNewRoom(t *testing.T) {
room := NewRoom("Test Room", 10, false, 0)
room := NewRoom("Test Room", 10, false, 0, false)
defer room.Stop()

if room.Name != "Test Room" {
Expand Down Expand Up @@ -35,7 +35,7 @@ func TestNewRoom(t *testing.T) {
}

func TestRoomStop(t *testing.T) {
room := NewRoom("Test Room", 10, false, 0)
room := NewRoom("Test Room", 10, false, 0, false)

// Give room time to start
time.Sleep(10 * time.Millisecond)
Expand Down Expand Up @@ -83,7 +83,7 @@ func TestMessageStruct(t *testing.T) {
}

func TestRoomChannels(t *testing.T) {
room := NewRoom("Test Room", 5, false, 0)
room := NewRoom("Test Room", 5, false, 0, false)
defer room.Stop()

// Test that channels are properly initialized
Expand Down
1 change: 1 addition & 0 deletions internal/server/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,5 @@ type Config struct {
HostName string // Tailscale hostname (only used if EnableTailscale is true)
EnableHistory bool // Whether to enable message history for new users
HistorySize int // Number of messages to keep in history
PlainText bool // Whether to disable ANSI formatting (for Windows telnet compatibility)
}
2 changes: 1 addition & 1 deletion internal/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ func NewServer(cfg Config) (*Server, error) {
ctx, cancel := context.WithCancel(context.Background())

// Create a new chat room
room := chat.NewRoom(cfg.RoomName, cfg.MaxUsers, cfg.EnableHistory, cfg.HistorySize)
room := chat.NewRoom(cfg.RoomName, cfg.MaxUsers, cfg.EnableHistory, cfg.HistorySize, cfg.PlainText)

return &Server{
config: cfg,
Expand Down
55 changes: 55 additions & 0 deletions internal/ui/plaintext.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package ui

import "fmt"

// PlainText formatters for Windows telnet and other clients with limited ANSI support

// FormatSystemMessagePlain formats a system message without ANSI codes
func FormatSystemMessagePlain(message string) string {
return "[System] " + message
}

// FormatUserMessagePlain formats a user message without ANSI codes
func FormatUserMessagePlain(username, message, timestamp string) string {
return "[" + timestamp + "] " + username + ": " + message
}

// FormatSelfMessagePlain formats the user's own message without ANSI codes
func FormatSelfMessagePlain(message, timestamp string) string {
return "[" + timestamp + "] You: " + message
}

// FormatActionMessagePlain formats an action message without ANSI codes
func FormatActionMessagePlain(username, action string) string {
return "* " + username + " " + action
}

// FormatTitlePlain formats a title without ANSI codes
func FormatTitlePlain(title string) string {
return "=== " + title + " ==="
}

// FormatHelpPlain formats the help message without ANSI codes
func FormatHelpPlain() string {
return `
Available Commands:
/who - Show all users in the room
/me <action> - Perform an action
/help - Show this help message
/quit - Leave the chat
`
}

// FormatUserListPlain formats the user list without ANSI codes
func FormatUserListPlain(roomName string, users []string, maxUsers int) string {
content := fmt.Sprintf("Users in %s (%d/%d):\n", roomName, len(users), maxUsers)
for _, user := range users {
content += "- " + user + "\n"
}
return content
}

// FormatWelcomeMessagePlain formats the welcome message without ANSI codes
func FormatWelcomeMessagePlain(roomName, nickname string) string {
return fmt.Sprintf("Welcome to %s, %s!\n\nType a message and press Enter to send. Use /help to see available commands.", roomName, nickname)
}