feat: Enhance z3ed networking documentation and CLI capabilities

- Updated NETWORKING.md to reflect the new versioning and provide a comprehensive overview of the z3ed networking, collaboration, and remote access features.
- Introduced detailed architecture descriptions, including communication layers and core components for collaboration.
- Added new CLI commands for hex and palette manipulation, enhancing the functionality of the z3ed CLI.
- Implemented a TODO management system within the CLI for better task tracking and execution planning.
- Improved README.md to include new CLI capabilities and enhancements related to the TODO management system.
This commit is contained in:
scawful
2025-10-05 01:15:15 -04:00
parent 40f44aa42a
commit 019c20e749
11 changed files with 1411 additions and 461 deletions

View File

@@ -1,22 +1,20 @@
# Z3ED Networking & Collaboration
# z3ed Networking, Collaboration, and Remote Access
## Overview
**Version**: 0.2.0-alpha
**Last Updated**: October 5, 2025
Z3ED provides comprehensive networking capabilities across all three components:
- **yaze app**: GUI application with real-time collaboration
- **z3ed CLI**: Command-line interface for remote operations
- **yaze-server**: WebSocket server for coordination
## 1. Overview
## Architecture
This document provides a comprehensive overview of the networking, collaboration, and remote access features within the z3ed ecosystem. These systems are designed to enable everything from real-time collaborative ROM hacking to powerful AI-driven remote automation.
### Cross-Platform Design
The architecture is composed of three main communication layers:
1. **WebSocket Protocol**: A real-time messaging layer for multi-user collaboration, managed by the `yaze-server`.
2. **gRPC Service**: A high-performance RPC layer for programmatic remote ROM manipulation, primarily used by the `z3ed` CLI and automated testing harnesses.
3. **Collaboration Service**: A high-level C++ API within the YAZE application that integrates version management with the networking protocols.
All networking code is designed to work on:
-**Windows** - Using native Win32 sockets (ws2_32)
-**macOS** - Using native BSD sockets
-**Linux** - Using native BSD sockets
## 2. Architecture
### Components
### 2.1. System Diagram
```
┌─────────────┐ WebSocket ┌──────────────┐
@@ -24,8 +22,7 @@ All networking code is designed to work on:
│ (GUI) │ │ (Node.js) │
└─────────────┘ └──────────────┘
▲ ▲
│ WebSocket │
gRPC (for GUI testing) WebSocket
│ │
└─────────────┐ │
│ │
@@ -33,194 +30,113 @@ All networking code is designed to work on:
│ z3ed CLI │◄──────────────────────┘
│ │
└─────────────┘
gRPC (for GUI testing)
┌──────▼──────┐
│ yaze app │
│ (ImGui) │
└─────────────┘
```
## WebSocket Protocol
### 2.2. Core Components
### Connection
The collaboration system is built on two key C++ components:
```cpp
#include "app/net/websocket_client.h"
1. **ROM Version Manager** (`app/net/rom_version_manager.h`)
- **Purpose**: Protects the ROM from corruption and unwanted changes.
- **Features**:
- Automatic periodic snapshots and manual checkpoints.
- "Safe points" to mark host-verified versions.
- Corruption detection and automatic recovery to the last safe point.
- Ability to roll back to any previous version.
net::WebSocketClient client;
2. **Proposal Approval Manager** (`app/net/rom_version_manager.h`)
- **Purpose**: Manages a collaborative voting system for applying changes.
- **Features**:
- Multiple approval modes: `Host-Only` (default), `Majority`, and `Unanimous`.
- Automatically creates snapshots before and after a proposal is applied.
- Handles the submission, voting, and application/rejection of proposals.
// Connect to server
auto status = client.Connect("localhost", 8765);
3. **Collaboration Panel** (`app/gui/widgets/collaboration_panel.h`)
- **Purpose**: Provides a dedicated UI within the YAZE editor for managing collaboration.
- **Features**:
- Version history timeline with one-click restore.
- ROM synchronization tracking.
- Visual snapshot gallery.
- A voting and approval interface for pending proposals.
// Set up callbacks
client.OnMessage("rom_sync", [](const nlohmann::json& payload) {
// Handle ROM sync
});
## 3. Protocols
client.OnStateChange([](net::ConnectionState state) {
// Handle state changes
});
### 3.1. WebSocket Protocol (yaze-server v2.0)
Used for real-time, multi-user collaboration.
**Connection**:
```javascript
const ws = new WebSocket('ws://localhost:8765');
```
### Message Types
**Message Types**:
#### 1. Session Management
| Type | Sender | Payload Description |
| :--- | :--- | :--- |
| `host_session` | Client | Initiates a new session with a name, username, and optional ROM hash. |
| `join_session` | Client | Joins an existing session using a 6-character code. |
| `rom_sync` | Client | Broadcasts a base64-encoded ROM diff to all participants. |
| `proposal_share` | Client | Shares a new proposal (e.g., from an AI agent) with the group. |
| `proposal_vote` | Client | Submits a vote (approve/reject) for a specific proposal. |
| `snapshot_share` | Client | Shares a snapshot (e.g., a screenshot) with the group. |
| `proposal_update` | Server | Broadcasts the new status of a proposal (`approved`, `rejected`). |
| `proposal_vote_received`| Server | Confirms a vote was received and shows the current vote tally. |
| `session_hosted` | Server | Confirms a session was created and returns its code. |
| `session_joined` | Server | Confirms a user joined and provides session state (participants, history). |
**Host Session**:
```json
{
"type": "host_session",
"payload": {
"session_name": "My ROM Hack",
"username": "host",
"rom_hash": "abc123",
"ai_enabled": true
}
### 3.2. gRPC Service (Remote ROM Manipulation)
Provides a high-performance API for programmatic access to the ROM, used by the `z3ed` CLI and test harnesses.
**Status**: ✅ Designed and Implemented. Pending final build system integration.
**Protocol Buffer (`protos/rom_service.proto`)**:
```proto
service RomService {
// Core
rpc ReadBytes(ReadBytesRequest) returns (ReadBytesResponse);
rpc WriteBytes(WriteBytesRequest) returns (WriteBytesResponse);
rpc GetRomInfo(GetRomInfoRequest) returns (GetRomInfoResponse);
// Overworld
rpc ReadOverworldMap(ReadOverworldMapRequest) returns (ReadOverworldMapResponse);
rpc WriteOverworldTile(WriteOverworldTileRequest) returns (WriteOverworldTileResponse);
// Dungeon
rpc ReadDungeonRoom(ReadDungeonRoomRequest) returns (ReadDungeonRoomResponse);
rpc WriteDungeonTile(WriteDungeonTileRequest) returns (WriteDungeonTileResponse);
// Proposals
rpc SubmitRomProposal(SubmitRomProposalRequest) returns (SubmitRomProposalResponse);
rpc GetProposalStatus(GetProposalStatusRequest) returns (GetProposalStatusResponse);
// Version Management
rpc CreateSnapshot(CreateSnapshotRequest) returns (CreateSnapshotResponse);
rpc RestoreSnapshot(RestoreSnapshotRequest) returns (RestoreSnapshotResponse);
rpc ListSnapshots(ListSnapshotsRequest) returns (ListSnapshotsResponse);
}
```
**Join Session**:
```json
{
"type": "join_session",
"payload": {
"session_code": "ABC123",
"username": "participant"
}
}
**Use Case: Write with Approval via `z3ed`**
The `z3ed` CLI can submit a change as a proposal via gRPC and wait for a host to approve it in the YAZE GUI.
```bash
# 1. CLI connects and submits a proposal via gRPC
z3ed net connect --host server.example.com --port 50051
z3ed agent run --prompt "Make dungeon 5 harder" --submit-grpc-proposal
# 2. The YAZE GUI receives the proposal and the host approves it.
# 3. The z3ed CLI polls for the status and receives the approval.
```
#### 2. Proposal System (NEW)
## 4. Client Integration
**Share Proposal**:
```json
{
"type": "proposal_share",
"payload": {
"sender": "username",
"proposal_data": {
"description": "Place tile 0x42 at (5,7)",
"type": "tile_edit",
"data": {...}
}
}
}
```
### 4.1. YAZE App Integration
**Vote on Proposal** (NEW):
```json
{
"type": "proposal_vote",
"payload": {
"proposal_id": "prop_123",
"approved": true,
"username": "voter"
}
}
```
Response:
```json
{
"type": "proposal_vote_received",
"payload": {
"proposal_id": "prop_123",
"username": "voter",
"approved": true,
"votes": {
"host": true,
"user1": true,
"user2": false
},
"timestamp": 1234567890
}
}
```
**Update Proposal Status**:
```json
{
"type": "proposal_update",
"payload": {
"proposal_id": "prop_123",
"status": "approved" // or "rejected", "applied"
}
}
```
#### 3. ROM Synchronization
**Send ROM Sync**:
```json
{
"type": "rom_sync",
"payload": {
"sender": "username",
"diff_data": "base64_encoded_diff",
"rom_hash": "new_hash"
}
}
```
#### 4. Snapshots
**Share Snapshot**:
```json
{
"type": "snapshot_share",
"payload": {
"sender": "username",
"snapshot_data": "base64_encoded_image",
"snapshot_type": "screenshot"
}
}
```
## YAZE App Integration
### Using WebSocketClient
```cpp
#include "app/net/websocket_client.h"
// Create client
auto client = std::make_unique<net::WebSocketClient>();
// Connect
if (auto status = client->Connect("localhost", 8765); !status.ok()) {
// Handle error
}
// Host a session
auto session_info = client->HostSession(
"My Hack",
"username",
rom->GetHash(),
true // AI enabled
);
// Set up proposal callback
client->OnMessage("proposal_shared", [this](const nlohmann::json& payload) {
std::string proposal_id = payload["proposal_id"];
nlohmann::json proposal_data = payload["proposal_data"];
// Add to approval manager
approval_mgr->SubmitProposal(
proposal_id,
payload["sender"],
proposal_data["description"],
proposal_data
);
});
// Vote on proposal
client->VoteOnProposal(proposal_id, true, "my_username");
```
### Using CollaborationService
The `CollaborationService` provides a high-level API that integrates the version manager, approval manager, and WebSocket client.
```cpp
#include "app/net/collaboration_service.h"
@@ -228,297 +144,67 @@ client->VoteOnProposal(proposal_id, true, "my_username");
// High-level service that integrates everything
auto collab_service = std::make_unique<net::CollaborationService>(rom);
// Initialize with version manager and approval manager
// Initialize with managers
collab_service->Initialize(config, version_mgr, approval_mgr);
// Connect and host
collab_service->Connect("localhost", 8765);
collab_service->HostSession("My Hack", "username");
// Submit local changes as proposal
collab_service->SubmitChangesAsProposal(
"Modified dungeon room 5",
"username"
);
// Auto-sync is handled automatically
// Submit local changes as a proposal to the group
collab_service->SubmitChangesAsProposal("Modified dungeon room 5", "username");
```
## Z3ED CLI Integration
### 4.2. z3ed CLI Integration
### Connection Commands
The CLI provides `net` commands for interacting with the collaboration server.
```bash
# Connect to collaboration server
# Connect to a collaboration server
z3ed net connect --host localhost --port 8765
# Join session
# Join an existing session
z3ed net join --code ABC123 --username myname
# Leave session
z3ed net leave
```
# Submit an AI-generated change as a proposal and wait for it to be approved
z3ed agent run --prompt "Make boss room more challenging" --submit-proposal --wait-approval
### Proposal Commands
```bash
# Submit proposal from z3ed
z3ed agent run --prompt "Place tile 42 at (5,7)" --submit-proposal
# Check proposal status
# Manually check a proposal's status
z3ed net proposal status --id prop_123
# Wait for approval (blocking)
z3ed net proposal wait --id prop_123 --timeout 60
```
### Example Workflow
```bash
# 1. Connect to server
z3ed net connect --host localhost
# 2. Join session
z3ed net join --code XYZ789 --username alice
# 3. Submit AI-generated proposal
z3ed agent run --prompt "Make boss room more challenging" \
--submit-proposal --wait-approval
# 4. If approved, changes are applied
# If rejected, original ROM is preserved
```
## Windows-Specific Notes
### Building on Windows
The networking library automatically links Windows socket support:
```cmake
if(WIN32)
target_link_libraries(yaze_net PUBLIC ws2_32)
endif()
```
### vcpkg Dependencies
For Windows with vcpkg:
```powershell
# Install dependencies
vcpkg install openssl:x64-windows
# CMake will automatically detect and use them
```
### Windows Firewall
You may need to allow connections:
```powershell
# Allow yaze-server
netsh advfirewall firewall add rule name="YAZE Server" dir=in action=allow protocol=TCP localport=8765
# Or through UI: Windows Defender Firewall → Allow an app
```
## Security Considerations
### Transport Security
1. **Use WSS (WebSocket Secure)** in production:
```cpp
client->Connect("wss://server.example.com", 443);
```
2. **Server configuration** with SSL:
```javascript
const https = require('https');
const fs = require('fs');
const server = https.createServer({
cert: fs.readFileSync('cert.pem'),
key: fs.readFileSync('key.pem')
});
```
### Approval Security
1. **Host-only mode** (safest):
```cpp
approval_mgr->SetApprovalMode(ApprovalMode::kHostOnly);
```
2. **Verify identities**: Use authentication tokens
3. **Rate limiting**: Server limits messages to 100/minute
### ROM Protection
1. **Always create snapshots** before applying proposals:
```cpp
config.create_snapshot_before_sync = true;
```
2. **Mark safe points** after verification:
```cpp
version_mgr->MarkAsSafePoint(snapshot_id);
```
3. **Auto-rollback** on errors:
```cpp
if (error) {
version_mgr->RestoreSnapshot(snapshot_before);
}
```
## Platform-Specific Implementation
### httplib WebSocket Support
The implementation uses `cpp-httplib` for cross-platform support:
- **Windows**: Uses Winsock2 (ws2_32.dll)
- **macOS/Linux**: Uses BSD sockets
- **SSL/TLS**: Optional OpenSSL support
### Threading
All platforms use C++11 threads:
```cpp
#include <thread>
#include <mutex>
std::thread receive_thread([this]() {
// Platform-independent receive loop
});
```
## Error Handling
### Connection Errors
```cpp
auto status = client->Connect(host, port);
if (!status.ok()) {
if (absl::IsUnavailable(status)) {
// Server not reachable
} else if (absl::IsDeadlineExceeded(status)) {
// Connection timeout
}
}
```
### Network State
```cpp
client->OnStateChange([](ConnectionState state) {
switch (state) {
case ConnectionState::kConnected:
// Ready to use
break;
case ConnectionState::kDisconnected:
// Clean shutdown
break;
case ConnectionState::kReconnecting:
// Attempting reconnect
break;
case ConnectionState::kError:
// Fatal error
break;
}
});
```
## Performance
### Compression
Large messages are compressed:
```cpp
// ROM diffs are compressed before sending
std::string compressed = CompressDiff(diff_data);
client->SendRomSync(compressed, hash, sender);
```
### Batching
Small changes are batched:
```cpp
config.sync_interval_ms = 5000; // Batch changes over 5 seconds
```
### Rate Limiting
Server enforces:
- 100 messages per minute per client
- 5MB max ROM diff size
- 10MB max snapshot size
## Testing
### Unit Tests
```cpp
TEST(WebSocketClientTest, ConnectAndDisconnect) {
net::WebSocketClient client;
ASSERT_TRUE(client.Connect("localhost", 8765).ok());
EXPECT_TRUE(client.IsConnected());
client.Disconnect();
EXPECT_FALSE(client.IsConnected());
}
```
### Integration Tests
```bash
# Start server
cd yaze-server
npm start
# Run tests
cd yaze
cmake --build build --target yaze_net_tests
./build/bin/yaze_net_tests
```
## Troubleshooting
### "Failed to connect"
- Check server is running: `ps aux | grep node`
- Check port is available: `netstat -an | grep 8765`
- Check firewall settings
### "Connection timeout"
- Increase timeout: `client->SetTimeout(10);`
- Check network connectivity
- Verify server address
### "SSL handshake failed"
- Verify OpenSSL is installed
- Check certificate validity
- Use WSS URL: `wss://` not `ws://`
### Windows-specific: "ws2_32.dll not found"
- Reinstall Windows SDK
- Check PATH environment variable
- Use vcpkg for dependencies
## Future Enhancements
- [ ] WebRTC for peer-to-peer connections
- [ ] Binary protocol for faster ROM syncs
- [ ] Automatic reconnection with exponential backoff
- [ ] Connection pooling for multiple sessions
- [ ] NAT traversal for home networks
- [ ] End-to-end encryption for proposals
## See Also
- [Collaboration Guide](COLLABORATION.md) - Version management and approval
- [Z3ED README](README.md) - Main documentation
- [yaze-server README](../../../yaze-server/README.md) - Server setup
## 5. Best Practices & Troubleshooting
### Best Practices
- **For Hosts**: Enable auto-backups, mark safe points after playtesting, and use `Host-Only` approval mode for maximum control.
- **For Participants**: Submit all changes as proposals, wait for approval, and use descriptive names.
- **For Everyone**: Test changes in an emulator before submitting, make small atomic changes, and communicate with the team.
### Troubleshooting
- **"Failed to connect"**: Check that the `yaze-server` is running and the port is not blocked by a firewall.
- **"Corruption detected"**: Use `version_mgr->AutoRecover()` or manually restore the last known safe point from the Collaboration Panel.
- **"Snapshot not found"**: Verify the snapshot ID is correct and wasn't deleted due to storage limits.
- **"SSL handshake failed"**: Ensure you are using a `wss://` URL and that a valid SSL certificate is configured on the server.
## 6. Security Considerations
- **Transport Security**: Production environments should use WebSocket Secure (`wss://`) with a valid SSL/TLS certificate.
- **Approval Security**: `Host-Only` mode is the safest. For more open collaboration, consider a token-based authentication system.
- **ROM Protection**: The `RomVersionManager` is the primary defense. Always create snapshots before applying proposals and mark safe points often.
- **Rate Limiting**: The `yaze-server` enforces a rate limit (default: 100 messages/minute) to prevent abuse.
## 7. Future Enhancements
- **Encrypted WebSocket (WSS) support**
- **Diff visualization in UI**
- **Merge conflict resolution**
- **Branch/fork support for experimental changes**
- **AI-assisted proposal review**
- **Cloud snapshot backup**
- **Multi-host sessions**
- **Access control lists (ACLs)**
- **WebRTC for peer-to-peer connections**
- **Binary protocol for faster ROM syncs**
- **Automatic reconnection with exponential backoff**
- **Connection pooling for multiple sessions**
- **NAT traversal for home networks**
- **End-to-end encryption for proposals**

View File

@@ -135,6 +135,14 @@ The `z3ed` CLI is the foundation for an AI-driven Model-Code-Program (MCP) loop,
- `agent simple-chat`: A lightweight, non-TUI chat mode for scripting and automation.
- `agent test ...`: Commands for running and managing automated GUI tests.
- `agent learn ...`: **NEW**: Manage learned knowledge (preferences, ROM patterns, project context, conversation memory).
- `agent todo create "Description" [--category=<category>] [--priority=<n>]`: Create a new TODO item.
- `agent todo list [--status=<status>] [--category=<category>]`: List TODOs, with optional filters.
- `agent todo update <id> --status=<status>`: Update the status of a TODO item.
- `agent todo show <id>`: Display full details of a TODO item.
- `agent todo delete <id>`: Delete a TODO item.
- `agent todo clear-completed`: Remove all completed TODOs.
- `agent todo next`: Get the next actionable TODO based on dependencies and priority.
- `agent todo plan`: Generate a topologically-sorted execution plan for all TODOs.
### Resource Commands
@@ -239,7 +247,23 @@ All learned data is stored in `~/.yaze/agent/`:
- `projects.json`: Project contexts
- `memories.json`: Conversation summaries
## 9. CLI Output & Help System
## 9. TODO Management System
The TODO Management System enables the z3ed AI agent to create, track, and execute complex multi-step tasks with dependency management and prioritization.
### Core Capabilities
- ✅ Create and manage TODO items with priorities
- ✅ Track task status (pending, in_progress, completed, blocked, cancelled)
- ✅ Dependency tracking between tasks
- ✅ Automatic execution plan generation
- ✅ Persistent storage in JSON format
- ✅ Category-based organization
- ✅ Tools/functions tracking per task
### Storage Location
TODOs are persisted to: `~/.yaze/agent/todos.json` (macOS/Linux) or `%APPDATA%/yaze/agent/todos.json` (Windows)
## 10. CLI Output & Help System
The `z3ed` CLI features a modernized output system designed to be clean for users and informative for developers.
@@ -895,6 +919,7 @@ The AI response appears in your chat history and can reference specific details
- **Native Gemini Function Calling**: Upgraded from manual curl to native function calling API with automatic tool schema generation
- **Multimodal Vision Testing**: Comprehensive test suite for Gemini vision capabilities with screenshot integration
- **AI-Controlled GUI Automation**: Natural language parsing (`AIActionParser`) and test script generation (`GuiActionGenerator`) for automated tile placement
- **TODO Management System**: Full `TodoManager` class with CRUD operations, CLI commands, dependency tracking, execution planning, and JSON persistence.
#### Version Management & Protection
- **ROM Version Management System**: `RomVersionManager` with automatic snapshots, safe points, corruption detection, and rollback capabilities
@@ -908,11 +933,16 @@ The AI response appears in your chat history and can reference specific details
- **Collaboration UI Panel**: `CollaborationPanel` widget with version history, ROM sync tracking, snapshot gallery, and approval workflow
- **gRPC ROM Service**: Complete protocol buffer and implementation for remote ROM manipulation (pending build integration)
#### UI/UX Enhancements
- **Welcome Screen Enhancement**: Dynamic theme integration, Zelda-themed animations, and project cards.
- **Component Refactoring**: `PaletteWidget` renamed and moved, UI organization improved (`app/editor/ui/` for welcome_screen, editor_selection_dialog, background_renderer).
#### Build System & Infrastructure
- **gRPC Windows Build Optimization**: vcpkg integration for 10-20x faster Windows builds, removed abseil-cpp submodule
- **Cross-Platform Networking**: Native socket support (ws2_32 on Windows, BSD sockets on Unix)
- **Namespace Refactoring**: Created `app/net` namespace for networking components
- **Improved Documentation**: Consolidated architecture, enhancement plans, networking guide, and build instructions with JSON-first approach
- **Build System Improvements**: `mac-ai` preset, proto fixes, and updated GEMINI.md with AI build policies.
## 12. Troubleshooting

View File

@@ -457,9 +457,10 @@ else()
set(YAZE_RES_FILE "${CMAKE_CURRENT_BINARY_DIR}/yaze.res")
# Add a custom command to generate the .res file from the .rc and .ico
# /I adds include directory so RC compiler can find yaze.ico
add_custom_command(
OUTPUT ${YAZE_RES_FILE}
COMMAND ${CMAKE_RC_COMPILER} /fo ${YAZE_RES_FILE} ${YAZE_RC_FILE}
COMMAND ${CMAKE_RC_COMPILER} /fo ${YAZE_RES_FILE} /I "${CMAKE_SOURCE_DIR}/assets" ${YAZE_RC_FILE}
DEPENDS ${YAZE_RC_FILE} ${YAZE_ICO_FILE}
COMMENT "Generating yaze.res from yaze.rc and yaze.ico"
VERBATIM

View File

@@ -68,6 +68,8 @@ set(YAZE_AGENT_SOURCES
cli/service/agent/proposal_executor.cc
cli/handlers/agent/tool_commands.cc
cli/handlers/agent/todo_commands.cc
cli/handlers/agent/hex_commands.cc
cli/handlers/agent/palette_commands.cc
cli/service/agent/conversational_agent_service.cc
cli/service/agent/simple_chat_session.cc
cli/service/agent/tool_dispatcher.cc

View File

@@ -151,6 +151,28 @@ absl::Status Agent::Run(const std::vector<std::string>& arg_vec) {
if (subcommand == "simple-chat") {
return agent::HandleSimpleChatCommand(subcommand_args, &rom_, absl::GetFlag(FLAGS_quiet));
}
// Hex manipulation commands
if (subcommand == "hex-read") {
return agent::HandleHexRead(subcommand_args, &rom_);
}
if (subcommand == "hex-write") {
return agent::HandleHexWrite(subcommand_args, &rom_);
}
if (subcommand == "hex-search") {
return agent::HandleHexSearch(subcommand_args, &rom_);
}
// Palette manipulation commands
if (subcommand == "palette-get-colors") {
return agent::HandlePaletteGetColors(subcommand_args, &rom_);
}
if (subcommand == "palette-set-color") {
return agent::HandlePaletteSetColor(subcommand_args, &rom_);
}
if (subcommand == "palette-analyze") {
return agent::HandlePaletteAnalyze(subcommand_args, &rom_);
}
return absl::InvalidArgumentError(std::string(agent::kUsage));
}

View File

@@ -69,6 +69,22 @@ absl::Status HandleSimpleChatCommand(const std::vector<std::string>&, Rom* rom,
absl::Status HandleTestConversationCommand(
const std::vector<std::string>& arg_vec);
// Hex manipulation commands
absl::Status HandleHexRead(const std::vector<std::string>& arg_vec,
Rom* rom_context = nullptr);
absl::Status HandleHexWrite(const std::vector<std::string>& arg_vec,
Rom* rom_context = nullptr);
absl::Status HandleHexSearch(const std::vector<std::string>& arg_vec,
Rom* rom_context = nullptr);
// Palette manipulation commands
absl::Status HandlePaletteGetColors(const std::vector<std::string>& arg_vec,
Rom* rom_context = nullptr);
absl::Status HandlePaletteSetColor(const std::vector<std::string>& arg_vec,
Rom* rom_context = nullptr);
absl::Status HandlePaletteAnalyze(const std::vector<std::string>& arg_vec,
Rom* rom_context = nullptr);
} // namespace agent
} // namespace cli
} // namespace yaze

View File

@@ -0,0 +1,287 @@
#include "cli/handlers/agent/hex_commands.h"
#include <algorithm>
#include <iomanip>
#include <iostream>
#include <sstream>
#include <vector>
#include "absl/status/status.h"
#include "absl/strings/str_format.h"
#include "absl/strings/str_split.h"
#include "app/rom.h"
namespace yaze {
namespace cli {
namespace agent {
namespace {
// Parse hex address from string (supports 0x prefix)
absl::StatusOr<uint32_t> ParseHexAddress(const std::string& str) {
try {
size_t pos;
uint32_t addr = std::stoul(str, &pos, 16);
if (pos != str.size()) {
return absl::InvalidArgumentError(
absl::StrFormat("Invalid hex address: %s", str));
}
return addr;
} catch (const std::exception& e) {
return absl::InvalidArgumentError(
absl::StrFormat("Failed to parse address '%s': %s", str, e.what()));
}
}
// Parse hex byte pattern (supports wildcards ??)
std::vector<std::pair<uint8_t, bool>> ParseHexPattern(const std::string& pattern) {
std::vector<std::pair<uint8_t, bool>> result;
std::vector<std::string> bytes = absl::StrSplit(pattern, ' ');
for (const auto& byte_str : bytes) {
if (byte_str.empty()) continue;
if (byte_str == "??" || byte_str == "?") {
result.push_back({0x00, false}); // Wildcard
} else {
try {
uint8_t value = static_cast<uint8_t>(std::stoul(byte_str, nullptr, 16));
result.push_back({value, true}); // Exact match
} catch (const std::exception&) {
std::cerr << "Warning: Invalid byte in pattern: " << byte_str << std::endl;
}
}
}
return result;
}
// Format bytes as hex string
std::string FormatHexBytes(const uint8_t* data, size_t length,
const std::string& format) {
std::ostringstream oss;
if (format == "hex" || format == "both") {
for (size_t i = 0; i < length; ++i) {
oss << std::hex << std::setw(2) << std::setfill('0')
<< static_cast<int>(data[i]);
if (i < length - 1) oss << " ";
}
}
if (format == "both") {
oss << " | ";
}
if (format == "ascii" || format == "both") {
for (size_t i = 0; i < length; ++i) {
char c = static_cast<char>(data[i]);
oss << (std::isprint(c) ? c : '.');
}
}
return oss.str();
}
} // namespace
absl::Status HandleHexRead(const std::vector<std::string>& args,
Rom* rom_context) {
if (!rom_context || !rom_context->is_loaded()) {
return absl::FailedPreconditionError("ROM not loaded");
}
// Parse arguments
std::string address_str;
int length = 16;
std::string format = "both";
for (const auto& arg : args) {
if (arg.rfind("--address=", 0) == 0) {
address_str = arg.substr(10);
} else if (arg.rfind("--length=", 0) == 0) {
length = std::stoi(arg.substr(9));
} else if (arg.rfind("--format=", 0) == 0) {
format = arg.substr(9);
}
}
if (address_str.empty()) {
return absl::InvalidArgumentError("--address required");
}
// Parse address
auto addr_or = ParseHexAddress(address_str);
if (!addr_or.ok()) {
return addr_or.status();
}
uint32_t address = addr_or.value();
// Validate range
if (address + length > rom_context->size()) {
return absl::OutOfRangeError(
absl::StrFormat("Read beyond ROM: 0x%X+%d > %zu",
address, length, rom_context->size()));
}
// Read and format data
const uint8_t* data = rom_context->data() + address;
std::string formatted = FormatHexBytes(data, length, format);
// Output
std::cout << absl::StrFormat("Address 0x%06X [%d bytes]:\n", address, length);
std::cout << formatted << std::endl;
return absl::OkStatus();
}
absl::Status HandleHexWrite(const std::vector<std::string>& args,
Rom* rom_context) {
if (!rom_context || !rom_context->is_loaded()) {
return absl::FailedPreconditionError("ROM not loaded");
}
// Parse arguments
std::string address_str;
std::string data_str;
for (const auto& arg : args) {
if (arg.rfind("--address=", 0) == 0) {
address_str = arg.substr(10);
} else if (arg.rfind("--data=", 0) == 0) {
data_str = arg.substr(7);
}
}
if (address_str.empty() || data_str.empty()) {
return absl::InvalidArgumentError("--address and --data required");
}
// Parse address
auto addr_or = ParseHexAddress(address_str);
if (!addr_or.ok()) {
return addr_or.status();
}
uint32_t address = addr_or.value();
// Parse data bytes
std::vector<std::string> byte_strs = absl::StrSplit(data_str, ' ');
std::vector<uint8_t> bytes;
for (const auto& byte_str : byte_strs) {
if (byte_str.empty()) continue;
try {
uint8_t value = static_cast<uint8_t>(std::stoul(byte_str, nullptr, 16));
bytes.push_back(value);
} catch (const std::exception& e) {
return absl::InvalidArgumentError(
absl::StrFormat("Invalid byte '%s': %s", byte_str, e.what()));
}
}
if (bytes.empty()) {
return absl::InvalidArgumentError("No valid bytes to write");
}
// Validate range
if (address + bytes.size() > rom_context->size()) {
return absl::OutOfRangeError(
absl::StrFormat("Write beyond ROM: 0x%X+%zu > %zu",
address, bytes.size(), rom_context->size()));
}
// Write data
for (size_t i = 0; i < bytes.size(); ++i) {
rom_context->WriteByte(address + i, bytes[i]);
}
// Output confirmation
std::cout << absl::StrFormat("✓ Wrote %zu bytes to address 0x%06X\n",
bytes.size(), address);
std::cout << " Data: " << FormatHexBytes(bytes.data(), bytes.size(), "hex")
<< std::endl;
// Note: In a full implementation, this would create a proposal
std::cout << "Note: Changes written directly to ROM (proposal system TBD)\n";
return absl::OkStatus();
}
absl::Status HandleHexSearch(const std::vector<std::string>& args,
Rom* rom_context) {
if (!rom_context || !rom_context->is_loaded()) {
return absl::FailedPreconditionError("ROM not loaded");
}
// Parse arguments
std::string pattern_str;
uint32_t start_address = 0;
uint32_t end_address = rom_context->size();
for (const auto& arg : args) {
if (arg.rfind("--pattern=", 0) == 0) {
pattern_str = arg.substr(10);
} else if (arg.rfind("--start=", 0) == 0) {
auto addr_or = ParseHexAddress(arg.substr(8));
if (addr_or.ok()) {
start_address = addr_or.value();
}
} else if (arg.rfind("--end=", 0) == 0) {
auto addr_or = ParseHexAddress(arg.substr(6));
if (addr_or.ok()) {
end_address = addr_or.value();
}
}
}
if (pattern_str.empty()) {
return absl::InvalidArgumentError("--pattern required");
}
// Parse pattern
auto pattern = ParseHexPattern(pattern_str);
if (pattern.empty()) {
return absl::InvalidArgumentError("Empty or invalid pattern");
}
// Search for pattern
std::vector<uint32_t> matches;
const uint8_t* rom_data = rom_context->data();
for (uint32_t i = start_address; i <= end_address - pattern.size(); ++i) {
bool match = true;
for (size_t j = 0; j < pattern.size(); ++j) {
if (pattern[j].second && // If not wildcard
rom_data[i + j] != pattern[j].first) {
match = false;
break;
}
}
if (match) {
matches.push_back(i);
}
}
// Output results
std::cout << absl::StrFormat("Pattern: %s\n", pattern_str);
std::cout << absl::StrFormat("Search range: 0x%06X - 0x%06X\n",
start_address, end_address);
std::cout << absl::StrFormat("Found %zu match(es):\n", matches.size());
for (size_t i = 0; i < matches.size() && i < 100; ++i) { // Limit output
uint32_t addr = matches[i];
std::string context = FormatHexBytes(rom_data + addr, pattern.size(), "hex");
std::cout << absl::StrFormat(" 0x%06X: %s\n", addr, context);
}
if (matches.size() > 100) {
std::cout << absl::StrFormat(" ... and %zu more matches\n",
matches.size() - 100);
}
return absl::OkStatus();
}
} // namespace agent
} // namespace cli
} // namespace yaze

View File

@@ -0,0 +1,55 @@
#ifndef YAZE_CLI_HANDLERS_AGENT_HEX_COMMANDS_H_
#define YAZE_CLI_HANDLERS_AGENT_HEX_COMMANDS_H_
#include <string>
#include <vector>
#include "absl/status/status.h"
namespace yaze {
class Rom;
namespace cli {
namespace agent {
/**
* @brief Read bytes from ROM at a specific address
*
* @param args Command arguments: [address, length, format]
* @param rom_context ROM instance to read from
* @return absl::Status Result of the operation
*
* Example: hex-read --address=0x1C800 --length=16 --format=both
*/
absl::Status HandleHexRead(const std::vector<std::string>& args,
Rom* rom_context = nullptr);
/**
* @brief Write bytes to ROM at a specific address (creates proposal)
*
* @param args Command arguments: [address, data]
* @param rom_context ROM instance to write to
* @return absl::Status Result of the operation
*
* Example: hex-write --address=0x1C800 --data="FF 00 12 34"
*/
absl::Status HandleHexWrite(const std::vector<std::string>& args,
Rom* rom_context = nullptr);
/**
* @brief Search for a byte pattern in ROM
*
* @param args Command arguments: [pattern, start_address, end_address]
* @param rom_context ROM instance to search in
* @return absl::Status Result of the operation
*
* Example: hex-search --pattern="FF 00 ?? 12" --start=0x00000
*/
absl::Status HandleHexSearch(const std::vector<std::string>& args,
Rom* rom_context = nullptr);
} // namespace agent
} // namespace cli
} // namespace yaze
#endif // YAZE_CLI_HANDLERS_AGENT_HEX_COMMANDS_H_

View File

@@ -0,0 +1,344 @@
#include "cli/handlers/agent/palette_commands.h"
#include <algorithm>
#include <iomanip>
#include <iostream>
#include <map>
#include <sstream>
#include "absl/status/status.h"
#include "absl/strings/str_format.h"
#include "app/gfx/snes_palette.h"
#include "app/rom.h"
namespace yaze {
namespace cli {
namespace agent {
namespace {
// Convert SNES color to RGB
struct RGB {
uint8_t r, g, b;
};
RGB SnesColorToRGB(uint16_t snes_color) {
// SNES color format: 0bbbbbgggggrrrrr (5 bits per channel)
uint8_t r = (snes_color & 0x1F) << 3;
uint8_t g = ((snes_color >> 5) & 0x1F) << 3;
uint8_t b = ((snes_color >> 10) & 0x1F) << 3;
return {r, g, b};
}
// Convert RGB to SNES color
uint16_t RGBToSnesColor(uint8_t r, uint8_t g, uint8_t b) {
return ((r >> 3) & 0x1F) | (((g >> 3) & 0x1F) << 5) | (((b >> 3) & 0x1F) << 10);
}
// Parse hex color (supports #RRGGBB or RRGGBB)
absl::StatusOr<RGB> ParseHexColor(const std::string& str) {
std::string clean = str;
if (!clean.empty() && clean[0] == '#') {
clean = clean.substr(1);
}
if (clean.length() != 6) {
return absl::InvalidArgumentError(
absl::StrFormat("Invalid hex color format: %s (expected 6 hex digits)", str));
}
try {
unsigned long value = std::stoul(clean, nullptr, 16);
RGB rgb;
rgb.r = (value >> 16) & 0xFF;
rgb.g = (value >> 8) & 0xFF;
rgb.b = value & 0xFF;
return rgb;
} catch (const std::exception& e) {
return absl::InvalidArgumentError(
absl::StrFormat("Failed to parse hex color '%s': %s", str, e.what()));
}
}
// Format color based on requested format
std::string FormatColor(uint16_t snes_color, const std::string& format) {
if (format == "snes") {
return absl::StrFormat("$%04X", snes_color);
}
RGB rgb = SnesColorToRGB(snes_color);
if (format == "rgb") {
return absl::StrFormat("rgb(%d, %d, %d)", rgb.r, rgb.g, rgb.b);
}
// Default to hex
return absl::StrFormat("#%02X%02X%02X", rgb.r, rgb.g, rgb.b);
}
} // namespace
absl::Status HandlePaletteGetColors(const std::vector<std::string>& args,
Rom* rom_context) {
if (!rom_context || !rom_context->is_loaded()) {
return absl::FailedPreconditionError("ROM not loaded");
}
// Parse arguments
int group = -1;
int palette = -1;
std::string format = "hex";
for (const auto& arg : args) {
if (arg.rfind("--group=", 0) == 0) {
group = std::stoi(arg.substr(8));
} else if (arg.rfind("--palette=", 0) == 0) {
palette = std::stoi(arg.substr(10));
} else if (arg.rfind("--format=", 0) == 0) {
format = arg.substr(9);
}
}
if (group < 0 || palette < 0) {
return absl::InvalidArgumentError("--group and --palette required");
}
// Validate indices
if (palette > 7) {
return absl::OutOfRangeError(
absl::StrFormat("Palette index %d out of range (0-7)", palette));
}
// Calculate palette address in ROM
// ALTTP palettes are stored at different locations depending on type
// For now, use a simplified overworld palette calculation
constexpr uint32_t kPaletteBase = 0xDE6C8; // Overworld palettes start
constexpr uint32_t kColorsPerPalette = 16;
constexpr uint32_t kBytesPerColor = 2;
uint32_t palette_addr = kPaletteBase +
(group * 8 * kColorsPerPalette * kBytesPerColor) +
(palette * kColorsPerPalette * kBytesPerColor);
if (palette_addr + (kColorsPerPalette * kBytesPerColor) > rom_context->size()) {
return absl::OutOfRangeError(
absl::StrFormat("Palette address 0x%X beyond ROM", palette_addr));
}
// Read palette colors
std::cout << absl::StrFormat("Palette Group %d, Palette %d:\n", group, palette);
std::cout << absl::StrFormat("ROM Address: 0x%06X\n\n", palette_addr);
for (int i = 0; i < kColorsPerPalette; ++i) {
uint32_t color_addr = palette_addr + (i * kBytesPerColor);
auto snes_color_or = rom_context->ReadWord(color_addr);
if (!snes_color_or.ok()) {
return snes_color_or.status();
}
uint16_t snes_color = snes_color_or.value();
std::string formatted = FormatColor(snes_color, format);
std::cout << absl::StrFormat(" Color %2d: %s", i, formatted);
// Show all formats for first color as example
if (i == 0) {
std::cout << absl::StrFormat(" (SNES: $%04X, RGB: %s, HEX: %s)",
snes_color,
FormatColor(snes_color, "rgb"),
FormatColor(snes_color, "hex"));
}
std::cout << "\n";
}
return absl::OkStatus();
}
absl::Status HandlePaletteSetColor(const std::vector<std::string>& args,
Rom* rom_context) {
if (!rom_context || !rom_context->is_loaded()) {
return absl::FailedPreconditionError("ROM not loaded");
}
// Parse arguments
int group = -1;
int palette = -1;
int color_index = -1;
std::string color_str;
for (const auto& arg : args) {
if (arg.rfind("--group=", 0) == 0) {
group = std::stoi(arg.substr(8));
} else if (arg.rfind("--palette=", 0) == 0) {
palette = std::stoi(arg.substr(10));
} else if (arg.rfind("--index=", 0) == 0) {
color_index = std::stoi(arg.substr(8));
} else if (arg.rfind("--color=", 0) == 0) {
color_str = arg.substr(8);
}
}
if (group < 0 || palette < 0 || color_index < 0 || color_str.empty()) {
return absl::InvalidArgumentError(
"--group, --palette, --index, and --color required");
}
// Validate indices
if (palette > 7) {
return absl::OutOfRangeError(
absl::StrFormat("Palette index %d out of range (0-7)", palette));
}
if (color_index >= 16) {
return absl::OutOfRangeError(
absl::StrFormat("Color index %d out of range (0-15)", color_index));
}
// Parse color
auto rgb_or = ParseHexColor(color_str);
if (!rgb_or.ok()) {
return rgb_or.status();
}
RGB rgb = rgb_or.value();
uint16_t snes_color = RGBToSnesColor(rgb.r, rgb.g, rgb.b);
// Calculate address
constexpr uint32_t kPaletteBase = 0xDE6C8;
constexpr uint32_t kColorsPerPalette = 16;
constexpr uint32_t kBytesPerColor = 2;
uint32_t color_addr = kPaletteBase +
(group * 8 * kColorsPerPalette * kBytesPerColor) +
(palette * kColorsPerPalette * kBytesPerColor) +
(color_index * kBytesPerColor);
if (color_addr + kBytesPerColor > rom_context->size()) {
return absl::OutOfRangeError(
absl::StrFormat("Color address 0x%X beyond ROM", color_addr));
}
// Read old value
auto old_color_or = rom_context->ReadWord(color_addr);
if (!old_color_or.ok()) {
return old_color_or.status();
}
uint16_t old_color = old_color_or.value();
// Write new value
auto write_status = rom_context->WriteWord(color_addr, snes_color);
if (!write_status.ok()) {
return write_status;
}
// Output confirmation
std::cout << absl::StrFormat("✓ Set color in Palette %d/%d, Index %d\n",
group, palette, color_index);
std::cout << absl::StrFormat(" Address: 0x%06X\n", color_addr);
std::cout << absl::StrFormat(" Old: %s\n", FormatColor(old_color, "hex"));
std::cout << absl::StrFormat(" New: %s (SNES: $%04X)\n",
FormatColor(snes_color, "hex"), snes_color);
std::cout << "Note: Changes written directly to ROM (proposal system TBD)\n";
return absl::OkStatus();
}
absl::Status HandlePaletteAnalyze(const std::vector<std::string>& args,
Rom* rom_context) {
if (!rom_context || !rom_context->is_loaded()) {
return absl::FailedPreconditionError("ROM not loaded");
}
// Parse arguments
std::string target_type;
std::string target_id;
for (const auto& arg : args) {
if (arg.rfind("--type=", 0) == 0) {
target_type = arg.substr(7);
} else if (arg.rfind("--id=", 0) == 0) {
target_id = arg.substr(5);
}
}
if (target_type.empty() || target_id.empty()) {
return absl::InvalidArgumentError("--type and --id required");
}
if (target_type == "palette") {
// Parse palette ID (assume format "group/palette")
size_t slash_pos = target_id.find('/');
if (slash_pos == std::string::npos) {
return absl::InvalidArgumentError(
"Palette ID format should be 'group/palette' (e.g., '0/0')");
}
int group = std::stoi(target_id.substr(0, slash_pos));
int palette = std::stoi(target_id.substr(slash_pos + 1));
// Read palette
constexpr uint32_t kPaletteBase = 0xDE6C8;
constexpr uint32_t kColorsPerPalette = 16;
constexpr uint32_t kBytesPerColor = 2;
uint32_t palette_addr = kPaletteBase +
(group * 8 * kColorsPerPalette * kBytesPerColor) +
(palette * kColorsPerPalette * kBytesPerColor);
// Analyze colors
std::map<uint16_t, int> color_usage;
int transparent_count = 0;
int darkest = 0xFFFF, brightest = 0;
for (int i = 0; i < kColorsPerPalette; ++i) {
uint32_t color_addr = palette_addr + (i * kBytesPerColor);
auto snes_color_or = rom_context->ReadWord(color_addr);
if (!snes_color_or.ok()) {
return snes_color_or.status();
}
uint16_t snes_color = snes_color_or.value();
color_usage[snes_color]++;
if (snes_color == 0) {
transparent_count++;
}
// Calculate brightness (simple sum of RGB components)
RGB rgb = SnesColorToRGB(snes_color);
int brightness = rgb.r + rgb.g + rgb.b;
if (brightness < (((darkest & 0x1F) + ((darkest >> 5) & 0x1F) + ((darkest >> 10) & 0x1F)) * 8)) {
darkest = snes_color;
}
if (brightness > (((brightest & 0x1F) + ((brightest >> 5) & 0x1F) + ((brightest >> 10) & 0x1F)) * 8)) {
brightest = snes_color;
}
}
// Output analysis
std::cout << absl::StrFormat("Palette Analysis: Group %d, Palette %d\n", group, palette);
std::cout << absl::StrFormat("Address: 0x%06X\n\n", palette_addr);
std::cout << absl::StrFormat("Total colors: %d\n", kColorsPerPalette);
std::cout << absl::StrFormat("Unique colors: %zu\n", color_usage.size());
std::cout << absl::StrFormat("Transparent/black (0): %d\n", transparent_count);
std::cout << absl::StrFormat("Darkest color: %s\n", FormatColor(darkest, "hex"));
std::cout << absl::StrFormat("Brightest color: %s\n", FormatColor(brightest, "hex"));
if (color_usage.size() < kColorsPerPalette) {
std::cout << "\nDuplicate colors found:\n";
for (const auto& [color, count] : color_usage) {
if (count > 1) {
std::cout << absl::StrFormat(" %s appears %d times\n",
FormatColor(color, "hex"), count);
}
}
}
} else {
return absl::UnimplementedError(
absl::StrFormat("Analysis for type '%s' not yet implemented", target_type));
}
return absl::OkStatus();
}
} // namespace agent
} // namespace cli
} // namespace yaze

View File

@@ -0,0 +1,55 @@
#ifndef YAZE_CLI_HANDLERS_AGENT_PALETTE_COMMANDS_H_
#define YAZE_CLI_HANDLERS_AGENT_PALETTE_COMMANDS_H_
#include <string>
#include <vector>
#include "absl/status/status.h"
namespace yaze {
class Rom;
namespace cli {
namespace agent {
/**
* @brief Get all colors from a specific palette
*
* @param args Command arguments: [group, palette, format]
* @param rom_context ROM instance to read from
* @return absl::Status Result of the operation
*
* Example: palette-get-colors --group=0 --palette=0 --format=hex
*/
absl::Status HandlePaletteGetColors(const std::vector<std::string>& args,
Rom* rom_context = nullptr);
/**
* @brief Set a specific color in a palette (creates proposal)
*
* @param args Command arguments: [group, palette, color_index, color]
* @param rom_context ROM instance to modify
* @return absl::Status Result of the operation
*
* Example: palette-set-color --group=0 --palette=0 --index=5 --color=FF0000
*/
absl::Status HandlePaletteSetColor(const std::vector<std::string>& args,
Rom* rom_context = nullptr);
/**
* @brief Analyze color usage and statistics for a palette or bitmap
*
* @param args Command arguments: [target_type, target_id]
* @param rom_context ROM instance to analyze
* @return absl::Status Result of the operation
*
* Example: palette-analyze --type=palette --id=0
*/
absl::Status HandlePaletteAnalyze(const std::vector<std::string>& args,
Rom* rom_context = nullptr);
} // namespace agent
} // namespace cli
} // namespace yaze
#endif // YAZE_CLI_HANDLERS_AGENT_PALETTE_COMMANDS_H_

View File

@@ -0,0 +1,452 @@
#include "cli/service/agent/vim_mode.h"
#include <algorithm>
#include <cctype>
#include <iostream>
#ifdef _WIN32
#include <conio.h>
#else
#include <termios.h>
#include <unistd.h>
#endif
namespace yaze {
namespace cli {
namespace agent {
namespace {
// Key codes for special keys
constexpr int KEY_ESC = 27;
constexpr int KEY_ENTER = 10;
constexpr int KEY_BACKSPACE = 127;
constexpr int KEY_CTRL_P = 16;
constexpr int KEY_CTRL_N = 14;
constexpr int KEY_TAB = 9;
// Terminal control sequences
const char* CLEAR_LINE = "\033[2K\r";
const char* MOVE_CURSOR_HOME = "\r";
const char* SAVE_CURSOR = "\033[s";
const char* RESTORE_CURSOR = "\033[u";
#ifndef _WIN32
// Set terminal to raw mode (character-by-character input)
void SetRawMode(bool enable) {
static struct termios orig_termios;
static bool has_orig = false;
if (enable) {
if (!has_orig) {
tcgetattr(STDIN_FILENO, &orig_termios);
has_orig = true;
}
struct termios raw = orig_termios;
raw.c_lflag &= ~(ECHO | ICANON); // Disable echo and canonical mode
raw.c_cc[VMIN] = 1; // Read at least 1 character
raw.c_cc[VTIME] = 0; // No timeout
tcsetattr(STDIN_FILENO, TCSAFLUSH, &raw);
} else {
if (has_orig) {
tcsetattr(STDIN_FILENO, TCSAFLUSH, &orig_termios);
}
}
}
#endif
} // namespace
VimMode::VimMode() {
#ifndef _WIN32
SetRawMode(true);
#endif
}
std::string VimMode::GetModeString() const {
switch (mode_) {
case VimModeType::NORMAL:
return "NORMAL";
case VimModeType::INSERT:
return "INSERT";
case VimModeType::VISUAL:
return "VISUAL";
case VimModeType::COMMAND_LINE:
return "COMMAND";
}
return "UNKNOWN";
}
void VimMode::Reset() {
current_line_.clear();
cursor_pos_ = 0;
line_complete_ = false;
redo_stack_.clear();
}
void VimMode::AddToHistory(const std::string& line) {
if (!line.empty()) {
history_.push_back(line);
history_index_ = -1; // Reset to no history selection
}
}
bool VimMode::ProcessKey(int ch) {
line_complete_ = false;
switch (mode_) {
case VimModeType::NORMAL:
HandleNormalMode(ch);
break;
case VimModeType::INSERT:
HandleInsertMode(ch);
break;
case VimModeType::VISUAL:
// Visual mode not yet implemented
SwitchMode(VimModeType::NORMAL);
break;
case VimModeType::COMMAND_LINE:
// Command line mode not yet implemented
if (ch == KEY_ESC) {
SwitchMode(VimModeType::NORMAL);
}
break;
}
return line_complete_;
}
void VimMode::SwitchMode(VimModeType new_mode) {
if (new_mode != mode_) {
mode_ = new_mode;
Render(); // Redraw with new mode indicator
}
}
void VimMode::HandleNormalMode(int ch) {
switch (ch) {
// Enter insert mode
case 'i':
SwitchMode(VimModeType::INSERT);
break;
case 'a':
MoveRight();
SwitchMode(VimModeType::INSERT);
break;
case 'o':
// Insert new line below (just append and go to insert mode)
MoveToLineEnd();
SwitchMode(VimModeType::INSERT);
break;
case 'O':
// Insert new line above (go to beginning and insert mode)
MoveToLineStart();
SwitchMode(VimModeType::INSERT);
break;
// Movement
case 'h':
MoveLeft();
break;
case 'l':
MoveRight();
break;
case 'w':
MoveWordForward();
break;
case 'b':
MoveWordBackward();
break;
case '0':
MoveToLineStart();
break;
case '$':
MoveToLineEnd();
break;
// Editing
case 'x':
DeleteChar();
break;
case 'd':
// Simple implementation: dd deletes line
{
int next_ch;
#ifdef _WIN32
next_ch = _getch();
#else
read(STDIN_FILENO, &next_ch, 1);
#endif
if (next_ch == 'd') {
DeleteLine();
}
}
break;
case 'y':
// yy yanks line
{
int next_ch;
#ifdef _WIN32
next_ch = _getch();
#else
read(STDIN_FILENO, &next_ch, 1);
#endif
if (next_ch == 'y') {
YankLine();
}
}
break;
case 'p':
PasteAfter();
break;
case 'P':
PasteBefore();
break;
case 'u':
Undo();
break;
case KEY_CTRL_P:
case 'k':
HistoryPrev();
break;
case KEY_CTRL_N:
case 'j':
HistoryNext();
break;
// Accept line (Enter in normal mode)
case KEY_ENTER:
line_complete_ = true;
break;
// Command mode
case ':':
SwitchMode(VimModeType::COMMAND_LINE);
break;
}
Render();
}
void VimMode::HandleInsertMode(int ch) {
switch (ch) {
case KEY_ESC:
SwitchMode(VimModeType::NORMAL);
if (cursor_pos_ > 0) {
cursor_pos_--; // Vim moves cursor left on ESC
}
break;
case KEY_ENTER:
line_complete_ = true;
break;
case KEY_BACKSPACE:
case 8: // Ctrl+H
Backspace();
break;
case KEY_TAB:
Complete();
break;
case KEY_CTRL_P:
HistoryPrev();
break;
case KEY_CTRL_N:
HistoryNext();
break;
default:
if (ch >= 32 && ch < 127) { // Printable ASCII
InsertChar(static_cast<char>(ch));
}
break;
}
if (!line_complete_) {
Render();
}
}
// Movement implementations
void VimMode::MoveLeft() {
if (cursor_pos_ > 0) {
cursor_pos_--;
}
}
void VimMode::MoveRight() {
if (cursor_pos_ < static_cast<int>(current_line_.length())) {
cursor_pos_++;
}
}
void VimMode::MoveWordForward() {
while (cursor_pos_ < static_cast<int>(current_line_.length()) &&
!std::isspace(current_line_[cursor_pos_])) {
cursor_pos_++;
}
while (cursor_pos_ < static_cast<int>(current_line_.length()) &&
std::isspace(current_line_[cursor_pos_])) {
cursor_pos_++;
}
}
void VimMode::MoveWordBackward() {
if (cursor_pos_ > 0) cursor_pos_--;
while (cursor_pos_ > 0 && std::isspace(current_line_[cursor_pos_])) {
cursor_pos_--;
}
while (cursor_pos_ > 0 && !std::isspace(current_line_[cursor_pos_ - 1])) {
cursor_pos_--;
}
}
void VimMode::MoveToLineStart() {
cursor_pos_ = 0;
}
void VimMode::MoveToLineEnd() {
cursor_pos_ = current_line_.length();
}
// Editing implementations
void VimMode::DeleteChar() {
if (cursor_pos_ < static_cast<int>(current_line_.length())) {
undo_stack_.push_back(current_line_);
current_line_.erase(cursor_pos_, 1);
}
}
void VimMode::DeleteLine() {
undo_stack_.push_back(current_line_);
yank_buffer_ = current_line_;
current_line_.clear();
cursor_pos_ = 0;
}
void VimMode::YankLine() {
yank_buffer_ = current_line_;
}
void VimMode::PasteBefore() {
if (!yank_buffer_.empty()) {
undo_stack_.push_back(current_line_);
current_line_.insert(cursor_pos_, yank_buffer_);
}
}
void VimMode::PasteAfter() {
if (!yank_buffer_.empty()) {
undo_stack_.push_back(current_line_);
if (cursor_pos_ < static_cast<int>(current_line_.length())) {
cursor_pos_++;
}
current_line_.insert(cursor_pos_, yank_buffer_);
cursor_pos_ += yank_buffer_.length() - 1;
}
}
void VimMode::Undo() {
if (!undo_stack_.empty()) {
redo_stack_.push_back(current_line_);
current_line_ = undo_stack_.back();
undo_stack_.pop_back();
cursor_pos_ = std::min(cursor_pos_, static_cast<int>(current_line_.length()));
}
}
void VimMode::Redo() {
if (!redo_stack_.empty()) {
undo_stack_.push_back(current_line_);
current_line_ = redo_stack_.back();
redo_stack_.pop_back();
cursor_pos_ = std::min(cursor_pos_, static_cast<int>(current_line_.length()));
}
}
void VimMode::InsertChar(char c) {
undo_stack_.push_back(current_line_);
current_line_.insert(cursor_pos_, 1, c);
cursor_pos_++;
}
void VimMode::Backspace() {
if (cursor_pos_ > 0) {
undo_stack_.push_back(current_line_);
current_line_.erase(cursor_pos_ - 1, 1);
cursor_pos_--;
}
}
void VimMode::Delete() {
DeleteChar();
}
void VimMode::Complete() {
if (autocomplete_callback_) {
autocomplete_options_ = autocomplete_callback_(current_line_);
if (!autocomplete_options_.empty()) {
// Simple implementation: insert first suggestion
std::string completion = autocomplete_options_[0];
undo_stack_.push_back(current_line_);
current_line_ = completion;
cursor_pos_ = completion.length();
}
}
}
// History navigation
void VimMode::HistoryPrev() {
if (history_.empty()) return;
if (history_index_ == -1) {
history_index_ = history_.size() - 1;
} else if (history_index_ > 0) {
history_index_--;
}
current_line_ = history_[history_index_];
cursor_pos_ = current_line_.length();
}
void VimMode::HistoryNext() {
if (history_.empty() || history_index_ == -1) return;
if (history_index_ < static_cast<int>(history_.size()) - 1) {
history_index_++;
current_line_ = history_[history_index_];
} else {
history_index_ = -1;
current_line_.clear();
}
cursor_pos_ = current_line_.length();
}
void VimMode::Render() const {
// Clear line and redraw
std::cout << CLEAR_LINE;
// Show mode indicator
if (mode_ == VimModeType::INSERT) {
std::cout << "-- INSERT -- ";
} else if (mode_ == VimModeType::NORMAL) {
std::cout << "-- NORMAL -- ";
} else if (mode_ == VimModeType::COMMAND_LINE) {
std::cout << ":";
}
// Show prompt and line
std::cout << current_line_;
// Move cursor to correct position
int display_offset = (mode_ == VimModeType::INSERT ? 13 : 13); // Length of "-- INSERT -- "
std::cout << "\r";
for (int i = 0; i < display_offset + cursor_pos_; ++i) {
std::cout << "\033[C"; // Move cursor right
}
std::cout.flush();
}
} // namespace agent
} // namespace cli
} // namespace yaze