a5d6041764
A Rust terminal chat client with OpenAI-compatible provider support, real-time SSE streaming, markdown rendering with syntax highlighting, and persistent chat history. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
144 lines
4.4 KiB
Markdown
144 lines
4.4 KiB
Markdown
# Meow - Rust Terminal AI Chat Client
|
|
|
|
A high-performance terminal UI (TUI) AI chat client written in Rust, supporting custom OpenAI-compatible providers (DeepSeek, Kimi, etc.).
|
|
|
|
## Overview
|
|
|
|
Meow is a pure-text terminal chat application. It features:
|
|
|
|
- OpenAI-compatible provider abstraction layer
|
|
- Real-time streaming (SSE) chat output
|
|
- Markdown rendering with syntax highlighting
|
|
- Persistent chat history in JSON Lines format
|
|
- Mouse scroll support for message history
|
|
|
|
## Tech Stack
|
|
|
|
| Component | Crate |
|
|
|-----------|-------|
|
|
| Async Runtime | `tokio` |
|
|
| HTTP Client | `reqwest` (with streaming/SSE) |
|
|
| TUI Framework | `ratatui` + `crossterm` |
|
|
| Markdown Parser | `pulldown-cmark` |
|
|
| Syntax Highlighting | `syntect` (fancy-regex backend) |
|
|
| Serialization | `serde` + `serde_json` + `toml` |
|
|
| Configuration Path | `dirs` (cross-platform home dir) |
|
|
|
|
## Architecture
|
|
|
|
### Layered Design
|
|
|
|
```
|
|
TUI Layer (ratatui widgets)
|
|
├── chat.rs (implicit in app.rs)
|
|
├── model_select.rs (popup)
|
|
├── provider_config.rs (form)
|
|
├── input.rs (multi-line text area)
|
|
└── markdown.rs (rendering pipeline)
|
|
|
|
App Layer (state machine)
|
|
└── app.rs - coordinates all modules, event loop, async stream handling
|
|
|
|
Provider Layer
|
|
├── providers/mod.rs - Provider trait
|
|
└── providers/openai.rs - OpenAI-compatible implementation
|
|
|
|
Storage Layer
|
|
├── config.rs - ~/.meow/config.toml
|
|
├── storage.rs - ~/.meow/data/sessions/*.jsonl
|
|
└── message.rs - core data types
|
|
```
|
|
|
|
### Data Flow
|
|
|
|
1. User types message in input box
|
|
2. `Enter` triggers `App::send_message()`
|
|
3. User message is saved to disk via `Storage::append_message()`
|
|
4. A tokio task spawns to call `Provider::chat_stream()`
|
|
5. SSE chunks flow back through `tokio::sync::mpsc::UnboundedChannel`
|
|
6. Main event loop polls channel via `App::poll_stream()` and redraws
|
|
7. When stream ends (`__STREAM_END__`), the full assistant message is persisted
|
|
|
|
## Project Structure
|
|
|
|
```
|
|
src/
|
|
├── main.rs # Entry point: initializes tokio, Tui, App, event loop
|
|
├── app.rs # App state machine: Chat / ModelSelect / ProviderConfig
|
|
├── config.rs # Config & ProviderConfig structs, load/save to ~/.meow/config.toml
|
|
├── message.rs # Role enum, Message, ChatSession
|
|
├── storage.rs # JSON Lines persistence for chat sessions
|
|
├── providers/
|
|
│ ├── mod.rs # Provider trait, Model struct, ProviderError
|
|
│ └── openai.rs # OpenAICompatibleProvider: /v1/models, /v1/chat/completions SSE
|
|
└── tui/
|
|
├── mod.rs # Tui struct: terminal init/restore, raw mode, panic hooks
|
|
├── input.rs # InputState: multi-line editor with cursor movement
|
|
└── markdown.rs # MarkdownRenderer: pulldown-cmark -> ratatui Lines + syntect highlighting
|
|
```
|
|
|
|
## Configuration
|
|
|
|
Config is stored at `~/.meow/config.toml` (cross-platform via `dirs::home_dir()`).
|
|
|
|
Example:
|
|
|
|
```toml
|
|
[[providers]]
|
|
name = "DeepSeek"
|
|
base_url = "https://api.deepseek.com"
|
|
api_key = "sk-..."
|
|
default_model = "deepseek-chat"
|
|
```
|
|
|
|
Chat sessions are stored as JSON Lines at `~/.meow/data/sessions/<uuid>.jsonl`.
|
|
|
|
## Keybindings
|
|
|
|
| Key | Action |
|
|
|-----|--------|
|
|
| `Enter` | Send message |
|
|
| `Shift+Enter` | Newline in input |
|
|
| `Ctrl+P` | Open model selector |
|
|
| `Ctrl+S` | Open provider config |
|
|
| `Ctrl+N` | Start new chat session |
|
|
| `Ctrl+B` | Toggle sidebar |
|
|
| `Ctrl+R` | Reset current chat |
|
|
| `Ctrl+C` | Stop streaming (while AI is typing) |
|
|
| `Ctrl+Q` | Quit |
|
|
| `Mouse Scroll` | Scroll message history |
|
|
|
|
## Provider Protocol
|
|
|
|
The `Provider` trait (`async_trait`) defines:
|
|
|
|
- `name() -> &str`
|
|
- `config() -> &ProviderConfig`
|
|
- `list_models() -> Result<Vec<Model>>`
|
|
- `chat_stream(model, messages) -> Result<BoxStream<Result<String>>>`
|
|
|
|
`OpenAICompatibleProvider` implements this for any OpenAI API-compatible endpoint.
|
|
|
|
## Markdown Support
|
|
|
|
Rendered elements:
|
|
|
|
- **Bold** / *Italic*
|
|
- `Inline code` (dark background)
|
|
- Code blocks with syntax highlighting (syntect themes)
|
|
- Bullet / numbered lists
|
|
- Blockquotes (│ prefix)
|
|
- Horizontal rules
|
|
|
|
## First-Time Setup
|
|
|
|
On first run, if no providers exist, the app automatically enters the **Add Provider** form. Fill in name, base URL, and API key to get started.
|
|
|
|
## Build
|
|
|
|
```bash
|
|
cargo build --release
|
|
```
|
|
|
|
The binary will be at `target/release/meow.exe` (Windows) or `target/release/meow` (Unix).
|