Initial commit

This commit is contained in:
m1ngsama 2025-07-01 09:00:00 +08:00
commit 63274b92ba
20 changed files with 1753 additions and 0 deletions

8
.gitignore vendored Normal file
View file

@ -0,0 +1,8 @@
*.o
obj/
tnt
messages.log
host_key
host_key.pub
*.swp
.DS_Store

21
LICENSE Normal file
View 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
View 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
View file

@ -0,0 +1,110 @@
# TNT
**TNT's Not Tunnel** - A lightweight terminal chat server written in C
![License](https://img.shields.io/badge/license-MIT-blue.svg)
![Language](https://img.shields.io/badge/language-C-blue.svg)
## 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
View 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
View 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
View file

50
include/chat_room.h Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View file

144
src/chat_room.c Normal file
View 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
View 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
View 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
View 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
View 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
View 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';
}