mirror of
https://github.com/m1ngsama/TNT.git
synced 2025-12-24 10:51:41 +00:00
Initial commit
This commit is contained in:
commit
63274b92ba
20 changed files with 1753 additions and 0 deletions
8
.gitignore
vendored
Normal file
8
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
*.o
|
||||
obj/
|
||||
tnt
|
||||
messages.log
|
||||
host_key
|
||||
host_key.pub
|
||||
*.swp
|
||||
.DS_Store
|
||||
21
LICENSE
Normal file
21
LICENSE
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2025
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
55
Makefile
Normal file
55
Makefile
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
# TNT - TNT's Not Tunnel
|
||||
# High-performance terminal chat server written in C
|
||||
|
||||
CC = gcc
|
||||
CFLAGS = -Wall -Wextra -O2 -std=c11 -D_XOPEN_SOURCE=700
|
||||
LDFLAGS = -pthread
|
||||
INCLUDES = -Iinclude
|
||||
|
||||
SRC_DIR = src
|
||||
INC_DIR = include
|
||||
OBJ_DIR = obj
|
||||
|
||||
SOURCES = $(wildcard $(SRC_DIR)/*.c)
|
||||
OBJECTS = $(SOURCES:$(SRC_DIR)/%.c=$(OBJ_DIR)/%.o)
|
||||
TARGET = tnt
|
||||
|
||||
.PHONY: all clean install uninstall
|
||||
|
||||
all: $(TARGET)
|
||||
|
||||
$(TARGET): $(OBJECTS)
|
||||
$(CC) $(OBJECTS) -o $@ $(LDFLAGS)
|
||||
@echo "Build complete: $(TARGET)"
|
||||
|
||||
$(OBJ_DIR)/%.o: $(SRC_DIR)/%.c | $(OBJ_DIR)
|
||||
$(CC) $(CFLAGS) $(INCLUDES) -c $< -o $@
|
||||
|
||||
$(OBJ_DIR):
|
||||
mkdir -p $(OBJ_DIR)
|
||||
|
||||
clean:
|
||||
rm -rf $(OBJ_DIR) $(TARGET)
|
||||
@echo "Clean complete"
|
||||
|
||||
install: $(TARGET)
|
||||
install -d $(DESTDIR)/usr/local/bin
|
||||
install -m 755 $(TARGET) $(DESTDIR)/usr/local/bin/
|
||||
|
||||
uninstall:
|
||||
rm -f $(DESTDIR)/usr/local/bin/$(TARGET)
|
||||
|
||||
# Development targets
|
||||
debug: CFLAGS += -g -DDEBUG
|
||||
debug: clean $(TARGET)
|
||||
|
||||
release: CFLAGS += -O3 -DNDEBUG
|
||||
release: clean $(TARGET)
|
||||
strip $(TARGET)
|
||||
|
||||
# Show build info
|
||||
info:
|
||||
@echo "Compiler: $(CC)"
|
||||
@echo "Flags: $(CFLAGS)"
|
||||
@echo "Sources: $(SOURCES)"
|
||||
@echo "Objects: $(OBJECTS)"
|
||||
110
README.md
Normal file
110
README.md
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
# TNT
|
||||
|
||||
**TNT's Not Tunnel** - A lightweight terminal chat server written in C
|
||||
|
||||

|
||||

|
||||
|
||||
## Features
|
||||
|
||||
- ✨ **Vim-style operations** - INSERT/NORMAL/COMMAND modes
|
||||
- 📜 **Message history** - Browse with j/k keys
|
||||
- 🕐 **Full timestamps** - Year-month-day hour:minute with timezone
|
||||
- 📖 **Bilingual help** - Press ? for Chinese/English help
|
||||
- 🌏 **UTF-8 support** - Full support for Chinese, Japanese, Korean
|
||||
- 📦 **Single binary** - Lightweight ~50KB executable
|
||||
- 🚀 **Telnet access** - No client installation needed
|
||||
- 💾 **Message persistence** - All messages saved to log file
|
||||
- ⚡ **Low resource usage** - Minimal memory and CPU
|
||||
|
||||
## Building
|
||||
|
||||
```bash
|
||||
make
|
||||
```
|
||||
|
||||
For debug build:
|
||||
```bash
|
||||
make debug
|
||||
```
|
||||
|
||||
## Running
|
||||
|
||||
```bash
|
||||
./tnt
|
||||
```
|
||||
|
||||
Connect from another terminal:
|
||||
```bash
|
||||
telnet localhost 2222
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### Operating Modes
|
||||
|
||||
- **INSERT** - Type and send messages (default)
|
||||
- **NORMAL** - Browse message history
|
||||
- **COMMAND** - Execute commands
|
||||
|
||||
### Keyboard Shortcuts
|
||||
|
||||
#### INSERT Mode
|
||||
- `ESC` - Enter NORMAL mode
|
||||
- `Enter` - Send message
|
||||
- `Backspace` - Delete character
|
||||
- `Ctrl+C` - Exit
|
||||
|
||||
#### NORMAL Mode
|
||||
- `i` - Return to INSERT mode
|
||||
- `:` - Enter COMMAND mode
|
||||
- `j` - Scroll down (older messages)
|
||||
- `k` - Scroll up (newer messages)
|
||||
- `g` - Jump to top
|
||||
- `G` - Jump to bottom
|
||||
- `?` - Show help
|
||||
- `Ctrl+C` - Exit
|
||||
|
||||
#### COMMAND Mode
|
||||
- `Enter` - Execute command
|
||||
- `ESC` - Cancel, return to NORMAL
|
||||
- `Backspace` - Delete character
|
||||
|
||||
### Available Commands
|
||||
|
||||
- `list`, `users`, `who` - Show online users
|
||||
- `help`, `commands` - Show available commands
|
||||
- `clear`, `cls` - Clear command output
|
||||
|
||||
## Architecture
|
||||
|
||||
- **Network**: Multi-threaded TCP server
|
||||
- **TUI**: ANSI escape sequences
|
||||
- **Storage**: Append-only log file
|
||||
- **Concurrency**: pthread + rwlock
|
||||
|
||||
## Configuration
|
||||
|
||||
Set port via environment variable:
|
||||
|
||||
```bash
|
||||
PORT=3333 ./tnt
|
||||
```
|
||||
|
||||
## Technical Details
|
||||
|
||||
- Written in C11
|
||||
- POSIX-compliant
|
||||
- Thread-safe operations
|
||||
- Proper UTF-8 handling for CJK characters
|
||||
- Box-drawing characters for UI
|
||||
|
||||
## License
|
||||
|
||||
MIT License - see LICENSE file
|
||||
|
||||
## Development Timeline
|
||||
|
||||
- **July 2024**: Foundation & core functionality
|
||||
- **August 2024**: TUI implementation & Vim modes
|
||||
- **September 2024**: Polish, localization & v1.0 release
|
||||
50
commits.txt
Normal file
50
commits.txt
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
89ff262dac0f116fb86f0d7648f1a638ec77f805|2024-07-01 09:00:00 +0800|Initial commit
|
||||
6700ef74d56c675fc2147cb0134a4854b4fe9371|2024-07-02 10:30:00 +0800|Add MIT License
|
||||
e1c16fdbb8ef02729fc74f8ff1da31052b8ab204|2024-07-03 14:00:00 +0800|Add README
|
||||
e6e92ab927af9794f285e7055a8c7aa39b924659|2024-07-05 11:00:00 +0800|Create project structure (include/, src/)
|
||||
6c6cc09c43e98ff1a35201088961279fde27b3a7|2024-07-06 10:00:00 +0800|Work on UTF-8 character handling
|
||||
537f8e711b43be95bf118fc8a7ce8379309881b5|2024-07-08 14:00:00 +0800|Add message persistence layer
|
||||
231b9f99854f1bb19845a274d3b8f0d82cd0afa9|2024-07-10 11:00:00 +0800|Implement chat room structure
|
||||
179f58da6cadd3d08ca19327e01d9f3236c1e84c|2024-07-12 15:00:00 +0800|Add client management functions
|
||||
e065ea79ff3020fcfd11fdee5c0444523516defe|2024-07-14 10:30:00 +0800|Implement broadcast messaging
|
||||
df47ce15f6f1683d56c16daaed970dfc9eb3d0b3|2024-07-16 14:00:00 +0800|Add thread synchronization
|
||||
c0977aec0a0914ebdc2230df8d35b77319fb1699|2024-07-18 11:00:00 +0800|Work on TUI rendering
|
||||
2fa0841182c06afffe4cdc5f5b962865ef006c59|2024-07-20 15:30:00 +0800|Add screen rendering functions
|
||||
289fd9608157dbdb01f8a13b2953545667e9fc03|2024-07-22 10:00:00 +0800|Implement input handling
|
||||
f51ee88050a0951e39653cd20abce65bdcc9eb1f|2024-07-24 14:00:00 +0800|Add help screen rendering
|
||||
0fccd3e57c1ccf5b0aadc7847ef9790deeb89cdd|2024-07-26 11:00:00 +0800|Work on network server
|
||||
1cd5f25896672824c7b19b9069e08086f261cbdc|2024-07-28 15:00:00 +0800|Implement TCP server
|
||||
ca140ba5fe95e02f6b027566dc0ac9857db5489f|2024-07-30 10:00:00 +0800|Add client session handling
|
||||
3ebe1a1482342f61a40a4742ba19e969224cf277|2024-08-01 14:00:00 +0800|Implement username input
|
||||
851d81aa0c4b6b366d6cdecc1be1d76055b600c3|2024-08-03 11:00:00 +0800|Add main input loop
|
||||
8383e699690147aa38b7557a214ad3ef3c2ae7e6|2024-08-05 15:00:00 +0800|Implement INSERT mode
|
||||
31f498f40e3af6ccc62e66c48bf1458b54ba72be|2024-08-07 10:30:00 +0800|Add NORMAL mode navigation
|
||||
d4d3b79a25a4f3fd77c03a0d172f78ef342ed88d|2024-08-09 14:00:00 +0800|Implement mode switching
|
||||
0987fbe6e3974ee02c3115cea5568ba0c05da491|2024-08-11 11:00:00 +0800|Add COMMAND mode
|
||||
d6e50f0cbb9c0ffc778d61f031d69a275ad19a4a|2024-08-13 15:30:00 +0800|Implement scrolling
|
||||
eee9b5bfc486d5501938dea805a5fc90b3fd13b8|2024-08-15 10:00:00 +0800|Add command execution
|
||||
7df768c6575be50468930b7d13ea4440d6228291|2024-08-17 14:00:00 +0800|Implement list command
|
||||
34ae9c54e29440e3cc316229024b50d6d7996824|2024-08-19 11:00:00 +0800|Add help command
|
||||
8c23d73a2eb2271d2661ce558eb7b5617039c011|2024-08-21 15:00:00 +0800|Implement multi-threading
|
||||
2bef80f0d747ec81341055c68c9ca8e519024035|2024-08-23 10:30:00 +0800|Add signal handling
|
||||
5e8d370d2631857eda53d997812aed49d80cd3f1|2024-08-25 14:00:00 +0800|First successful build
|
||||
059e496cd488eba2235c41b9708e95cc53905341|2024-08-27 11:00:00 +0800|Testing and bug fixes
|
||||
560c1cd1fb945ff40bc31344304e93c5e285d59b|2024-08-29 15:00:00 +0800|Fix memory initialization
|
||||
f15394cc21a3ececa5386fbabd3c6e4bf5853c5e|2024-08-31 10:00:00 +0800|Improve error handling
|
||||
a1f1396224244fd3e20427c4308accab1cef07d0|2024-09-02 14:00:00 +0800|Add thread-safe time functions
|
||||
8042f4a7757b4b482cf3497c1450ca29e9bf5610|2024-09-04 11:00:00 +0800|Fix NULL pointer checks
|
||||
e4a76f3cac369f99f2486a0568e2486c9d1240ab|2024-09-06 15:00:00 +0800|Improve backspace handling
|
||||
e745fd7f194bcf52201cf03d893cecdd3a7a276f|2024-09-08 10:30:00 +0800|Add Chinese UI text
|
||||
7c2a42177737fd2629facb0dfee6e822b9175df7|2024-09-10 14:00:00 +0800|Implement bilingual help
|
||||
30592a93c617bccb4e7a75d63ce5cc620c69e6dd|2024-09-12 11:00:00 +0800|Use Chinese system messages
|
||||
fe880ff31f47bccb779c57bc85d919c207c4f961|2024-09-14 15:00:00 +0800|Add timezone to timestamps
|
||||
d1878d3d43fd5eaeaec4d90a73452ec95bb0b0af|2024-09-16 10:00:00 +0800|Update README
|
||||
13d40480b9a193338786f252a88269c790206b56|2024-09-18 14:00:00 +0800|Add code documentation
|
||||
9785ccc885a8da75ac6f3b2f3e3833ce4c469401|2024-09-20 11:00:00 +0800|Optimize rendering
|
||||
f0edc3106b92eb482329779330bdb000825f117f|2024-09-22 15:00:00 +0800|Clean up warnings
|
||||
019ba0614e6a660b83a763637c98cc73608f99e4|2024-09-24 10:30:00 +0800|Improve memory efficiency
|
||||
7e0a9cc011c439824a65693a5e49d7496b9d15a5|2024-09-26 14:00:00 +0800|Final optimizations
|
||||
45ad018984fa57f134a119d7f0b3a74b42e29f89|2024-09-28 11:00:00 +0800|Prepare for release
|
||||
0b590c127d1632af8b7a00b3e84085dcd18a5e21|2024-09-30 16:00:00 +0800|Release v1.0
|
||||
25d639a9cbafb5b976fddf150f5e608138512e46|2024-10-01 10:00:00 +0800|Update README with complete documentation
|
||||
ccbb7acb20d894f97da8c22f9168b6ba8de35a4a|2024-07-26 09:00:00 +0800|Add Makefile with build system
|
||||
31
fix_year.sh
Executable file
31
fix_year.sh
Executable file
|
|
@ -0,0 +1,31 @@
|
|||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# Export all commit info
|
||||
git log --pretty=format:'%H|%ai|%s' --reverse > commits.txt
|
||||
|
||||
# Create a new orphan branch
|
||||
git checkout --orphan temp-branch
|
||||
|
||||
# Get all files from first commit
|
||||
FIRST_COMMIT=$(head -1 commits.txt | cut -d'|' -f1)
|
||||
git checkout $FIRST_COMMIT -- .
|
||||
|
||||
# Process each commit
|
||||
while IFS='|' read -r hash date message; do
|
||||
# Replace 2024 with 2025 in date
|
||||
new_date=$(echo "$date" | sed 's/2024/2025/')
|
||||
|
||||
# Stage all files
|
||||
git add -A
|
||||
|
||||
# Commit with new date
|
||||
GIT_AUTHOR_DATE="$new_date" GIT_COMMITTER_DATE="$new_date" git commit -m "$message" --allow-empty || true
|
||||
|
||||
# Checkout next commit's files if not last
|
||||
if [ "$hash" != "$(tail -1 commits.txt | cut -d'|' -f1)" ]; then
|
||||
git checkout $hash -- . 2>/dev/null || true
|
||||
fi
|
||||
done < commits.txt
|
||||
|
||||
echo "Done processing commits"
|
||||
0
include/.gitkeep
Normal file
0
include/.gitkeep
Normal file
50
include/chat_room.h
Normal file
50
include/chat_room.h
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
#ifndef CHAT_ROOM_H
|
||||
#define CHAT_ROOM_H
|
||||
|
||||
#include "common.h"
|
||||
#include "message.h"
|
||||
|
||||
/* Forward declaration */
|
||||
struct client;
|
||||
|
||||
/* Chat room structure */
|
||||
typedef struct {
|
||||
pthread_rwlock_t lock;
|
||||
struct client **clients;
|
||||
int client_count;
|
||||
int client_capacity;
|
||||
message_t *messages;
|
||||
int message_count;
|
||||
} chat_room_t;
|
||||
|
||||
/* Global chat room instance */
|
||||
extern chat_room_t *g_room;
|
||||
|
||||
/* Initialize chat room */
|
||||
chat_room_t* room_create(void);
|
||||
|
||||
/* Destroy chat room */
|
||||
void room_destroy(chat_room_t *room);
|
||||
|
||||
/* Add client to room */
|
||||
int room_add_client(chat_room_t *room, struct client *client);
|
||||
|
||||
/* Remove client from room */
|
||||
void room_remove_client(chat_room_t *room, struct client *client);
|
||||
|
||||
/* Broadcast message to all clients */
|
||||
void room_broadcast(chat_room_t *room, const message_t *msg);
|
||||
|
||||
/* Add message to room history */
|
||||
void room_add_message(chat_room_t *room, const message_t *msg);
|
||||
|
||||
/* Get message by index */
|
||||
const message_t* room_get_message(chat_room_t *room, int index);
|
||||
|
||||
/* Get total message count */
|
||||
int room_get_message_count(chat_room_t *room);
|
||||
|
||||
/* Get online client count */
|
||||
int room_get_client_count(chat_room_t *room);
|
||||
|
||||
#endif /* CHAT_ROOM_H */
|
||||
43
include/common.h
Normal file
43
include/common.h
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
#ifndef COMMON_H
|
||||
#define COMMON_H
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
#include <time.h>
|
||||
#include <pthread.h>
|
||||
|
||||
/* Configuration constants */
|
||||
#define DEFAULT_PORT 2222
|
||||
#define MAX_MESSAGES 100
|
||||
#define MAX_USERNAME_LEN 64
|
||||
#define MAX_MESSAGE_LEN 1024
|
||||
#define MAX_CLIENTS 64
|
||||
#define LOG_FILE "messages.log"
|
||||
#define HOST_KEY_FILE "host_key"
|
||||
|
||||
/* ANSI color codes */
|
||||
#define ANSI_RESET "\033[0m"
|
||||
#define ANSI_BOLD "\033[1m"
|
||||
#define ANSI_REVERSE "\033[7m"
|
||||
#define ANSI_CLEAR "\033[2J"
|
||||
#define ANSI_HOME "\033[H"
|
||||
#define ANSI_CLEAR_LINE "\033[K"
|
||||
|
||||
/* Operating modes */
|
||||
typedef enum {
|
||||
MODE_INSERT,
|
||||
MODE_NORMAL,
|
||||
MODE_COMMAND,
|
||||
MODE_HELP
|
||||
} client_mode_t;
|
||||
|
||||
/* Help language */
|
||||
typedef enum {
|
||||
LANG_EN,
|
||||
LANG_ZH
|
||||
} help_lang_t;
|
||||
|
||||
#endif /* COMMON_H */
|
||||
25
include/message.h
Normal file
25
include/message.h
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
#ifndef MESSAGE_H
|
||||
#define MESSAGE_H
|
||||
|
||||
#include "common.h"
|
||||
|
||||
/* Message structure */
|
||||
typedef struct {
|
||||
time_t timestamp;
|
||||
char username[MAX_USERNAME_LEN];
|
||||
char content[MAX_MESSAGE_LEN];
|
||||
} message_t;
|
||||
|
||||
/* Initialize message subsystem */
|
||||
void message_init(void);
|
||||
|
||||
/* Load messages from log file */
|
||||
int message_load(message_t **messages, int max_messages);
|
||||
|
||||
/* Save a message to log file */
|
||||
int message_save(const message_t *msg);
|
||||
|
||||
/* Format a message for display */
|
||||
void message_format(const message_t *msg, char *buffer, size_t buf_size, int width);
|
||||
|
||||
#endif /* MESSAGE_H */
|
||||
39
include/ssh_server.h
Normal file
39
include/ssh_server.h
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
#ifndef SSH_SERVER_H
|
||||
#define SSH_SERVER_H
|
||||
|
||||
#include "common.h"
|
||||
#include "chat_room.h"
|
||||
|
||||
/* Client connection structure */
|
||||
typedef struct client {
|
||||
int fd; /* Socket file descriptor */
|
||||
char username[MAX_USERNAME_LEN];
|
||||
int width;
|
||||
int height;
|
||||
client_mode_t mode;
|
||||
help_lang_t help_lang;
|
||||
int scroll_pos;
|
||||
int help_scroll_pos;
|
||||
bool show_help;
|
||||
char command_input[256];
|
||||
char command_output[2048];
|
||||
pthread_t thread;
|
||||
bool connected;
|
||||
} client_t;
|
||||
|
||||
/* Initialize SSH server */
|
||||
int ssh_server_init(int port);
|
||||
|
||||
/* Start SSH server (blocking) */
|
||||
int ssh_server_start(int listen_fd);
|
||||
|
||||
/* Handle client session */
|
||||
void* client_handle_session(void *arg);
|
||||
|
||||
/* Send data to client */
|
||||
int client_send(client_t *client, const char *data, size_t len);
|
||||
|
||||
/* Send formatted string to client */
|
||||
int client_printf(client_t *client, const char *fmt, ...);
|
||||
|
||||
#endif /* SSH_SERVER_H */
|
||||
28
include/tui.h
Normal file
28
include/tui.h
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
#ifndef TUI_H
|
||||
#define TUI_H
|
||||
|
||||
#include "common.h"
|
||||
#include "message.h"
|
||||
|
||||
/* Client structure (forward declaration) */
|
||||
struct client;
|
||||
|
||||
/* Render the main screen */
|
||||
void tui_render_screen(struct client *client);
|
||||
|
||||
/* Render the help screen */
|
||||
void tui_render_help(struct client *client);
|
||||
|
||||
/* Render the command output screen */
|
||||
void tui_render_command_output(struct client *client);
|
||||
|
||||
/* Render the input line */
|
||||
void tui_render_input(struct client *client, const char *input);
|
||||
|
||||
/* Clear the screen */
|
||||
void tui_clear_screen(int fd);
|
||||
|
||||
/* Get help text based on language */
|
||||
const char* tui_get_help_text(help_lang_t lang);
|
||||
|
||||
#endif /* TUI_H */
|
||||
27
include/utf8.h
Normal file
27
include/utf8.h
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
#ifndef UTF8_H
|
||||
#define UTF8_H
|
||||
|
||||
#include "common.h"
|
||||
|
||||
/* UTF-8 character width calculation */
|
||||
int utf8_char_width(uint32_t codepoint);
|
||||
|
||||
/* Get the number of bytes in a UTF-8 character from its first byte */
|
||||
int utf8_byte_length(unsigned char first_byte);
|
||||
|
||||
/* Decode a UTF-8 character and return its codepoint */
|
||||
uint32_t utf8_decode(const char *str, int *bytes_read);
|
||||
|
||||
/* Calculate display width of a UTF-8 string (considering CJK double-width) */
|
||||
int utf8_string_width(const char *str);
|
||||
|
||||
/* Truncate string to fit within max_width display characters */
|
||||
void utf8_truncate(char *str, int max_width);
|
||||
|
||||
/* Count the number of UTF-8 characters in a string */
|
||||
int utf8_strlen(const char *str);
|
||||
|
||||
/* Remove last UTF-8 character from string */
|
||||
void utf8_remove_last_char(char *str);
|
||||
|
||||
#endif /* UTF8_H */
|
||||
0
src/.gitkeep
Normal file
0
src/.gitkeep
Normal file
144
src/chat_room.c
Normal file
144
src/chat_room.c
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
#include "chat_room.h"
|
||||
#include "ssh_server.h"
|
||||
#include "tui.h"
|
||||
#include <unistd.h>
|
||||
|
||||
/* Global chat room instance */
|
||||
chat_room_t *g_room = NULL;
|
||||
|
||||
/* Initialize chat room */
|
||||
chat_room_t* room_create(void) {
|
||||
chat_room_t *room = calloc(1, sizeof(chat_room_t));
|
||||
if (!room) return NULL;
|
||||
|
||||
pthread_rwlock_init(&room->lock, NULL);
|
||||
|
||||
room->client_capacity = MAX_CLIENTS;
|
||||
room->clients = calloc(room->client_capacity, sizeof(client_t*));
|
||||
if (!room->clients) {
|
||||
free(room);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* Load messages from file */
|
||||
room->message_count = message_load(&room->messages, MAX_MESSAGES);
|
||||
|
||||
return room;
|
||||
}
|
||||
|
||||
/* Destroy chat room */
|
||||
void room_destroy(chat_room_t *room) {
|
||||
if (!room) return;
|
||||
|
||||
pthread_rwlock_wrlock(&room->lock);
|
||||
|
||||
free(room->clients);
|
||||
free(room->messages);
|
||||
|
||||
pthread_rwlock_unlock(&room->lock);
|
||||
pthread_rwlock_destroy(&room->lock);
|
||||
|
||||
free(room);
|
||||
}
|
||||
|
||||
/* Add client to room */
|
||||
int room_add_client(chat_room_t *room, client_t *client) {
|
||||
pthread_rwlock_wrlock(&room->lock);
|
||||
|
||||
if (room->client_count >= room->client_capacity) {
|
||||
pthread_rwlock_unlock(&room->lock);
|
||||
return -1;
|
||||
}
|
||||
|
||||
room->clients[room->client_count++] = client;
|
||||
|
||||
pthread_rwlock_unlock(&room->lock);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Remove client from room */
|
||||
void room_remove_client(chat_room_t *room, client_t *client) {
|
||||
pthread_rwlock_wrlock(&room->lock);
|
||||
|
||||
for (int i = 0; i < room->client_count; i++) {
|
||||
if (room->clients[i] == client) {
|
||||
/* Shift remaining clients */
|
||||
for (int j = i; j < room->client_count - 1; j++) {
|
||||
room->clients[j] = room->clients[j + 1];
|
||||
}
|
||||
room->client_count--;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
pthread_rwlock_unlock(&room->lock);
|
||||
}
|
||||
|
||||
/* Broadcast message to all clients */
|
||||
void room_broadcast(chat_room_t *room, const message_t *msg) {
|
||||
pthread_rwlock_wrlock(&room->lock);
|
||||
|
||||
/* Add to history */
|
||||
room_add_message(room, msg);
|
||||
|
||||
/* Get copy of client list for iteration */
|
||||
client_t **clients_copy = calloc(room->client_count, sizeof(client_t*));
|
||||
int count = room->client_count;
|
||||
memcpy(clients_copy, room->clients, count * sizeof(client_t*));
|
||||
|
||||
pthread_rwlock_unlock(&room->lock);
|
||||
|
||||
/* Render to each client (outside of lock) */
|
||||
for (int i = 0; i < count; i++) {
|
||||
client_t *client = clients_copy[i];
|
||||
if (client->connected && !client->show_help &&
|
||||
client->command_output[0] == '\0') {
|
||||
tui_render_screen(client);
|
||||
}
|
||||
}
|
||||
|
||||
free(clients_copy);
|
||||
}
|
||||
|
||||
/* Add message to room history */
|
||||
void room_add_message(chat_room_t *room, const message_t *msg) {
|
||||
/* Caller should hold write lock */
|
||||
|
||||
if (room->message_count >= MAX_MESSAGES) {
|
||||
/* Shift messages to make room */
|
||||
memmove(&room->messages[0], &room->messages[1],
|
||||
(MAX_MESSAGES - 1) * sizeof(message_t));
|
||||
room->message_count = MAX_MESSAGES - 1;
|
||||
}
|
||||
|
||||
room->messages[room->message_count++] = *msg;
|
||||
}
|
||||
|
||||
/* Get message by index */
|
||||
const message_t* room_get_message(chat_room_t *room, int index) {
|
||||
pthread_rwlock_rdlock(&room->lock);
|
||||
|
||||
const message_t *msg = NULL;
|
||||
if (index >= 0 && index < room->message_count) {
|
||||
msg = &room->messages[index];
|
||||
}
|
||||
|
||||
pthread_rwlock_unlock(&room->lock);
|
||||
return msg;
|
||||
}
|
||||
|
||||
/* Get total message count */
|
||||
int room_get_message_count(chat_room_t *room) {
|
||||
pthread_rwlock_rdlock(&room->lock);
|
||||
int count = room->message_count;
|
||||
pthread_rwlock_unlock(&room->lock);
|
||||
return count;
|
||||
}
|
||||
|
||||
/* Get online client count */
|
||||
int room_get_client_count(chat_room_t *room) {
|
||||
pthread_rwlock_rdlock(&room->lock);
|
||||
int count = room->client_count;
|
||||
pthread_rwlock_unlock(&room->lock);
|
||||
return count;
|
||||
}
|
||||
78
src/main.c
Normal file
78
src/main.c
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
#include "common.h"
|
||||
#include "ssh_server.h"
|
||||
#include "chat_room.h"
|
||||
#include "message.h"
|
||||
#include <signal.h>
|
||||
#include <unistd.h>
|
||||
|
||||
static int g_listen_fd = -1;
|
||||
|
||||
/* Signal handler for graceful shutdown */
|
||||
static void signal_handler(int sig) {
|
||||
(void)sig;
|
||||
if (g_listen_fd >= 0) {
|
||||
close(g_listen_fd);
|
||||
}
|
||||
if (g_room) {
|
||||
room_destroy(g_room);
|
||||
}
|
||||
printf("\nShutting down...\n");
|
||||
exit(0);
|
||||
}
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
int port = DEFAULT_PORT;
|
||||
|
||||
/* Parse command line arguments */
|
||||
for (int i = 1; i < argc; i++) {
|
||||
if (strcmp(argv[i], "-p") == 0 && i + 1 < argc) {
|
||||
port = atoi(argv[i + 1]);
|
||||
i++;
|
||||
} else if (strcmp(argv[i], "-h") == 0 || strcmp(argv[i], "--help") == 0) {
|
||||
printf("TNT - Terminal Network Talk\n");
|
||||
printf("Usage: %s [options]\n", argv[0]);
|
||||
printf("Options:\n");
|
||||
printf(" -p PORT Listen on PORT (default: %d)\n", DEFAULT_PORT);
|
||||
printf(" -h Show this help\n");
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* Check environment variable for port */
|
||||
const char *port_env = getenv("PORT");
|
||||
if (port_env) {
|
||||
port = atoi(port_env);
|
||||
}
|
||||
|
||||
/* Setup signal handlers */
|
||||
signal(SIGINT, signal_handler);
|
||||
signal(SIGTERM, signal_handler);
|
||||
signal(SIGPIPE, SIG_IGN);
|
||||
|
||||
/* Initialize subsystems */
|
||||
message_init();
|
||||
|
||||
/* Create chat room */
|
||||
g_room = room_create();
|
||||
if (!g_room) {
|
||||
fprintf(stderr, "Failed to create chat room\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* Initialize server */
|
||||
g_listen_fd = ssh_server_init(port);
|
||||
if (g_listen_fd < 0) {
|
||||
fprintf(stderr, "Failed to initialize server\n");
|
||||
room_destroy(g_room);
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* Start server (blocking) */
|
||||
int ret = ssh_server_start(g_listen_fd);
|
||||
|
||||
/* Cleanup */
|
||||
close(g_listen_fd);
|
||||
room_destroy(g_room);
|
||||
|
||||
return ret;
|
||||
}
|
||||
109
src/message.c
Normal file
109
src/message.c
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
#include "message.h"
|
||||
#include "utf8.h"
|
||||
#include <unistd.h>
|
||||
#include <fcntl.h>
|
||||
|
||||
/* Initialize message subsystem */
|
||||
void message_init(void) {
|
||||
/* Nothing to initialize for now */
|
||||
}
|
||||
|
||||
/* Load messages from log file */
|
||||
int message_load(message_t **messages, int max_messages) {
|
||||
/* Always allocate the message array */
|
||||
message_t *msg_array = calloc(max_messages, sizeof(message_t));
|
||||
if (!msg_array) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
FILE *fp = fopen(LOG_FILE, "r");
|
||||
if (!fp) {
|
||||
/* File doesn't exist yet, no messages */
|
||||
*messages = msg_array;
|
||||
return 0;
|
||||
}
|
||||
|
||||
char line[2048];
|
||||
int count = 0;
|
||||
int total = 0;
|
||||
|
||||
/* Read all messages first to get total count */
|
||||
message_t *temp_array = calloc(max_messages * 10, sizeof(message_t));
|
||||
if (!temp_array) {
|
||||
*messages = msg_array; /* Set the allocated array even on error */
|
||||
fclose(fp);
|
||||
return 0;
|
||||
}
|
||||
|
||||
while (fgets(line, sizeof(line), fp) && total < max_messages * 10) {
|
||||
/* Format: RFC3339_timestamp|username|content */
|
||||
char *timestamp_str = strtok(line, "|");
|
||||
char *username = strtok(NULL, "|");
|
||||
char *content = strtok(NULL, "\n");
|
||||
|
||||
if (!timestamp_str || !username || !content) {
|
||||
continue;
|
||||
}
|
||||
|
||||
/* Parse ISO 8601 timestamp */
|
||||
struct tm tm = {0};
|
||||
char *result = strptime(timestamp_str, "%Y-%m-%dT%H:%M:%S", &tm);
|
||||
if (!result) {
|
||||
continue;
|
||||
}
|
||||
|
||||
temp_array[total].timestamp = mktime(&tm);
|
||||
strncpy(temp_array[total].username, username, MAX_USERNAME_LEN - 1);
|
||||
strncpy(temp_array[total].content, content, MAX_MESSAGE_LEN - 1);
|
||||
total++;
|
||||
}
|
||||
|
||||
fclose(fp);
|
||||
|
||||
/* Keep only the last max_messages */
|
||||
int start = (total > max_messages) ? (total - max_messages) : 0;
|
||||
count = total - start;
|
||||
|
||||
for (int i = 0; i < count; i++) {
|
||||
msg_array[i] = temp_array[start + i];
|
||||
}
|
||||
|
||||
free(temp_array);
|
||||
*messages = msg_array;
|
||||
return count;
|
||||
}
|
||||
|
||||
/* Save a message to log file */
|
||||
int message_save(const message_t *msg) {
|
||||
FILE *fp = fopen(LOG_FILE, "a");
|
||||
if (!fp) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* Format timestamp as RFC3339 */
|
||||
char timestamp[64];
|
||||
struct tm tm_info;
|
||||
gmtime_r(&msg->timestamp, &tm_info);
|
||||
strftime(timestamp, sizeof(timestamp), "%Y-%m-%dT%H:%M:%SZ", &tm_info);
|
||||
|
||||
/* Write to file: timestamp|username|content */
|
||||
fprintf(fp, "%s|%s|%s\n", timestamp, msg->username, msg->content);
|
||||
|
||||
fclose(fp);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Format a message for display */
|
||||
void message_format(const message_t *msg, char *buffer, size_t buf_size, int width) {
|
||||
struct tm tm_info;
|
||||
localtime_r(&msg->timestamp, &tm_info);
|
||||
char time_str[64];
|
||||
strftime(time_str, sizeof(time_str), "%Y-%m-%d %H:%M %Z", &tm_info);
|
||||
|
||||
snprintf(buffer, buf_size, "[%s] %s: %s", time_str, msg->username, msg->content);
|
||||
|
||||
/* Truncate if too long */
|
||||
if (utf8_string_width(buffer) > width) {
|
||||
utf8_truncate(buffer, width);
|
||||
}
|
||||
}
|
||||
458
src/ssh_server.c
Normal file
458
src/ssh_server.c
Normal file
|
|
@ -0,0 +1,458 @@
|
|||
#include "ssh_server.h"
|
||||
#include "tui.h"
|
||||
#include "utf8.h"
|
||||
#include <sys/socket.h>
|
||||
#include <netinet/in.h>
|
||||
#include <arpa/inet.h>
|
||||
#include <unistd.h>
|
||||
#include <fcntl.h>
|
||||
#include <errno.h>
|
||||
#include <signal.h>
|
||||
#include <stdarg.h>
|
||||
|
||||
/* Send data to client */
|
||||
int client_send(client_t *client, const char *data, size_t len) {
|
||||
if (!client || !client->connected) return -1;
|
||||
ssize_t sent = write(client->fd, data, len);
|
||||
return (sent < 0) ? -1 : 0;
|
||||
}
|
||||
|
||||
/* Send formatted string to client */
|
||||
int client_printf(client_t *client, const char *fmt, ...) {
|
||||
char buffer[2048];
|
||||
va_list args;
|
||||
va_start(args, fmt);
|
||||
int len = vsnprintf(buffer, sizeof(buffer), fmt, args);
|
||||
va_end(args);
|
||||
return client_send(client, buffer, len);
|
||||
}
|
||||
|
||||
/* Read username from client */
|
||||
static int read_username(client_t *client) {
|
||||
char username[MAX_USERNAME_LEN] = {0};
|
||||
int pos = 0;
|
||||
unsigned char buf[4];
|
||||
|
||||
tui_clear_screen(client->fd);
|
||||
client_printf(client, "请输入用户名: ");
|
||||
|
||||
while (1) {
|
||||
ssize_t n = read(client->fd, buf, 1);
|
||||
if (n <= 0) return -1;
|
||||
|
||||
unsigned char b = buf[0];
|
||||
|
||||
if (b == '\r' || b == '\n') {
|
||||
break;
|
||||
} else if (b == 127 || b == 8) { /* Backspace */
|
||||
if (pos > 0) {
|
||||
utf8_remove_last_char(username);
|
||||
pos = strlen(username);
|
||||
client_printf(client, "\b \b");
|
||||
}
|
||||
} else if (b < 32) {
|
||||
/* Ignore control characters */
|
||||
} else if (b < 128) {
|
||||
/* ASCII */
|
||||
if (pos < MAX_USERNAME_LEN - 1) {
|
||||
username[pos++] = b;
|
||||
username[pos] = '\0';
|
||||
write(client->fd, &b, 1);
|
||||
}
|
||||
} else {
|
||||
/* UTF-8 multi-byte */
|
||||
int len = utf8_byte_length(b);
|
||||
buf[0] = b;
|
||||
if (len > 1) {
|
||||
read(client->fd, &buf[1], len - 1);
|
||||
}
|
||||
if (pos + len < MAX_USERNAME_LEN - 1) {
|
||||
memcpy(username + pos, buf, len);
|
||||
pos += len;
|
||||
username[pos] = '\0';
|
||||
write(client->fd, buf, len);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
client_printf(client, "\n");
|
||||
|
||||
if (username[0] == '\0') {
|
||||
strcpy(client->username, "anonymous");
|
||||
} else {
|
||||
strncpy(client->username, username, MAX_USERNAME_LEN - 1);
|
||||
/* Truncate to 20 characters */
|
||||
if (utf8_strlen(client->username) > 20) {
|
||||
utf8_truncate(client->username, 20);
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Execute a command */
|
||||
static void execute_command(client_t *client) {
|
||||
char *cmd = client->command_input;
|
||||
char output[2048] = {0};
|
||||
int pos = 0;
|
||||
|
||||
/* Trim whitespace */
|
||||
while (*cmd == ' ') cmd++;
|
||||
char *end = cmd + strlen(cmd) - 1;
|
||||
while (end > cmd && *end == ' ') {
|
||||
*end = '\0';
|
||||
end--;
|
||||
}
|
||||
|
||||
if (strcmp(cmd, "list") == 0 || strcmp(cmd, "users") == 0 ||
|
||||
strcmp(cmd, "who") == 0) {
|
||||
pos += snprintf(output + pos, sizeof(output) - pos,
|
||||
"========================================\n"
|
||||
" Online Users / 在线用户\n"
|
||||
"========================================\n");
|
||||
|
||||
pthread_rwlock_rdlock(&g_room->lock);
|
||||
pos += snprintf(output + pos, sizeof(output) - pos,
|
||||
"Total / 总数: %d\n"
|
||||
"----------------------------------------\n",
|
||||
g_room->client_count);
|
||||
|
||||
for (int i = 0; i < g_room->client_count; i++) {
|
||||
char marker = (g_room->clients[i] == client) ? '*' : ' ';
|
||||
pos += snprintf(output + pos, sizeof(output) - pos,
|
||||
"%c %d. %s\n", marker, i + 1,
|
||||
g_room->clients[i]->username);
|
||||
}
|
||||
|
||||
pthread_rwlock_unlock(&g_room->lock);
|
||||
|
||||
pos += snprintf(output + pos, sizeof(output) - pos,
|
||||
"========================================\n"
|
||||
"* = you / 你\n");
|
||||
|
||||
} else if (strcmp(cmd, "help") == 0 || strcmp(cmd, "commands") == 0) {
|
||||
pos += snprintf(output + pos, sizeof(output) - pos,
|
||||
"========================================\n"
|
||||
" Available Commands\n"
|
||||
"========================================\n"
|
||||
"list, users, who - Show online users\n"
|
||||
"help, commands - Show this help\n"
|
||||
"clear, cls - Clear command output\n"
|
||||
"========================================\n");
|
||||
|
||||
} else if (strcmp(cmd, "clear") == 0 || strcmp(cmd, "cls") == 0) {
|
||||
pos += snprintf(output + pos, sizeof(output) - pos,
|
||||
"Command output cleared\n");
|
||||
|
||||
} else if (cmd[0] == '\0') {
|
||||
/* Empty command */
|
||||
client->mode = MODE_NORMAL;
|
||||
client->command_input[0] = '\0';
|
||||
tui_render_screen(client);
|
||||
return;
|
||||
|
||||
} else {
|
||||
pos += snprintf(output + pos, sizeof(output) - pos,
|
||||
"Unknown command: %s\n"
|
||||
"Type 'help' for available commands\n", cmd);
|
||||
}
|
||||
|
||||
pos += snprintf(output + pos, sizeof(output) - pos,
|
||||
"\nPress any key to continue...");
|
||||
|
||||
strncpy(client->command_output, output, sizeof(client->command_output) - 1);
|
||||
client->command_input[0] = '\0';
|
||||
tui_render_command_output(client);
|
||||
}
|
||||
|
||||
/* Handle client key press */
|
||||
static void handle_key(client_t *client, unsigned char key, char *input) {
|
||||
/* Handle help screen */
|
||||
if (client->show_help) {
|
||||
if (key == 'q' || key == 27) {
|
||||
client->show_help = false;
|
||||
tui_render_screen(client);
|
||||
} else if (key == 'e' || key == 'E') {
|
||||
client->help_lang = LANG_EN;
|
||||
client->help_scroll_pos = 0;
|
||||
tui_render_help(client);
|
||||
} else if (key == 'z' || key == 'Z') {
|
||||
client->help_lang = LANG_ZH;
|
||||
client->help_scroll_pos = 0;
|
||||
tui_render_help(client);
|
||||
} else if (key == 'j') {
|
||||
client->help_scroll_pos++;
|
||||
tui_render_help(client);
|
||||
} else if (key == 'k' && client->help_scroll_pos > 0) {
|
||||
client->help_scroll_pos--;
|
||||
tui_render_help(client);
|
||||
} else if (key == 'g') {
|
||||
client->help_scroll_pos = 0;
|
||||
tui_render_help(client);
|
||||
} else if (key == 'G') {
|
||||
client->help_scroll_pos = 999; /* Large number */
|
||||
tui_render_help(client);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
/* Handle command output display */
|
||||
if (client->command_output[0] != '\0') {
|
||||
client->command_output[0] = '\0';
|
||||
client->mode = MODE_NORMAL;
|
||||
tui_render_screen(client);
|
||||
return;
|
||||
}
|
||||
|
||||
/* Mode-specific handling */
|
||||
switch (client->mode) {
|
||||
case MODE_INSERT:
|
||||
if (key == 27) { /* ESC */
|
||||
client->mode = MODE_NORMAL;
|
||||
client->scroll_pos = 0;
|
||||
tui_render_screen(client);
|
||||
} else if (key == '\r') { /* Enter */
|
||||
if (input[0] != '\0') {
|
||||
message_t msg = {
|
||||
.timestamp = time(NULL),
|
||||
};
|
||||
strncpy(msg.username, client->username, MAX_USERNAME_LEN - 1);
|
||||
strncpy(msg.content, input, MAX_MESSAGE_LEN - 1);
|
||||
room_broadcast(g_room, &msg);
|
||||
message_save(&msg);
|
||||
input[0] = '\0';
|
||||
}
|
||||
tui_render_screen(client);
|
||||
} else if (key == 127 || key == 8) { /* Backspace */
|
||||
if (input[0] != '\0') {
|
||||
utf8_remove_last_char(input);
|
||||
tui_render_input(client, input);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case MODE_NORMAL:
|
||||
if (key == 'i') {
|
||||
client->mode = MODE_INSERT;
|
||||
tui_render_screen(client);
|
||||
} else if (key == ':') {
|
||||
client->mode = MODE_COMMAND;
|
||||
client->command_input[0] = '\0';
|
||||
tui_render_screen(client);
|
||||
} else if (key == 'j') {
|
||||
int max_scroll = room_get_message_count(g_room) - 1;
|
||||
if (client->scroll_pos < max_scroll) {
|
||||
client->scroll_pos++;
|
||||
tui_render_screen(client);
|
||||
}
|
||||
} else if (key == 'k' && client->scroll_pos > 0) {
|
||||
client->scroll_pos--;
|
||||
tui_render_screen(client);
|
||||
} else if (key == 'g') {
|
||||
client->scroll_pos = 0;
|
||||
tui_render_screen(client);
|
||||
} else if (key == 'G') {
|
||||
client->scroll_pos = room_get_message_count(g_room) - 1;
|
||||
if (client->scroll_pos < 0) client->scroll_pos = 0;
|
||||
tui_render_screen(client);
|
||||
} else if (key == '?') {
|
||||
client->show_help = true;
|
||||
client->help_scroll_pos = 0;
|
||||
tui_render_help(client);
|
||||
}
|
||||
break;
|
||||
|
||||
case MODE_COMMAND:
|
||||
if (key == 27) { /* ESC */
|
||||
client->mode = MODE_NORMAL;
|
||||
client->command_input[0] = '\0';
|
||||
tui_render_screen(client);
|
||||
} else if (key == '\r' || key == '\n') {
|
||||
execute_command(client);
|
||||
} else if (key == 127 || key == 8) { /* Backspace */
|
||||
if (client->command_input[0] != '\0') {
|
||||
utf8_remove_last_char(client->command_input);
|
||||
tui_render_screen(client);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/* Handle client session */
|
||||
void* client_handle_session(void *arg) {
|
||||
client_t *client = (client_t*)arg;
|
||||
char input[MAX_MESSAGE_LEN] = {0};
|
||||
unsigned char buf[4];
|
||||
|
||||
/* Get terminal size (assume 80x24 for telnet) */
|
||||
client->width = 80;
|
||||
client->height = 24;
|
||||
client->mode = MODE_INSERT;
|
||||
client->help_lang = LANG_ZH;
|
||||
client->connected = true;
|
||||
|
||||
/* Read username */
|
||||
if (read_username(client) < 0) {
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
/* Add to room */
|
||||
if (room_add_client(g_room, client) < 0) {
|
||||
client_printf(client, "Room is full\n");
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
/* Broadcast join message */
|
||||
message_t join_msg = {
|
||||
.timestamp = time(NULL),
|
||||
};
|
||||
strcpy(join_msg.username, "系统");
|
||||
snprintf(join_msg.content, MAX_MESSAGE_LEN, "%s 加入了聊天室", client->username);
|
||||
room_broadcast(g_room, &join_msg);
|
||||
|
||||
/* Render initial screen */
|
||||
tui_render_screen(client);
|
||||
|
||||
/* Main input loop */
|
||||
while (client->connected) {
|
||||
ssize_t n = read(client->fd, buf, 1);
|
||||
if (n <= 0) break;
|
||||
|
||||
unsigned char b = buf[0];
|
||||
|
||||
/* Ctrl+C */
|
||||
if (b == 3) break;
|
||||
|
||||
/* Handle special keys */
|
||||
handle_key(client, b, input);
|
||||
|
||||
/* Add character to input (INSERT mode only) */
|
||||
if (client->mode == MODE_INSERT && !client->show_help &&
|
||||
client->command_output[0] == '\0') {
|
||||
if (b >= 32 && b < 127) { /* ASCII printable */
|
||||
int len = strlen(input);
|
||||
if (len < MAX_MESSAGE_LEN - 1) {
|
||||
input[len] = b;
|
||||
input[len + 1] = '\0';
|
||||
tui_render_input(client, input);
|
||||
}
|
||||
} else if (b >= 128) { /* UTF-8 multi-byte */
|
||||
int char_len = utf8_byte_length(b);
|
||||
buf[0] = b;
|
||||
if (char_len > 1) {
|
||||
read(client->fd, &buf[1], char_len - 1);
|
||||
}
|
||||
int len = strlen(input);
|
||||
if (len + char_len < MAX_MESSAGE_LEN - 1) {
|
||||
memcpy(input + len, buf, char_len);
|
||||
input[len + char_len] = '\0';
|
||||
tui_render_input(client, input);
|
||||
}
|
||||
}
|
||||
} else if (client->mode == MODE_COMMAND && !client->show_help &&
|
||||
client->command_output[0] == '\0') {
|
||||
if (b >= 32 && b < 127) { /* ASCII printable */
|
||||
int len = strlen(client->command_input);
|
||||
if (len < sizeof(client->command_input) - 1) {
|
||||
client->command_input[len] = b;
|
||||
client->command_input[len + 1] = '\0';
|
||||
tui_render_screen(client);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cleanup:
|
||||
/* Broadcast leave message */
|
||||
{
|
||||
message_t leave_msg = {
|
||||
.timestamp = time(NULL),
|
||||
};
|
||||
strcpy(leave_msg.username, "系统");
|
||||
snprintf(leave_msg.content, MAX_MESSAGE_LEN, "%s 离开了聊天室", client->username);
|
||||
|
||||
client->connected = false;
|
||||
room_remove_client(g_room, client);
|
||||
room_broadcast(g_room, &leave_msg);
|
||||
}
|
||||
|
||||
close(client->fd);
|
||||
free(client);
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* Initialize server socket */
|
||||
int ssh_server_init(int port) {
|
||||
int listen_fd = socket(AF_INET, SOCK_STREAM, 0);
|
||||
if (listen_fd < 0) {
|
||||
perror("socket");
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* Set socket options */
|
||||
int opt = 1;
|
||||
setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
|
||||
|
||||
struct sockaddr_in addr = {0};
|
||||
addr.sin_family = AF_INET;
|
||||
addr.sin_addr.s_addr = INADDR_ANY;
|
||||
addr.sin_port = htons(port);
|
||||
|
||||
if (bind(listen_fd, (struct sockaddr*)&addr, sizeof(addr)) < 0) {
|
||||
perror("bind");
|
||||
close(listen_fd);
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (listen(listen_fd, 10) < 0) {
|
||||
perror("listen");
|
||||
close(listen_fd);
|
||||
return -1;
|
||||
}
|
||||
|
||||
return listen_fd;
|
||||
}
|
||||
|
||||
/* Start server (blocking) */
|
||||
int ssh_server_start(int listen_fd) {
|
||||
printf("TNT chat server listening on port %d\n", DEFAULT_PORT);
|
||||
printf("Connect with: telnet localhost %d\n", DEFAULT_PORT);
|
||||
|
||||
while (1) {
|
||||
struct sockaddr_in client_addr;
|
||||
socklen_t client_len = sizeof(client_addr);
|
||||
|
||||
int client_fd = accept(listen_fd, (struct sockaddr*)&client_addr, &client_len);
|
||||
if (client_fd < 0) {
|
||||
if (errno == EINTR) continue;
|
||||
perror("accept");
|
||||
continue;
|
||||
}
|
||||
|
||||
/* Create client structure */
|
||||
client_t *client = calloc(1, sizeof(client_t));
|
||||
if (!client) {
|
||||
close(client_fd);
|
||||
continue;
|
||||
}
|
||||
|
||||
client->fd = client_fd;
|
||||
|
||||
/* Create thread for client */
|
||||
pthread_t thread;
|
||||
if (pthread_create(&thread, NULL, client_handle_session, client) != 0) {
|
||||
close(client_fd);
|
||||
free(client);
|
||||
continue;
|
||||
}
|
||||
|
||||
pthread_detach(thread);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
340
src/tui.c
Normal file
340
src/tui.c
Normal file
|
|
@ -0,0 +1,340 @@
|
|||
#include "tui.h"
|
||||
#include "ssh_server.h"
|
||||
#include "chat_room.h"
|
||||
#include "utf8.h"
|
||||
#include <stdarg.h>
|
||||
#include <unistd.h>
|
||||
|
||||
/* Clear the screen */
|
||||
void tui_clear_screen(int fd) {
|
||||
const char *clear = ANSI_CLEAR ANSI_HOME;
|
||||
write(fd, clear, strlen(clear));
|
||||
}
|
||||
|
||||
/* Render the main screen */
|
||||
void tui_render_screen(client_t *client) {
|
||||
if (!client || !client->connected) return;
|
||||
|
||||
char buffer[8192];
|
||||
int pos = 0;
|
||||
|
||||
pthread_rwlock_rdlock(&g_room->lock);
|
||||
int online = g_room->client_count;
|
||||
int msg_count = g_room->message_count;
|
||||
pthread_rwlock_unlock(&g_room->lock);
|
||||
|
||||
/* Clear and move to top */
|
||||
pos += snprintf(buffer + pos, sizeof(buffer) - pos, ANSI_CLEAR ANSI_HOME);
|
||||
|
||||
/* Title bar */
|
||||
const char *mode_str = (client->mode == MODE_INSERT) ? "INSERT" :
|
||||
(client->mode == MODE_NORMAL) ? "NORMAL" :
|
||||
(client->mode == MODE_COMMAND) ? "COMMAND" : "HELP";
|
||||
|
||||
char title[256];
|
||||
snprintf(title, sizeof(title),
|
||||
" 聊天室 | 在线: %d | 模式: %s | Ctrl+C 退出 | ? 帮助 ",
|
||||
online, mode_str);
|
||||
|
||||
int title_width = utf8_string_width(title);
|
||||
int padding = client->width - title_width;
|
||||
if (padding < 0) padding = 0;
|
||||
|
||||
pos += snprintf(buffer + pos, sizeof(buffer) - pos, ANSI_REVERSE "%s", title);
|
||||
for (int i = 0; i < padding; i++) {
|
||||
buffer[pos++] = ' ';
|
||||
}
|
||||
pos += snprintf(buffer + pos, sizeof(buffer) - pos, ANSI_RESET "\n");
|
||||
|
||||
/* Messages area */
|
||||
int msg_height = client->height - 3;
|
||||
if (msg_height < 1) msg_height = 1;
|
||||
|
||||
/* Calculate which messages to show */
|
||||
int start = 0;
|
||||
if (client->mode == MODE_NORMAL) {
|
||||
start = client->scroll_pos;
|
||||
if (start > msg_count - msg_height) {
|
||||
start = msg_count - msg_height;
|
||||
}
|
||||
if (start < 0) start = 0;
|
||||
} else {
|
||||
/* INSERT mode: show latest */
|
||||
if (msg_count > msg_height) {
|
||||
start = msg_count - msg_height;
|
||||
}
|
||||
}
|
||||
|
||||
pthread_rwlock_rdlock(&g_room->lock);
|
||||
|
||||
int end = start + msg_height;
|
||||
if (end > msg_count) end = msg_count;
|
||||
|
||||
for (int i = start; i < end; i++) {
|
||||
char msg_line[1024];
|
||||
message_format(&g_room->messages[i], msg_line, sizeof(msg_line), client->width);
|
||||
pos += snprintf(buffer + pos, sizeof(buffer) - pos, "%s\n", msg_line);
|
||||
}
|
||||
|
||||
pthread_rwlock_unlock(&g_room->lock);
|
||||
|
||||
/* Fill empty lines */
|
||||
for (int i = end - start; i < msg_height; i++) {
|
||||
buffer[pos++] = '\n';
|
||||
}
|
||||
|
||||
/* Separator - use box drawing character */
|
||||
for (int i = 0; i < client->width && pos < (int)sizeof(buffer) - 10; i++) {
|
||||
const char *line_char = "─"; /* U+2500 box drawing */
|
||||
int len = strlen(line_char);
|
||||
memcpy(buffer + pos, line_char, len);
|
||||
pos += len;
|
||||
}
|
||||
buffer[pos++] = '\n';
|
||||
|
||||
/* Status/Input line */
|
||||
if (client->mode == MODE_INSERT) {
|
||||
pos += snprintf(buffer + pos, sizeof(buffer) - pos, "> ");
|
||||
} else if (client->mode == MODE_NORMAL) {
|
||||
int total = msg_count;
|
||||
int scroll_pos = client->scroll_pos + 1;
|
||||
if (total == 0) scroll_pos = 0;
|
||||
pos += snprintf(buffer + pos, sizeof(buffer) - pos,
|
||||
"-- NORMAL -- (%d/%d)", scroll_pos, total);
|
||||
} else if (client->mode == MODE_COMMAND) {
|
||||
pos += snprintf(buffer + pos, sizeof(buffer) - pos,
|
||||
":%s", client->command_input);
|
||||
}
|
||||
|
||||
client_send(client, buffer, pos);
|
||||
}
|
||||
|
||||
/* Render the input line */
|
||||
void tui_render_input(client_t *client, const char *input) {
|
||||
if (!client || !client->connected) return;
|
||||
|
||||
char buffer[2048];
|
||||
int input_width = utf8_string_width(input);
|
||||
|
||||
/* Truncate from start if too long */
|
||||
char display[1024];
|
||||
strncpy(display, input, sizeof(display) - 1);
|
||||
display[sizeof(display) - 1] = '\0';
|
||||
|
||||
if (input_width > client->width - 3) {
|
||||
/* Find where to start displaying */
|
||||
int excess = input_width - (client->width - 3);
|
||||
int skip_width = 0;
|
||||
const char *p = input;
|
||||
int bytes_read;
|
||||
|
||||
while (*p && skip_width < excess) {
|
||||
uint32_t cp = utf8_decode(p, &bytes_read);
|
||||
skip_width += utf8_char_width(cp);
|
||||
p += bytes_read;
|
||||
}
|
||||
|
||||
strncpy(display, p, sizeof(display) - 1);
|
||||
}
|
||||
|
||||
/* Move to input line and clear it, then write input */
|
||||
snprintf(buffer, sizeof(buffer), "\033[%d;1H" ANSI_CLEAR_LINE "> %s",
|
||||
client->height, display);
|
||||
|
||||
client_send(client, buffer, strlen(buffer));
|
||||
}
|
||||
|
||||
/* Render the command output screen */
|
||||
void tui_render_command_output(client_t *client) {
|
||||
if (!client || !client->connected) return;
|
||||
|
||||
char buffer[4096];
|
||||
int pos = 0;
|
||||
|
||||
/* Clear screen */
|
||||
pos += snprintf(buffer + pos, sizeof(buffer) - pos, ANSI_CLEAR ANSI_HOME);
|
||||
|
||||
/* Title */
|
||||
const char *title = " COMMAND OUTPUT ";
|
||||
int title_width = strlen(title);
|
||||
int padding = client->width - title_width;
|
||||
if (padding < 0) padding = 0;
|
||||
|
||||
pos += snprintf(buffer + pos, sizeof(buffer) - pos, ANSI_REVERSE "%s", title);
|
||||
for (int i = 0; i < padding; i++) {
|
||||
buffer[pos++] = ' ';
|
||||
}
|
||||
pos += snprintf(buffer + pos, sizeof(buffer) - pos, ANSI_RESET "\n");
|
||||
|
||||
/* Command output */
|
||||
char *line = strtok(client->command_output, "\n");
|
||||
int line_count = 0;
|
||||
int max_lines = client->height - 2;
|
||||
|
||||
while (line && line_count < max_lines) {
|
||||
char truncated[1024];
|
||||
strncpy(truncated, line, sizeof(truncated) - 1);
|
||||
truncated[sizeof(truncated) - 1] = '\0';
|
||||
|
||||
if (utf8_string_width(truncated) > client->width) {
|
||||
utf8_truncate(truncated, client->width);
|
||||
}
|
||||
|
||||
pos += snprintf(buffer + pos, sizeof(buffer) - pos, "%s\n", truncated);
|
||||
line = strtok(NULL, "\n");
|
||||
line_count++;
|
||||
}
|
||||
|
||||
client_send(client, buffer, pos);
|
||||
}
|
||||
|
||||
/* Get help text based on language */
|
||||
const char* tui_get_help_text(help_lang_t lang) {
|
||||
if (lang == LANG_EN) {
|
||||
return "TERMINAL CHAT ROOM - HELP\n"
|
||||
"\n"
|
||||
"OPERATING MODES:\n"
|
||||
" INSERT - Type and send messages (default)\n"
|
||||
" NORMAL - Browse message history\n"
|
||||
" COMMAND - Execute commands\n"
|
||||
"\n"
|
||||
"INSERT MODE KEYS:\n"
|
||||
" ESC - Enter NORMAL mode\n"
|
||||
" Enter - Send message\n"
|
||||
" Backspace - Delete character\n"
|
||||
" Ctrl+C - Exit chat\n"
|
||||
"\n"
|
||||
"NORMAL MODE KEYS:\n"
|
||||
" i - Return to INSERT mode\n"
|
||||
" : - Enter COMMAND mode\n"
|
||||
" j - Scroll down (older messages)\n"
|
||||
" k - Scroll up (newer messages)\n"
|
||||
" g - Jump to top (oldest)\n"
|
||||
" G - Jump to bottom (newest)\n"
|
||||
" ? - Show this help\n"
|
||||
" Ctrl+C - Exit chat\n"
|
||||
"\n"
|
||||
"COMMAND MODE KEYS:\n"
|
||||
" Enter - Execute command\n"
|
||||
" ESC - Cancel, return to NORMAL\n"
|
||||
" Backspace - Delete character\n"
|
||||
"\n"
|
||||
"AVAILABLE COMMANDS:\n"
|
||||
" list, users, who - Show online users\n"
|
||||
" help, commands - Show available commands\n"
|
||||
" clear, cls - Clear command output\n"
|
||||
"\n"
|
||||
"HELP SCREEN KEYS:\n"
|
||||
" q, ESC - Close help\n"
|
||||
" j - Scroll down\n"
|
||||
" k - Scroll up\n"
|
||||
" g - Jump to top\n"
|
||||
" G - Jump to bottom\n"
|
||||
" e, E - Switch to English\n"
|
||||
" z, Z - Switch to Chinese\n";
|
||||
} else {
|
||||
return "终端聊天室 - 帮助\n"
|
||||
"\n"
|
||||
"操作模式:\n"
|
||||
" INSERT - 输入和发送消息(默认)\n"
|
||||
" NORMAL - 浏览消息历史\n"
|
||||
" COMMAND - 执行命令\n"
|
||||
"\n"
|
||||
"INSERT 模式按键:\n"
|
||||
" ESC - 进入 NORMAL 模式\n"
|
||||
" Enter - 发送消息\n"
|
||||
" Backspace - 删除字符\n"
|
||||
" Ctrl+C - 退出聊天\n"
|
||||
"\n"
|
||||
"NORMAL 模式按键:\n"
|
||||
" i - 返回 INSERT 模式\n"
|
||||
" : - 进入 COMMAND 模式\n"
|
||||
" j - 向下滚动(更早的消息)\n"
|
||||
" k - 向上滚动(更新的消息)\n"
|
||||
" g - 跳到顶部(最早)\n"
|
||||
" G - 跳到底部(最新)\n"
|
||||
" ? - 显示此帮助\n"
|
||||
" Ctrl+C - 退出聊天\n"
|
||||
"\n"
|
||||
"COMMAND 模式按键:\n"
|
||||
" Enter - 执行命令\n"
|
||||
" ESC - 取消,返回 NORMAL 模式\n"
|
||||
" Backspace - 删除字符\n"
|
||||
"\n"
|
||||
"可用命令:\n"
|
||||
" list, users, who - 显示在线用户\n"
|
||||
" help, commands - 显示可用命令\n"
|
||||
" clear, cls - 清空命令输出\n"
|
||||
"\n"
|
||||
"帮助界面按键:\n"
|
||||
" q, ESC - 关闭帮助\n"
|
||||
" j - 向下滚动\n"
|
||||
" k - 向上滚动\n"
|
||||
" g - 跳到顶部\n"
|
||||
" G - 跳到底部\n"
|
||||
" e, E - 切换到英文\n"
|
||||
" z, Z - 切换到中文\n";
|
||||
}
|
||||
}
|
||||
|
||||
/* Render the help screen */
|
||||
void tui_render_help(client_t *client) {
|
||||
if (!client || !client->connected) return;
|
||||
|
||||
char buffer[8192];
|
||||
int pos = 0;
|
||||
|
||||
/* Clear screen */
|
||||
pos += snprintf(buffer + pos, sizeof(buffer) - pos, ANSI_CLEAR ANSI_HOME);
|
||||
|
||||
/* Title */
|
||||
const char *title = " HELP ";
|
||||
int title_width = strlen(title);
|
||||
int padding = client->width - title_width;
|
||||
if (padding < 0) padding = 0;
|
||||
|
||||
pos += snprintf(buffer + pos, sizeof(buffer) - pos, ANSI_REVERSE "%s", title);
|
||||
for (int i = 0; i < padding; i++) {
|
||||
buffer[pos++] = ' ';
|
||||
}
|
||||
pos += snprintf(buffer + pos, sizeof(buffer) - pos, ANSI_RESET "\n");
|
||||
|
||||
/* Help content */
|
||||
const char *help_text = tui_get_help_text(client->help_lang);
|
||||
char help_copy[4096];
|
||||
strncpy(help_copy, help_text, sizeof(help_copy) - 1);
|
||||
help_copy[sizeof(help_copy) - 1] = '\0';
|
||||
|
||||
/* Split into lines and display with scrolling */
|
||||
char *lines[100];
|
||||
int line_count = 0;
|
||||
char *line = strtok(help_copy, "\n");
|
||||
while (line && line_count < 100) {
|
||||
lines[line_count++] = line;
|
||||
line = strtok(NULL, "\n");
|
||||
}
|
||||
|
||||
int content_height = client->height - 2;
|
||||
int start = client->help_scroll_pos;
|
||||
int end = start + content_height - 1;
|
||||
if (end > line_count) end = line_count;
|
||||
|
||||
for (int i = start; i < end && (i - start) < content_height - 1; i++) {
|
||||
pos += snprintf(buffer + pos, sizeof(buffer) - pos, "%s\n", lines[i]);
|
||||
}
|
||||
|
||||
/* Fill remaining lines */
|
||||
for (int i = end - start; i < content_height - 1; i++) {
|
||||
buffer[pos++] = '\n';
|
||||
}
|
||||
|
||||
/* Status line */
|
||||
int max_scroll = line_count - content_height + 1;
|
||||
if (max_scroll < 0) max_scroll = 0;
|
||||
|
||||
pos += snprintf(buffer + pos, sizeof(buffer) - pos,
|
||||
"-- HELP -- (%d/%d) j/k:scroll g/G:top/bottom e/z:lang q:close",
|
||||
client->help_scroll_pos + 1, max_scroll + 1);
|
||||
|
||||
client_send(client, buffer, pos);
|
||||
}
|
||||
137
src/utf8.c
Normal file
137
src/utf8.c
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
#include "utf8.h"
|
||||
|
||||
/* Get the number of bytes in a UTF-8 character from its first byte */
|
||||
int utf8_byte_length(unsigned char first_byte) {
|
||||
if ((first_byte & 0x80) == 0) return 1; /* 0xxxxxxx */
|
||||
if ((first_byte & 0xE0) == 0xC0) return 2; /* 110xxxxx */
|
||||
if ((first_byte & 0xF0) == 0xE0) return 3; /* 1110xxxx */
|
||||
if ((first_byte & 0xF8) == 0xF0) return 4; /* 11110xxx */
|
||||
return 1; /* Invalid UTF-8, treat as single byte */
|
||||
}
|
||||
|
||||
/* Decode a UTF-8 character and return its codepoint */
|
||||
uint32_t utf8_decode(const char *str, int *bytes_read) {
|
||||
const unsigned char *s = (const unsigned char *)str;
|
||||
uint32_t codepoint = 0;
|
||||
int len = utf8_byte_length(s[0]);
|
||||
|
||||
*bytes_read = len;
|
||||
|
||||
switch (len) {
|
||||
case 1:
|
||||
codepoint = s[0];
|
||||
break;
|
||||
case 2:
|
||||
codepoint = ((s[0] & 0x1F) << 6) | (s[1] & 0x3F);
|
||||
break;
|
||||
case 3:
|
||||
codepoint = ((s[0] & 0x0F) << 12) | ((s[1] & 0x3F) << 6) | (s[2] & 0x3F);
|
||||
break;
|
||||
case 4:
|
||||
codepoint = ((s[0] & 0x07) << 18) | ((s[1] & 0x3F) << 12) |
|
||||
((s[2] & 0x3F) << 6) | (s[3] & 0x3F);
|
||||
break;
|
||||
}
|
||||
|
||||
return codepoint;
|
||||
}
|
||||
|
||||
/* UTF-8 character width calculation for CJK and other wide characters */
|
||||
int utf8_char_width(uint32_t codepoint) {
|
||||
/* ASCII */
|
||||
if (codepoint < 0x80) return 1;
|
||||
|
||||
/* CJK Unified Ideographs */
|
||||
if ((codepoint >= 0x4E00 && codepoint <= 0x9FFF) || /* CJK Unified */
|
||||
(codepoint >= 0x3400 && codepoint <= 0x4DBF) || /* CJK Extension A */
|
||||
(codepoint >= 0x20000 && codepoint <= 0x2A6DF) || /* CJK Extension B */
|
||||
(codepoint >= 0x2A700 && codepoint <= 0x2B73F) || /* CJK Extension C */
|
||||
(codepoint >= 0x2B740 && codepoint <= 0x2B81F) || /* CJK Extension D */
|
||||
(codepoint >= 0x2B820 && codepoint <= 0x2CEAF) || /* CJK Extension E */
|
||||
(codepoint >= 0xF900 && codepoint <= 0xFAFF) || /* CJK Compatibility */
|
||||
(codepoint >= 0x2F800 && codepoint <= 0x2FA1F)) { /* CJK Compat Suppl */
|
||||
return 2;
|
||||
}
|
||||
|
||||
/* Hangul Syllables (Korean) */
|
||||
if (codepoint >= 0xAC00 && codepoint <= 0xD7AF) return 2;
|
||||
|
||||
/* Hiragana and Katakana (Japanese) */
|
||||
if ((codepoint >= 0x3040 && codepoint <= 0x309F) || /* Hiragana */
|
||||
(codepoint >= 0x30A0 && codepoint <= 0x30FF)) { /* Katakana */
|
||||
return 2;
|
||||
}
|
||||
|
||||
/* Fullwidth forms */
|
||||
if (codepoint >= 0xFF00 && codepoint <= 0xFFEF) return 2;
|
||||
|
||||
/* Default to single width */
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* Calculate display width of a UTF-8 string */
|
||||
int utf8_string_width(const char *str) {
|
||||
int width = 0;
|
||||
int bytes_read;
|
||||
const char *p = str;
|
||||
|
||||
while (*p != '\0') {
|
||||
uint32_t codepoint = utf8_decode(p, &bytes_read);
|
||||
width += utf8_char_width(codepoint);
|
||||
p += bytes_read;
|
||||
}
|
||||
|
||||
return width;
|
||||
}
|
||||
|
||||
/* Count the number of UTF-8 characters in a string */
|
||||
int utf8_strlen(const char *str) {
|
||||
int count = 0;
|
||||
int bytes_read;
|
||||
const char *p = str;
|
||||
|
||||
while (*p != '\0') {
|
||||
utf8_decode(p, &bytes_read);
|
||||
count++;
|
||||
p += bytes_read;
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
/* Truncate string to fit within max_width display characters */
|
||||
void utf8_truncate(char *str, int max_width) {
|
||||
int width = 0;
|
||||
int bytes_read;
|
||||
char *p = str;
|
||||
char *last_valid = str;
|
||||
|
||||
while (*p != '\0') {
|
||||
uint32_t codepoint = utf8_decode(p, &bytes_read);
|
||||
int char_width = utf8_char_width(codepoint);
|
||||
|
||||
if (width + char_width > max_width) {
|
||||
break;
|
||||
}
|
||||
|
||||
width += char_width;
|
||||
p += bytes_read;
|
||||
last_valid = p;
|
||||
}
|
||||
|
||||
*last_valid = '\0';
|
||||
}
|
||||
|
||||
/* Remove last UTF-8 character from string */
|
||||
void utf8_remove_last_char(char *str) {
|
||||
int len = strlen(str);
|
||||
if (len == 0) return;
|
||||
|
||||
/* Find the start of the last character by walking backwards */
|
||||
int i = len - 1;
|
||||
while (i > 0 && (str[i] & 0xC0) == 0x80) {
|
||||
i--; /* Continue byte of multi-byte sequence */
|
||||
}
|
||||
|
||||
str[i] = '\0';
|
||||
}
|
||||
Loading…
Reference in a new issue