20260604
This commit is contained in:
197
AGENTS.md
Normal file
197
AGENTS.md
Normal file
@@ -0,0 +1,197 @@
|
|||||||
|
# Everjoy — Agent Onboarding Guide
|
||||||
|
|
||||||
|
> This file is written for AI coding agents. It assumes you know Rust but nothing about this project. For background, also read `doc/Purpose.MD`, `doc/Architecture.md`, `doc/Architecture-Runtime-and-Plugin.md`, `doc/Plugin-Lifecycle-Tool-vs-Provider.md`, and `TECH_STACK.md`.
|
||||||
|
|
||||||
|
## Project Overview
|
||||||
|
|
||||||
|
**Everjoy** is an AI-native TUI IDE / workflow tool written in Rust. It is currently at version `0.1.0` and is best described as an early MVP:
|
||||||
|
|
||||||
|
- A terminal user interface built with `ratatui` + `crossterm`.
|
||||||
|
- Three-panel layout: Sidebar (file tree), Main (editor / terminal), Agent (chat).
|
||||||
|
- A runtime architecture based on providers, tools, and an explicit `Command` pattern.
|
||||||
|
- An agent framework skeleton (roles, orchestrator, memory, tool invoker) that is not yet fully wired to an LLM.
|
||||||
|
- A plugin subsystem designed for external `.wasm` / `.so` extensions, but the loader and translator are currently stubs.
|
||||||
|
|
||||||
|
The project is organized as a single Cargo crate. There is no `pyproject.toml`, `package.json`, or other build system — Rust/Cargo is the only toolchain you need.
|
||||||
|
|
||||||
|
## Key Configuration Files
|
||||||
|
|
||||||
|
- `Cargo.toml` — Single crate named `everjoy`, edition `2021`. Lists all production dependencies (TUI, async, serialization, SQLite, text/files, config, utils). `[dev-dependencies]` is empty.
|
||||||
|
- `Cargo.lock` — Committed; pin your dependency versions.
|
||||||
|
- `.gitignore` — Standard Rust ignore (`/target`).
|
||||||
|
|
||||||
|
No custom `rustfmt.toml` or `clippy.toml` was found; use the default toolchain settings.
|
||||||
|
|
||||||
|
## Build and Run
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Compile
|
||||||
|
cargo build
|
||||||
|
|
||||||
|
# Run the TUI application
|
||||||
|
cargo run
|
||||||
|
|
||||||
|
# Fast check
|
||||||
|
cargo check
|
||||||
|
|
||||||
|
# Release build
|
||||||
|
cargo build --release
|
||||||
|
```
|
||||||
|
|
||||||
|
The binary runs in the terminal using the alternate screen. Current controls:
|
||||||
|
|
||||||
|
- `Ctrl+Q` — quit.
|
||||||
|
- `Ctrl+J` / `Ctrl+K` / `Ctrl+L` — focus Sidebar / Main / Agent panel.
|
||||||
|
- `Alt+<underlined letter>` — open menubar.
|
||||||
|
- Press any key from the splash screen to enter the main UI.
|
||||||
|
|
||||||
|
## Test Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo test
|
||||||
|
```
|
||||||
|
|
||||||
|
There are 23 unit tests, all passing. They live in inline `#[cfg(test)]` modules inside the built-in providers:
|
||||||
|
|
||||||
|
- `src/builtin/editor/tests.rs`
|
||||||
|
- `src/builtin/filetree/tests.rs`
|
||||||
|
- `src/builtin/agentchat/tests.rs`
|
||||||
|
- `src/builtin/terminal/tests.rs`
|
||||||
|
|
||||||
|
There are no integration tests or benchmarks yet. Keep it that way unless a feature genuinely needs them.
|
||||||
|
|
||||||
|
## Code Organization
|
||||||
|
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
main.rs # App struct, event loop, command dispatch, terminal setup
|
||||||
|
lib.rs # Re-exports: agent, builtin, plugin, runtime, ui
|
||||||
|
|
||||||
|
runtime/ # Core engine interfaces and registry
|
||||||
|
mod.rs
|
||||||
|
traits.rs # Plugin, PanelProvider, Tool, Command, Source, Registrar, etc.
|
||||||
|
registry.rs # ModuleRegistry: lazy provider factories, tool/menu/status storage, bidding
|
||||||
|
lifecycle.rs # LifecycleManager (mostly TODO)
|
||||||
|
ctx.rs # RuntimeServices trait (ProviderCtx was removed)
|
||||||
|
|
||||||
|
ui/ # TUI framework helpers
|
||||||
|
mod.rs
|
||||||
|
layout.rs # Three-panel ratios (sidebar 20%, main 52%, agent 28%)
|
||||||
|
focus.rs # FocusManager across panels
|
||||||
|
menubar.rs # Top menubar with Alt+ shortcuts
|
||||||
|
statusbar.rs # Bottom status bar
|
||||||
|
splash.rs # Startup splash screen
|
||||||
|
scheduler.rs # RenderScheduler (stub)
|
||||||
|
router.rs # EventRouter (stub)
|
||||||
|
|
||||||
|
agent/ # Agent framework skeleton
|
||||||
|
mod.rs
|
||||||
|
roles.rs # Built-in roles: secretary, driver, negfeedback, executor
|
||||||
|
orchestrator.rs # Multi-agent coordinator (stub)
|
||||||
|
memory.rs # MemorySystem (stub)
|
||||||
|
tools.rs # ToolInvoker (stub)
|
||||||
|
|
||||||
|
plugin/ # External plugin adapter (designed but not implemented)
|
||||||
|
mod.rs
|
||||||
|
loader.rs # ExtensionLoader (stub)
|
||||||
|
translator.rs # C-ABI ↔ Rust trait wrapper (stub)
|
||||||
|
surface.rs # EjSurface / EjCell for plugin rendering
|
||||||
|
panic.rs # Panic isolation helper
|
||||||
|
|
||||||
|
builtin/ # Built-in panel providers
|
||||||
|
mod.rs
|
||||||
|
editor/mod.rs # EditorProvider (Main panel, text files)
|
||||||
|
filetree/mod.rs # FileTreeProvider (Sidebar, project navigator)
|
||||||
|
agentchat/mod.rs # AgentChatProvider (Agent panel, chat UI)
|
||||||
|
terminal/mod.rs # TerminalProvider (Main panel, mock shell)
|
||||||
|
tasktree/mod.rs # TaskTreeProvider (Main panel, task view stub)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Runtime Architecture
|
||||||
|
|
||||||
|
The project centers on the **Runtime Subsystem** (`src/runtime/`). Understand these concepts before changing anything:
|
||||||
|
|
||||||
|
1. **`PanelProvider`** — Anything that renders into one of the three panels (`Sidebar`, `Main`, `Agent`). Each provider has:
|
||||||
|
- `id()` — unique string id.
|
||||||
|
- `render(area, buf)` — called every frame.
|
||||||
|
- `handle_event(event) -> (EventResult, Vec<Command>)` — input handling plus explicit commands back to the app.
|
||||||
|
- `focus_changed`, `hide`, `show`, `shutdown` — lifecycle hooks.
|
||||||
|
- `can_open(source, mime_hint) -> i32` — bidding score; higher wins when the app needs to present content.
|
||||||
|
- `as_any` / `as_any_mut` — implemented so the app can downcast to concrete types (e.g. to read editor cursor state for the status bar).
|
||||||
|
|
||||||
|
2. **`Command` pattern** — Providers do not call system APIs directly. They return `Vec<Command>` from `handle_event`. The `App` in `main.rs` is the sole dispatcher. Commands are:
|
||||||
|
- `Present { panel, source, mime_hint }` — let providers bid for the source.
|
||||||
|
- `Launch { panel, tool_id, params }` — direct dispatch by provider id.
|
||||||
|
- `FocusPanel(panel)` — change focus.
|
||||||
|
- `ShowMessage { msg, level }` — show a transient status message.
|
||||||
|
|
||||||
|
3. **`ModuleRegistry`** — Holds provider factories, tool handlers, menu items, and status widgets. Provider instances are created lazily via `ensure_instance(id)`. It also implements `Registrar`, which is the interface exposed during plugin bootstrap.
|
||||||
|
|
||||||
|
4. **Tool vs Provider lifecycle** — Tools are registered once at plugin bootstrap and live until unload. Providers are created/destroyed per panel instance. This separation is intentional; see `doc/Plugin-Lifecycle-Tool-vs-Provider.md`.
|
||||||
|
|
||||||
|
5. **Built-in vs External** — Built-in modules implement Runtime traits directly. External modules are planned to be loaded by `plugin::loader` and wrapped by `plugin::translator` into `Box<dyn PanelProvider>` / `Box<dyn Tool>`.
|
||||||
|
|
||||||
|
## Current Built-in Providers
|
||||||
|
|
||||||
|
| Id | Panel | Purpose |
|
||||||
|
|---|---|---|
|
||||||
|
| `filetree` | Sidebar | Project file navigator; emits `Command::Present` on Enter to open files in Main. |
|
||||||
|
| `editor` | Main | Simple text editor with line numbers, basic Rust highlighting, and cursor movement. |
|
||||||
|
| `terminal` | Main | Mock integrated terminal with command history (`help`, `clear`, `pwd`, `ls`, `agents`, `version`). |
|
||||||
|
| `agentchat` | Agent | Chat UI with backend selector (Claude / GPT-4 / Local), message scrolling, and input. |
|
||||||
|
| `tasktree` | Main | Visual stub of agent task lists. |
|
||||||
|
|
||||||
|
Default active providers are hard-coded in `main.rs`: Sidebar = `filetree`, Main = `editor`, Agent = `agentchat`.
|
||||||
|
|
||||||
|
## Agent Framework
|
||||||
|
|
||||||
|
The agent code is intentionally lightweight and not yet connected to an LLM:
|
||||||
|
|
||||||
|
- `agent::roles::builtin_roles()` defines four roles (`secretary`, `driver`, `negfeedback`, `executor`) with icons, colors, system prompts, and allowed tools.
|
||||||
|
- `agent::tools::ToolInvoker` is a stub that returns `ToolOutput::err("not implemented")`.
|
||||||
|
- `agent::orchestrator::Orchestrator`, `agent::memory::MemorySystem` are also stubs.
|
||||||
|
- The `Tool` trait lives in `runtime::traits` and is the interface agents will call via LLM function calling.
|
||||||
|
|
||||||
|
When you add LLM integration, the expected flow is:
|
||||||
|
|
||||||
|
1. Agent decides to call a tool.
|
||||||
|
2. `ToolInvoker` looks up the tool by name in `ModuleRegistry::tools`.
|
||||||
|
3. `Tool::execute()` runs and returns `ToolOutput`.
|
||||||
|
4. Result goes back into the agent conversation loop.
|
||||||
|
|
||||||
|
## Code Style Guidelines
|
||||||
|
|
||||||
|
- Follow the existing module structure: one folder per subsystem, `mod.rs` + focused files.
|
||||||
|
- Implement `PanelProvider` using the same pattern as the built-ins:
|
||||||
|
- Implement `as_any` / `as_any_mut` with `self`.
|
||||||
|
- Return `(EventResult, Vec<Command>)` from `handle_event`.
|
||||||
|
- Provide lifecycle methods (`focus_changed`, `hide`, `show`, `shutdown`).
|
||||||
|
- Keep `main.rs` as the single dispatcher for `Command`. Do not re-introduce a `ProviderCtx`-style black-box router.
|
||||||
|
- Use `anyhow::Result` for application-level error handling. `thiserror` is a dependency but not yet used heavily.
|
||||||
|
- Prefer explicit TODO comments for missing functionality; many already exist. Do not delete them unless you actually implement the feature.
|
||||||
|
- Avoid adding heavy new dependencies without updating `TECH_STACK.md` and this file.
|
||||||
|
|
||||||
|
## Testing Instructions
|
||||||
|
|
||||||
|
- Add unit tests inside `#[cfg(test)] mod tests;` in the relevant module. Reference the existing test modules for style.
|
||||||
|
- Use `crossterm::event::{Event, KeyCode, KeyEvent}` to simulate keyboard input in tests.
|
||||||
|
- Assert on `EventResult::Consumed` vs `EventResult::PassThrough` and on emitted `Command`s where appropriate.
|
||||||
|
- Run `cargo test` before finishing any change. The current baseline is 23 passed tests.
|
||||||
|
|
||||||
|
## Security Considerations
|
||||||
|
|
||||||
|
- There are no secrets, API keys, or credentials in this repository.
|
||||||
|
- Future AI provider API keys are planned to live in `~/.config/everjoy/config.toml` (not yet implemented). Do not commit keys.
|
||||||
|
- Tool execution is intended to be guarded by `auto_execute_tools = false` in user config; dangerous operations should require user confirmation. Respect this design if you wire up tool execution.
|
||||||
|
- External plugins are designed to be sandboxed via WASM / C-ABI translation with panic isolation. Until the loader and translator are implemented, no external plugins are actually loaded.
|
||||||
|
- The terminal provider is currently a mock shell. If you replace it with real `tokio::process::Command` execution, sanitize inputs and require explicit user approval before running commands.
|
||||||
|
|
||||||
|
## Useful References
|
||||||
|
|
||||||
|
- `doc/Purpose.MD` — Why the project exists.
|
||||||
|
- `doc/Architecture.md` — High-level architecture and data flows.
|
||||||
|
- `doc/Architecture-Runtime-and-Plugin.md` — Runtime vs Plugin subsystem responsibilities.
|
||||||
|
- `doc/Plugin-Lifecycle-Tool-vs-Provider.md` — Why tools and providers have separate lifecycles.
|
||||||
|
- `TECH_STACK.md` — Dependency map and design decisions (note: some entries are aspirational).
|
||||||
|
|
||||||
|
*Have fun building Everjoy. — For Hoshu Chiu & Everjoy Chiu 🐕*
|
||||||
@@ -1,3 +1,7 @@
|
|||||||
|
// Copyright (c) 2026 Hoshu Chiu, Hochanglo Chiu 🐕, and The Goodest Boy/Girl
|
||||||
|
// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||||
|
// Everjoy loves treats, walks, and clean Rust code.
|
||||||
|
|
||||||
/// Short-term and long-term memory for agents.
|
/// Short-term and long-term memory for agents.
|
||||||
pub struct MemorySystem;
|
pub struct MemorySystem;
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
// Copyright (c) 2026 Hoshu Chiu, Hochanglo Chiu 🐕, and The Goodest Boy/Girl
|
||||||
|
// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||||
|
// Everjoy loves treats, walks, and clean Rust code.
|
||||||
|
|
||||||
pub mod memory;
|
pub mod memory;
|
||||||
pub mod orchestrator;
|
pub mod orchestrator;
|
||||||
pub mod roles;
|
pub mod roles;
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
// Copyright (c) 2026 Hoshu Chiu, Hochanglo Chiu 🐕, and The Goodest Boy/Girl
|
||||||
|
// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||||
|
// Everjoy loves treats, walks, and clean Rust code.
|
||||||
|
|
||||||
/// Coordinates multiple agents and dispatches tasks.
|
/// Coordinates multiple agents and dispatches tasks.
|
||||||
pub struct Orchestrator;
|
pub struct Orchestrator;
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
// Copyright (c) 2026 Hoshu Chiu, Hochanglo Chiu 🐕, and The Goodest Boy/Girl
|
||||||
|
// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||||
|
// Everjoy loves treats, walks, and clean Rust code.
|
||||||
|
|
||||||
use ratatui::style::Color;
|
use ratatui::style::Color;
|
||||||
|
|
||||||
/// Agent role definition. Managed by core system, not plugins.
|
/// Agent role definition. Managed by core system, not plugins.
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
// Copyright (c) 2026 Hoshu Chiu, Hochanglo Chiu 🐕, and The Goodest Boy/Girl
|
||||||
|
// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||||
|
// Everjoy loves treats, walks, and clean Rust code.
|
||||||
|
|
||||||
use crate::runtime::{ToolInput, ToolOutput};
|
use crate::runtime::{ToolInput, ToolOutput};
|
||||||
|
|
||||||
/// Invokes tools on behalf of agents via LLM function calling.
|
/// Invokes tools on behalf of agents via LLM function calling.
|
||||||
|
|||||||
71
src/app/commands.rs
Normal file
71
src/app/commands.rs
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
// Copyright (c) 2026 Hoshu Chiu, Hochanglo Chiu 🐕, and The Goodest Boy/Girl
|
||||||
|
// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||||
|
// Everjoy loves treats, walks, and clean Rust code.
|
||||||
|
|
||||||
|
use ratatui::style::Color;
|
||||||
|
|
||||||
|
use crate::runtime::{Command, MessageLevel};
|
||||||
|
|
||||||
|
use super::App;
|
||||||
|
|
||||||
|
impl App {
|
||||||
|
pub(crate) fn dispatch_command(&mut self, cmd: Command) {
|
||||||
|
match cmd {
|
||||||
|
Command::Present {
|
||||||
|
panel,
|
||||||
|
source,
|
||||||
|
mime_hint,
|
||||||
|
} => {
|
||||||
|
let guessed = source.mime_hint();
|
||||||
|
let mime_str = mime_hint.as_deref().or(guessed.as_deref());
|
||||||
|
if let Some((id, _score)) =
|
||||||
|
self.registry.find_best_provider(panel, &source, mime_str)
|
||||||
|
{
|
||||||
|
// Hide current provider on this panel if different
|
||||||
|
if let Some(old_id) = self.active_providers.get(&panel) {
|
||||||
|
if old_id != &id {
|
||||||
|
if let Some(inst) = self.registry.ensure_instance(old_id) {
|
||||||
|
inst.hide();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.active_providers.insert(panel, id.clone());
|
||||||
|
if let Some(inst) = self.registry.ensure_instance(&id) {
|
||||||
|
inst.show();
|
||||||
|
if self.focus.panel == panel {
|
||||||
|
inst.focus_changed(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Command::Launch { panel, tool_id, .. } => {
|
||||||
|
// Direct dispatch by provider id
|
||||||
|
if let Some(old_id) = self.active_providers.get(&panel) {
|
||||||
|
if old_id != &tool_id {
|
||||||
|
if let Some(inst) = self.registry.ensure_instance(old_id) {
|
||||||
|
inst.hide();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.active_providers.insert(panel, tool_id.clone());
|
||||||
|
if let Some(inst) = self.registry.ensure_instance(&tool_id) {
|
||||||
|
inst.show();
|
||||||
|
if self.focus.panel == panel {
|
||||||
|
inst.focus_changed(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Command::FocusPanel(panel) => {
|
||||||
|
self.set_focus(panel);
|
||||||
|
}
|
||||||
|
Command::ShowMessage { msg, level } => {
|
||||||
|
let color = match level {
|
||||||
|
MessageLevel::Info => Color::Cyan,
|
||||||
|
MessageLevel::Warn => Color::Yellow,
|
||||||
|
MessageLevel::Error => Color::Red,
|
||||||
|
};
|
||||||
|
self.statusbar.message = Some((msg, color));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
266
src/app/core.rs
Normal file
266
src/app/core.rs
Normal file
@@ -0,0 +1,266 @@
|
|||||||
|
// Copyright (c) 2026 Hoshu Chiu, Hochanglo Chiu 🐕, and The Goodest Boy/Girl
|
||||||
|
// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||||
|
// Everjoy loves treats, walks, and clean Rust code.
|
||||||
|
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::io;
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
use crossterm::event::{self};
|
||||||
|
use ratatui::{
|
||||||
|
backend::CrosstermBackend,
|
||||||
|
layout::{Constraint, Direction, Layout, Rect},
|
||||||
|
style::Color,
|
||||||
|
widgets::Widget,
|
||||||
|
Terminal,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::builtin;
|
||||||
|
use crate::runtime::{
|
||||||
|
Capability, ModuleRegistry, PanelType, ProviderDesc, Registrar,
|
||||||
|
};
|
||||||
|
use crate::ui;
|
||||||
|
|
||||||
|
pub(crate) enum AppState {
|
||||||
|
Splash,
|
||||||
|
Main,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct App {
|
||||||
|
pub(crate) state: AppState,
|
||||||
|
pub(crate) layout: ui::LayoutManager,
|
||||||
|
pub(crate) focus: ui::focus::FocusManager,
|
||||||
|
pub(crate) menubar: ui::menubar::Menubar,
|
||||||
|
pub(crate) statusbar: ui::statusbar::StatusBar,
|
||||||
|
pub(crate) splash: ui::splash::SplashScreen,
|
||||||
|
pub(crate) registry: ModuleRegistry,
|
||||||
|
/// Current active provider id per panel.
|
||||||
|
pub(crate) active_providers: HashMap<PanelType, String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl App {
|
||||||
|
/// `App::new()`
|
||||||
|
///
|
||||||
|
/// App构造函数依次完成以下工作:
|
||||||
|
/// - 注册 `builtin providers`
|
||||||
|
/// - 初始化所有providers
|
||||||
|
pub fn new() -> Self {
|
||||||
|
let mut registry = ModuleRegistry::default();
|
||||||
|
|
||||||
|
// Register built-in providers
|
||||||
|
registry.register_provider(
|
||||||
|
ProviderDesc {
|
||||||
|
id: "filetree".into(),
|
||||||
|
name: "File Tree".into(),
|
||||||
|
panel: PanelType::Sidebar,
|
||||||
|
capabilities: vec![Capability::Render, Capability::HandleEvent],
|
||||||
|
},
|
||||||
|
Box::new(|| Box::new(builtin::filetree::FileTreeProvider::new())),
|
||||||
|
);
|
||||||
|
registry.register_provider(
|
||||||
|
ProviderDesc {
|
||||||
|
id: "editor".into(),
|
||||||
|
name: "Editor".into(),
|
||||||
|
panel: PanelType::Main,
|
||||||
|
capabilities: vec![Capability::Render, Capability::HandleEvent],
|
||||||
|
},
|
||||||
|
Box::new(|| Box::new(builtin::editor::EditorProvider::new())),
|
||||||
|
);
|
||||||
|
registry.register_provider(
|
||||||
|
ProviderDesc {
|
||||||
|
id: "terminal".into(),
|
||||||
|
name: "Terminal".into(),
|
||||||
|
panel: PanelType::Main,
|
||||||
|
capabilities: vec![Capability::Render, Capability::HandleEvent],
|
||||||
|
},
|
||||||
|
Box::new(|| Box::new(builtin::terminal::TerminalProvider::new())),
|
||||||
|
);
|
||||||
|
registry.register_provider(
|
||||||
|
ProviderDesc {
|
||||||
|
id: "agentchat".into(),
|
||||||
|
name: "Agent Chat".into(),
|
||||||
|
panel: PanelType::Agent,
|
||||||
|
capabilities: vec![Capability::Render, Capability::HandleEvent],
|
||||||
|
},
|
||||||
|
Box::new(|| Box::new(builtin::agentchat::AgentChatProvider::new())),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Default active providers
|
||||||
|
let mut active_providers: HashMap<PanelType, String> = HashMap::new();
|
||||||
|
active_providers.insert(PanelType::Sidebar, "filetree".into());
|
||||||
|
active_providers.insert(PanelType::Main, "terminal".into());
|
||||||
|
active_providers.insert(PanelType::Agent, "agentchat".into());
|
||||||
|
|
||||||
|
// Ensure all default providers are instantiated
|
||||||
|
for id in active_providers.values() {
|
||||||
|
let _ = registry.ensure_instance(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
Self {
|
||||||
|
state: AppState::Splash,
|
||||||
|
layout: ui::LayoutManager::default(),
|
||||||
|
focus: ui::focus::FocusManager::default(),
|
||||||
|
menubar: ui::menubar::Menubar::default(),
|
||||||
|
statusbar: ui::statusbar::StatusBar::default(),
|
||||||
|
splash: ui::splash::SplashScreen,
|
||||||
|
registry,
|
||||||
|
active_providers,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `App::run`
|
||||||
|
///
|
||||||
|
/// 事务处理循环:
|
||||||
|
/// - 更新状态栏
|
||||||
|
///
|
||||||
|
pub fn run(
|
||||||
|
&mut self,
|
||||||
|
terminal: &mut Terminal<CrosstermBackend<io::Stdout>>,
|
||||||
|
) -> Result<()> {
|
||||||
|
loop {
|
||||||
|
self.update_statusbar();
|
||||||
|
terminal.draw(|frame| self.render(frame.area(), frame.buffer_mut()))?;
|
||||||
|
|
||||||
|
if event::poll(std::time::Duration::from_millis(16))? {
|
||||||
|
let ev = event::read()?;
|
||||||
|
if self.handle_event(ev) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn shutdown(&mut self) {
|
||||||
|
self.registry.shutdown_all();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn update_statusbar(&mut self) {
|
||||||
|
// Pull state from the active Main provider if it's an editor
|
||||||
|
if let Some(id) = self.active_providers.get(&PanelType::Main) {
|
||||||
|
if let Some(inst) = self.registry.ensure_instance(id) {
|
||||||
|
if let Some(editor) =
|
||||||
|
inst.as_any().downcast_ref::<builtin::editor::EditorProvider>()
|
||||||
|
{
|
||||||
|
self.statusbar.filename = editor.filename().to_string();
|
||||||
|
self.statusbar.cursor_line = editor.cursor_line();
|
||||||
|
self.statusbar.cursor_col = editor.cursor_col();
|
||||||
|
if editor.is_modified() {
|
||||||
|
self.statusbar.filename.push_str(" [+]");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Focus info
|
||||||
|
self.statusbar.focused_panel = match self.focus.panel {
|
||||||
|
PanelType::Sidebar => "Sidebar".into(),
|
||||||
|
PanelType::Main => "Main".into(),
|
||||||
|
PanelType::Agent => "Agent".into(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn render(&mut self, area: Rect, buf: &mut ratatui::buffer::Buffer) {
|
||||||
|
match self.state {
|
||||||
|
AppState::Splash => {
|
||||||
|
self.splash.render(area, buf);
|
||||||
|
}
|
||||||
|
AppState::Main => {
|
||||||
|
self.render_main(area, buf);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn render_main(&mut self, area: Rect, buf: &mut ratatui::buffer::Buffer) {
|
||||||
|
let vertical = Layout::default()
|
||||||
|
.direction(Direction::Vertical)
|
||||||
|
.constraints([
|
||||||
|
Constraint::Length(1),
|
||||||
|
Constraint::Min(0),
|
||||||
|
Constraint::Length(1),
|
||||||
|
])
|
||||||
|
.split(area);
|
||||||
|
|
||||||
|
let menubar_area = vertical[0];
|
||||||
|
let body_area = vertical[1];
|
||||||
|
let status_area = vertical[2];
|
||||||
|
|
||||||
|
self.menubar.render(menubar_area, buf);
|
||||||
|
|
||||||
|
let panels = self.layout.compute(body_area);
|
||||||
|
|
||||||
|
let sidebar_title = match self.focus.panel {
|
||||||
|
PanelType::Sidebar => " Sidebar * ",
|
||||||
|
_ => " Sidebar ",
|
||||||
|
};
|
||||||
|
let main_title = match self.focus.panel {
|
||||||
|
PanelType::Main => " Main * ",
|
||||||
|
_ => " Main ",
|
||||||
|
};
|
||||||
|
let agent_title = match self.focus.panel {
|
||||||
|
PanelType::Agent => " Agent * ",
|
||||||
|
_ => " Agent ",
|
||||||
|
};
|
||||||
|
|
||||||
|
self.draw_panel_frame(panels.sidebar, buf, sidebar_title);
|
||||||
|
self.draw_panel_frame(panels.main, buf, main_title);
|
||||||
|
self.draw_panel_frame(panels.agent, buf, agent_title);
|
||||||
|
|
||||||
|
let sidebar_inner = inner_rect(panels.sidebar);
|
||||||
|
let main_inner = inner_rect(panels.main);
|
||||||
|
let agent_inner = inner_rect(panels.agent);
|
||||||
|
|
||||||
|
// Fill panel backgrounds
|
||||||
|
for inner in [sidebar_inner, main_inner, agent_inner] {
|
||||||
|
for y in inner.top()..inner.bottom() {
|
||||||
|
for x in inner.left()..inner.right() {
|
||||||
|
buf[(x, y)].set_bg(Color::Rgb(25, 25, 25));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.render_provider(PanelType::Sidebar, sidebar_inner, buf);
|
||||||
|
self.render_provider(PanelType::Main, main_inner, buf);
|
||||||
|
self.render_provider(PanelType::Agent, agent_inner, buf);
|
||||||
|
|
||||||
|
self.statusbar.render(status_area, buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn render_provider(
|
||||||
|
&mut self,
|
||||||
|
panel: PanelType,
|
||||||
|
area: Rect,
|
||||||
|
buf: &mut ratatui::buffer::Buffer,
|
||||||
|
) {
|
||||||
|
if let Some(id) = self.active_providers.get(&panel) {
|
||||||
|
if let Some(inst) = self.registry.ensure_instance(id) {
|
||||||
|
inst.render(area, buf);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn draw_panel_frame(
|
||||||
|
&self,
|
||||||
|
area: Rect,
|
||||||
|
buf: &mut ratatui::buffer::Buffer,
|
||||||
|
title: &str,
|
||||||
|
) {
|
||||||
|
use ratatui::style::{Color, Style};
|
||||||
|
use ratatui::widgets::{Block, Borders};
|
||||||
|
|
||||||
|
let block = Block::default()
|
||||||
|
.title(title)
|
||||||
|
.borders(Borders::ALL)
|
||||||
|
.border_style(Style::default().fg(Color::DarkGray));
|
||||||
|
block.render(area, buf);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn inner_rect(area: Rect) -> Rect {
|
||||||
|
Rect {
|
||||||
|
x: area.x.saturating_add(1),
|
||||||
|
y: area.y.saturating_add(1),
|
||||||
|
width: area.width.saturating_sub(2),
|
||||||
|
height: area.height.saturating_sub(2),
|
||||||
|
}
|
||||||
|
}
|
||||||
112
src/app/events.rs
Normal file
112
src/app/events.rs
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
// Copyright (c) 2026 Hoshu Chiu, Hochanglo Chiu 🐕, and The Goodest Boy/Girl
|
||||||
|
// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||||
|
// Everjoy loves treats, walks, and clean Rust code.
|
||||||
|
|
||||||
|
use crossterm::event::{Event, KeyCode, KeyModifiers};
|
||||||
|
|
||||||
|
use crate::runtime::{EventResult, PanelType};
|
||||||
|
|
||||||
|
use super::App;
|
||||||
|
|
||||||
|
impl App {
|
||||||
|
pub(crate) fn handle_event(&mut self, ev: Event) -> bool {
|
||||||
|
if let Event::Key(key) = &ev {
|
||||||
|
if key.code == KeyCode::Char('q') && key.modifiers.contains(KeyModifiers::CONTROL) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
match self.state {
|
||||||
|
super::AppState::Splash => {
|
||||||
|
if let Event::Key(_) = ev {
|
||||||
|
self.state = super::AppState::Main;
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
super::AppState::Main => self.handle_main_event(ev),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn handle_main_event(&mut self, ev: Event) -> bool {
|
||||||
|
// Menubar
|
||||||
|
if self.menubar.is_active() {
|
||||||
|
if self.menubar.handle_event(&ev) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else if let Event::Key(key) = &ev {
|
||||||
|
if key.modifiers.contains(KeyModifiers::ALT) {
|
||||||
|
if self.menubar.handle_event(&ev) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Focus shortcuts
|
||||||
|
if let Event::Key(key) = &ev {
|
||||||
|
if key.modifiers.contains(KeyModifiers::CONTROL) {
|
||||||
|
match key.code {
|
||||||
|
KeyCode::Char('j') => {
|
||||||
|
self.set_focus(PanelType::Sidebar);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
KeyCode::Char('k') => {
|
||||||
|
self.set_focus(PanelType::Main);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
KeyCode::Char('l') => {
|
||||||
|
self.set_focus(PanelType::Agent);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Route to focused provider
|
||||||
|
let focused = self.focus.panel;
|
||||||
|
let commands = if let Some(id) = self.active_providers.get(&focused) {
|
||||||
|
if let Some(inst) = self.registry.ensure_instance(id) {
|
||||||
|
let (result, cmds) = inst.handle_event(ev.clone());
|
||||||
|
if matches!(result, EventResult::PassThrough) {
|
||||||
|
if let Event::Key(key) = &ev {
|
||||||
|
if key.modifiers.contains(KeyModifiers::ALT) {
|
||||||
|
let _ = self.menubar.handle_event(&ev);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cmds
|
||||||
|
} else {
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Vec::new()
|
||||||
|
};
|
||||||
|
|
||||||
|
for cmd in commands {
|
||||||
|
self.dispatch_command(cmd);
|
||||||
|
}
|
||||||
|
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn set_focus(&mut self, panel: PanelType) {
|
||||||
|
let old = self.focus.panel;
|
||||||
|
if old == panel {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self.focus.focus_panel(panel);
|
||||||
|
|
||||||
|
// Notify old provider
|
||||||
|
if let Some(id) = self.active_providers.get(&old) {
|
||||||
|
if let Some(inst) = self.registry.ensure_instance(id) {
|
||||||
|
inst.focus_changed(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Notify new provider
|
||||||
|
if let Some(id) = self.active_providers.get(&panel) {
|
||||||
|
if let Some(inst) = self.registry.ensure_instance(id) {
|
||||||
|
inst.focus_changed(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
10
src/app/mod.rs
Normal file
10
src/app/mod.rs
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
// Copyright (c) 2026 Hoshu Chiu, Hochanglo Chiu 🐕, and The Goodest Boy/Girl
|
||||||
|
// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||||
|
// Everjoy loves treats, walks, and clean Rust code.
|
||||||
|
|
||||||
|
pub mod commands;
|
||||||
|
pub mod events;
|
||||||
|
pub mod core;
|
||||||
|
|
||||||
|
pub use core::App;
|
||||||
|
pub(crate) use core::AppState;
|
||||||
@@ -1,3 +1,7 @@
|
|||||||
|
// Copyright (c) 2026 Hoshu Chiu, Hochanglo Chiu 🐕, and The Goodest Boy/Girl
|
||||||
|
// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||||
|
// Everjoy loves treats, walks, and clean Rust code.
|
||||||
|
|
||||||
use crate::runtime::{Command, EventResult, PanelProvider, Source};
|
use crate::runtime::{Command, EventResult, PanelProvider, Source};
|
||||||
use crossterm::event::{Event, KeyCode, KeyModifiers};
|
use crossterm::event::{Event, KeyCode, KeyModifiers};
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
// Copyright (c) 2026 Hoshu Chiu, Hochanglo Chiu 🐕, and The Goodest Boy/Girl
|
||||||
|
// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||||
|
// Everjoy loves treats, walks, and clean Rust code.
|
||||||
|
|
||||||
use crate::builtin::agentchat::AgentChatProvider;
|
use crate::builtin::agentchat::AgentChatProvider;
|
||||||
use crate::runtime::PanelProvider;
|
use crate::runtime::PanelProvider;
|
||||||
use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers};
|
use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers};
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
// Copyright (c) 2026 Hoshu Chiu, Hochanglo Chiu 🐕, and The Goodest Boy/Girl
|
||||||
|
// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||||
|
// Everjoy loves treats, walks, and clean Rust code.
|
||||||
|
|
||||||
use crate::runtime::{Command, EventResult, PanelProvider, Source};
|
use crate::runtime::{Command, EventResult, PanelProvider, Source};
|
||||||
use crossterm::event::{Event, KeyCode, KeyModifiers};
|
use crossterm::event::{Event, KeyCode, KeyModifiers};
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
// Copyright (c) 2026 Hoshu Chiu, Hochanglo Chiu 🐕, and The Goodest Boy/Girl
|
||||||
|
// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||||
|
// Everjoy loves treats, walks, and clean Rust code.
|
||||||
|
|
||||||
use crate::builtin::editor::EditorProvider;
|
use crate::builtin::editor::EditorProvider;
|
||||||
use crate::runtime::PanelProvider;
|
use crate::runtime::PanelProvider;
|
||||||
use crossterm::event::{Event, KeyCode, KeyEvent};
|
use crossterm::event::{Event, KeyCode, KeyEvent};
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
// Copyright (c) 2026 Hoshu Chiu, Hochanglo Chiu 🐕, and The Goodest Boy/Girl
|
||||||
|
// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||||
|
// Everjoy loves treats, walks, and clean Rust code.
|
||||||
|
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
// Copyright (c) 2026 Hoshu Chiu, Hochanglo Chiu 🐕, and The Goodest Boy/Girl
|
||||||
|
// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||||
|
// Everjoy loves treats, walks, and clean Rust code.
|
||||||
|
|
||||||
use crate::builtin::filetree::FileTreeProvider;
|
use crate::builtin::filetree::FileTreeProvider;
|
||||||
use crate::runtime::PanelProvider;
|
use crate::runtime::PanelProvider;
|
||||||
use crossterm::event::{Event, KeyCode, KeyEvent};
|
use crossterm::event::{Event, KeyCode, KeyEvent};
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
// Copyright (c) 2026 Hoshu Chiu, Hochanglo Chiu 🐕, and The Goodest Boy/Girl
|
||||||
|
// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||||
|
// Everjoy loves treats, walks, and clean Rust code.
|
||||||
|
|
||||||
pub mod agentchat;
|
pub mod agentchat;
|
||||||
pub mod editor;
|
pub mod editor;
|
||||||
pub mod filetree;
|
pub mod filetree;
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
// Copyright (c) 2026 Hoshu Chiu, Hochanglo Chiu 🐕, and The Goodest Boy/Girl
|
||||||
|
// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||||
|
// Everjoy loves treats, walks, and clean Rust code.
|
||||||
|
|
||||||
use crate::runtime::{Command, EventResult, PanelProvider, Source};
|
use crate::runtime::{Command, EventResult, PanelProvider, Source};
|
||||||
use crossterm::event::Event;
|
use crossterm::event::Event;
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
// Copyright (c) 2026 Hoshu Chiu, Hochanglo Chiu 🐕, and The Goodest Boy/Girl
|
||||||
|
// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||||
|
// Everjoy loves treats, walks, and clean Rust code.
|
||||||
|
|
||||||
use crate::runtime::{Command, EventResult, PanelProvider, Source};
|
use crate::runtime::{Command, EventResult, PanelProvider, Source};
|
||||||
use crossterm::event::{Event, KeyCode};
|
use crossterm::event::{Event, KeyCode};
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
// Copyright (c) 2026 Hoshu Chiu, Hochanglo Chiu 🐕, and The Goodest Boy/Girl
|
||||||
|
// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||||
|
// Everjoy loves treats, walks, and clean Rust code.
|
||||||
|
|
||||||
use crate::builtin::terminal::TerminalProvider;
|
use crate::builtin::terminal::TerminalProvider;
|
||||||
use crate::runtime::PanelProvider;
|
use crate::runtime::PanelProvider;
|
||||||
use crossterm::event::{Event, KeyCode, KeyEvent};
|
use crossterm::event::{Event, KeyCode, KeyEvent};
|
||||||
|
|||||||
@@ -1,4 +1,9 @@
|
|||||||
|
// Copyright (c) 2026 Hoshu Chiu, Hochanglo Chiu 🐕, and The Goodest Boy/Girl
|
||||||
|
// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||||
|
// Everjoy loves treats, walks, and clean Rust code.
|
||||||
|
|
||||||
pub mod agent;
|
pub mod agent;
|
||||||
|
pub mod app;
|
||||||
pub mod builtin;
|
pub mod builtin;
|
||||||
pub mod plugin;
|
pub mod plugin;
|
||||||
pub mod runtime;
|
pub mod runtime;
|
||||||
|
|||||||
414
src/main.rs
414
src/main.rs
@@ -1,413 +1,39 @@
|
|||||||
use std::collections::HashMap;
|
// Copyright (c) 2026 Hoshu Chiu, Hochanglo Chiu 🐕, and The Goodest Boy/Girl
|
||||||
use std::io::{self, stdout};
|
// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||||
|
// Everjoy loves treats, walks, and clean Rust code.
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
use std::io::stdout;
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
|
||||||
|
// terminal library
|
||||||
use crossterm::{
|
use crossterm::{
|
||||||
event::{self, Event, KeyCode, KeyModifiers},
|
|
||||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||||
ExecutableCommand,
|
ExecutableCommand,
|
||||||
};
|
};
|
||||||
use ratatui::{
|
|
||||||
backend::CrosstermBackend,
|
|
||||||
layout::{Constraint, Direction, Layout, Rect},
|
|
||||||
style::Color,
|
|
||||||
widgets::Widget,
|
|
||||||
Terminal,
|
|
||||||
};
|
|
||||||
|
|
||||||
use everjoy::builtin;
|
// TUI library
|
||||||
use everjoy::runtime::{
|
use ratatui::{backend::CrosstermBackend, Terminal};
|
||||||
Capability, Command, EventResult, ModuleRegistry, PanelType, ProviderDesc, Registrar,
|
|
||||||
};
|
|
||||||
use everjoy::ui;
|
|
||||||
|
|
||||||
enum AppState {
|
|
||||||
Splash,
|
|
||||||
Main,
|
|
||||||
}
|
|
||||||
|
|
||||||
struct App {
|
|
||||||
state: AppState,
|
|
||||||
layout: ui::LayoutManager,
|
|
||||||
focus: ui::focus::FocusManager,
|
|
||||||
menubar: ui::menubar::Menubar,
|
|
||||||
statusbar: ui::statusbar::StatusBar,
|
|
||||||
splash: ui::splash::SplashScreen,
|
|
||||||
registry: ModuleRegistry,
|
|
||||||
/// Current active provider id per panel.
|
|
||||||
active_providers: HashMap<PanelType, String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl App {
|
|
||||||
fn new() -> Self {
|
|
||||||
let mut registry = ModuleRegistry::default();
|
|
||||||
|
|
||||||
// Register built-in providers
|
|
||||||
registry.register_provider(
|
|
||||||
ProviderDesc {
|
|
||||||
id: "filetree".into(),
|
|
||||||
name: "File Tree".into(),
|
|
||||||
panel: PanelType::Sidebar,
|
|
||||||
capabilities: vec![Capability::Render, Capability::HandleEvent],
|
|
||||||
},
|
|
||||||
Box::new(|| Box::new(builtin::filetree::FileTreeProvider::new())),
|
|
||||||
);
|
|
||||||
registry.register_provider(
|
|
||||||
ProviderDesc {
|
|
||||||
id: "editor".into(),
|
|
||||||
name: "Editor".into(),
|
|
||||||
panel: PanelType::Main,
|
|
||||||
capabilities: vec![Capability::Render, Capability::HandleEvent],
|
|
||||||
},
|
|
||||||
Box::new(|| Box::new(builtin::editor::EditorProvider::new())),
|
|
||||||
);
|
|
||||||
registry.register_provider(
|
|
||||||
ProviderDesc {
|
|
||||||
id: "terminal".into(),
|
|
||||||
name: "Terminal".into(),
|
|
||||||
panel: PanelType::Main,
|
|
||||||
capabilities: vec![Capability::Render, Capability::HandleEvent],
|
|
||||||
},
|
|
||||||
Box::new(|| Box::new(builtin::terminal::TerminalProvider::new())),
|
|
||||||
);
|
|
||||||
registry.register_provider(
|
|
||||||
ProviderDesc {
|
|
||||||
id: "agentchat".into(),
|
|
||||||
name: "Agent Chat".into(),
|
|
||||||
panel: PanelType::Agent,
|
|
||||||
capabilities: vec![Capability::Render, Capability::HandleEvent],
|
|
||||||
},
|
|
||||||
Box::new(|| Box::new(builtin::agentchat::AgentChatProvider::new())),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Default active providers
|
|
||||||
let mut active_providers: HashMap<PanelType, String> = HashMap::new();
|
|
||||||
active_providers.insert(PanelType::Sidebar, "filetree".into());
|
|
||||||
active_providers.insert(PanelType::Main, "editor".into());
|
|
||||||
active_providers.insert(PanelType::Agent, "agentchat".into());
|
|
||||||
|
|
||||||
// Ensure all default providers are instantiated
|
|
||||||
for id in active_providers.values() {
|
|
||||||
let _ = registry.ensure_instance(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
Self {
|
|
||||||
state: AppState::Splash,
|
|
||||||
layout: ui::LayoutManager::default(),
|
|
||||||
focus: ui::focus::FocusManager::default(),
|
|
||||||
menubar: ui::menubar::Menubar::default(),
|
|
||||||
statusbar: ui::statusbar::StatusBar::default(),
|
|
||||||
splash: ui::splash::SplashScreen,
|
|
||||||
registry,
|
|
||||||
active_providers,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn run(&mut self, terminal: &mut Terminal<CrosstermBackend<io::Stdout>>) -> Result<()> {
|
|
||||||
loop {
|
|
||||||
self.update_statusbar();
|
|
||||||
terminal.draw(|frame| self.render(frame.area(), frame.buffer_mut()))?;
|
|
||||||
|
|
||||||
if event::poll(std::time::Duration::from_millis(16))? {
|
|
||||||
let ev = event::read()?;
|
|
||||||
if self.handle_event(ev) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn update_statusbar(&mut self) {
|
|
||||||
// Pull state from the active Main provider if it's an editor
|
|
||||||
if let Some(id) = self.active_providers.get(&PanelType::Main) {
|
|
||||||
if let Some(inst) = self.registry.ensure_instance(id) {
|
|
||||||
if let Some(editor) = inst.as_any().downcast_ref::<builtin::editor::EditorProvider>() {
|
|
||||||
self.statusbar.filename = editor.filename().to_string();
|
|
||||||
self.statusbar.cursor_line = editor.cursor_line();
|
|
||||||
self.statusbar.cursor_col = editor.cursor_col();
|
|
||||||
if editor.is_modified() {
|
|
||||||
self.statusbar.filename.push_str(" [+]");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Focus info
|
|
||||||
self.statusbar.focused_panel = match self.focus.panel {
|
|
||||||
PanelType::Sidebar => "Sidebar".into(),
|
|
||||||
PanelType::Main => "Main".into(),
|
|
||||||
PanelType::Agent => "Agent".into(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render(&mut self, area: Rect, buf: &mut ratatui::buffer::Buffer) {
|
|
||||||
match self.state {
|
|
||||||
AppState::Splash => {
|
|
||||||
self.splash.render(area, buf);
|
|
||||||
}
|
|
||||||
AppState::Main => {
|
|
||||||
self.render_main(area, buf);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render_main(&mut self, area: Rect, buf: &mut ratatui::buffer::Buffer) {
|
|
||||||
let vertical = Layout::default()
|
|
||||||
.direction(Direction::Vertical)
|
|
||||||
.constraints([
|
|
||||||
Constraint::Length(1),
|
|
||||||
Constraint::Min(0),
|
|
||||||
Constraint::Length(1),
|
|
||||||
])
|
|
||||||
.split(area);
|
|
||||||
|
|
||||||
let menubar_area = vertical[0];
|
|
||||||
let body_area = vertical[1];
|
|
||||||
let status_area = vertical[2];
|
|
||||||
|
|
||||||
self.menubar.render(menubar_area, buf);
|
|
||||||
|
|
||||||
let panels = self.layout.compute(body_area);
|
|
||||||
|
|
||||||
let sidebar_title = match self.focus.panel {
|
|
||||||
PanelType::Sidebar => " Sidebar * ",
|
|
||||||
_ => " Sidebar ",
|
|
||||||
};
|
|
||||||
let main_title = match self.focus.panel {
|
|
||||||
PanelType::Main => " Main * ",
|
|
||||||
_ => " Main ",
|
|
||||||
};
|
|
||||||
let agent_title = match self.focus.panel {
|
|
||||||
PanelType::Agent => " Agent * ",
|
|
||||||
_ => " Agent ",
|
|
||||||
};
|
|
||||||
|
|
||||||
self.draw_panel_frame(panels.sidebar, buf, sidebar_title);
|
|
||||||
self.draw_panel_frame(panels.main, buf, main_title);
|
|
||||||
self.draw_panel_frame(panels.agent, buf, agent_title);
|
|
||||||
|
|
||||||
let sidebar_inner = inner_rect(panels.sidebar);
|
|
||||||
let main_inner = inner_rect(panels.main);
|
|
||||||
let agent_inner = inner_rect(panels.agent);
|
|
||||||
|
|
||||||
// Fill panel backgrounds
|
|
||||||
for inner in [sidebar_inner, main_inner, agent_inner] {
|
|
||||||
for y in inner.top()..inner.bottom() {
|
|
||||||
for x in inner.left()..inner.right() {
|
|
||||||
buf[(x, y)].set_bg(Color::Rgb(25, 25, 25));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
self.render_provider(PanelType::Sidebar, sidebar_inner, buf);
|
|
||||||
self.render_provider(PanelType::Main, main_inner, buf);
|
|
||||||
self.render_provider(PanelType::Agent, agent_inner, buf);
|
|
||||||
|
|
||||||
self.statusbar.render(status_area, buf);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render_provider(&mut self, panel: PanelType, area: Rect, buf: &mut ratatui::buffer::Buffer) {
|
|
||||||
if let Some(id) = self.active_providers.get(&panel) {
|
|
||||||
if let Some(inst) = self.registry.ensure_instance(id) {
|
|
||||||
inst.render(area, buf);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn draw_panel_frame(&self, area: Rect, buf: &mut ratatui::buffer::Buffer, title: &str) {
|
|
||||||
use ratatui::style::{Color, Style};
|
|
||||||
use ratatui::widgets::{Block, Borders};
|
|
||||||
|
|
||||||
let block = Block::default()
|
|
||||||
.title(title)
|
|
||||||
.borders(Borders::ALL)
|
|
||||||
.border_style(Style::default().fg(Color::DarkGray));
|
|
||||||
block.render(area, buf);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn handle_event(&mut self, ev: Event) -> bool {
|
|
||||||
if let Event::Key(key) = &ev {
|
|
||||||
if key.code == KeyCode::Char('q') && key.modifiers.contains(KeyModifiers::CONTROL) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
match self.state {
|
|
||||||
AppState::Splash => {
|
|
||||||
if let Event::Key(_) = ev {
|
|
||||||
self.state = AppState::Main;
|
|
||||||
}
|
|
||||||
false
|
|
||||||
}
|
|
||||||
AppState::Main => self.handle_main_event(ev),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn handle_main_event(&mut self, ev: Event) -> bool {
|
|
||||||
// Menubar
|
|
||||||
if self.menubar.is_active() {
|
|
||||||
if self.menubar.handle_event(&ev) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
} else if let Event::Key(key) = &ev {
|
|
||||||
if key.modifiers.contains(KeyModifiers::ALT) {
|
|
||||||
if self.menubar.handle_event(&ev) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Focus shortcuts
|
|
||||||
if let Event::Key(key) = &ev {
|
|
||||||
if key.modifiers.contains(KeyModifiers::CONTROL) {
|
|
||||||
match key.code {
|
|
||||||
KeyCode::Char('j') => {
|
|
||||||
self.set_focus(PanelType::Sidebar);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
KeyCode::Char('k') => {
|
|
||||||
self.set_focus(PanelType::Main);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
KeyCode::Char('l') => {
|
|
||||||
self.set_focus(PanelType::Agent);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Route to focused provider
|
|
||||||
let focused = self.focus.panel;
|
|
||||||
let commands = if let Some(id) = self.active_providers.get(&focused) {
|
|
||||||
if let Some(inst) = self.registry.ensure_instance(id) {
|
|
||||||
let (result, cmds) = inst.handle_event(ev.clone());
|
|
||||||
if matches!(result, EventResult::PassThrough) {
|
|
||||||
if let Event::Key(key) = &ev {
|
|
||||||
if key.modifiers.contains(KeyModifiers::ALT) {
|
|
||||||
let _ = self.menubar.handle_event(&ev);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
cmds
|
|
||||||
} else {
|
|
||||||
Vec::new()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Vec::new()
|
|
||||||
};
|
|
||||||
|
|
||||||
for cmd in commands {
|
|
||||||
self.dispatch_command(cmd);
|
|
||||||
}
|
|
||||||
|
|
||||||
false
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set_focus(&mut self, panel: PanelType) {
|
|
||||||
let old = self.focus.panel;
|
|
||||||
if old == panel {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
self.focus.focus_panel(panel);
|
|
||||||
|
|
||||||
// Notify old provider
|
|
||||||
if let Some(id) = self.active_providers.get(&old) {
|
|
||||||
if let Some(inst) = self.registry.ensure_instance(id) {
|
|
||||||
inst.focus_changed(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Notify new provider
|
|
||||||
if let Some(id) = self.active_providers.get(&panel) {
|
|
||||||
if let Some(inst) = self.registry.ensure_instance(id) {
|
|
||||||
inst.focus_changed(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn dispatch_command(&mut self, cmd: Command) {
|
|
||||||
match cmd {
|
|
||||||
Command::Present {
|
|
||||||
panel,
|
|
||||||
source,
|
|
||||||
mime_hint,
|
|
||||||
} => {
|
|
||||||
let guessed = source.mime_hint();
|
|
||||||
let mime_str = mime_hint.as_deref().or(guessed.as_deref());
|
|
||||||
if let Some((id, _score)) =
|
|
||||||
self.registry.find_best_provider(panel, &source, mime_str)
|
|
||||||
{
|
|
||||||
// Hide current provider on this panel if different
|
|
||||||
if let Some(old_id) = self.active_providers.get(&panel) {
|
|
||||||
if old_id != &id {
|
|
||||||
if let Some(inst) = self.registry.ensure_instance(old_id) {
|
|
||||||
inst.hide();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
self.active_providers.insert(panel, id.clone());
|
|
||||||
if let Some(inst) = self.registry.ensure_instance(&id) {
|
|
||||||
inst.show();
|
|
||||||
if self.focus.panel == panel {
|
|
||||||
inst.focus_changed(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Command::Launch { panel, tool_id, .. } => {
|
|
||||||
// Direct dispatch by provider id
|
|
||||||
if let Some(old_id) = self.active_providers.get(&panel) {
|
|
||||||
if old_id != &tool_id {
|
|
||||||
if let Some(inst) = self.registry.ensure_instance(old_id) {
|
|
||||||
inst.hide();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
self.active_providers.insert(panel, tool_id.clone());
|
|
||||||
if let Some(inst) = self.registry.ensure_instance(&tool_id) {
|
|
||||||
inst.show();
|
|
||||||
if self.focus.panel == panel {
|
|
||||||
inst.focus_changed(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Command::FocusPanel(panel) => {
|
|
||||||
self.set_focus(panel);
|
|
||||||
}
|
|
||||||
Command::ShowMessage { msg, level } => {
|
|
||||||
let color = match level {
|
|
||||||
everjoy::runtime::MessageLevel::Info => Color::Cyan,
|
|
||||||
everjoy::runtime::MessageLevel::Warn => Color::Yellow,
|
|
||||||
everjoy::runtime::MessageLevel::Error => Color::Red,
|
|
||||||
};
|
|
||||||
self.statusbar.message = Some((msg, color));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn inner_rect(area: Rect) -> Rect {
|
|
||||||
Rect {
|
|
||||||
x: area.x.saturating_add(1),
|
|
||||||
y: area.y.saturating_add(1),
|
|
||||||
width: area.width.saturating_sub(2),
|
|
||||||
height: area.height.saturating_sub(2),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn main() -> Result<()> {
|
fn main() -> Result<()> {
|
||||||
|
// Passthrough all input and output to application.
|
||||||
enable_raw_mode()?;
|
enable_raw_mode()?;
|
||||||
|
|
||||||
|
// Goto a new Page.
|
||||||
stdout().execute(EnterAlternateScreen)?;
|
stdout().execute(EnterAlternateScreen)?;
|
||||||
|
|
||||||
let mut terminal = Terminal::new(CrosstermBackend::new(stdout()))?;
|
let mut terminal = Terminal::new(CrosstermBackend::new(stdout()))?;
|
||||||
|
|
||||||
let mut app = App::new();
|
// Launch app layer
|
||||||
|
let mut app = everjoy::app::App::new();
|
||||||
let result = app.run(&mut terminal);
|
let result = app.run(&mut terminal);
|
||||||
|
|
||||||
// Shutdown all providers before exit
|
app.shutdown();
|
||||||
app.registry.shutdown_all();
|
|
||||||
|
|
||||||
|
// Recover users' previous environment.
|
||||||
disable_raw_mode()?;
|
disable_raw_mode()?;
|
||||||
stdout().execute(LeaveAlternateScreen)?;
|
stdout().execute(LeaveAlternateScreen)?;
|
||||||
result
|
result
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
// Copyright (c) 2026 Hoshu Chiu, Hochanglo Chiu 🐕, and The Goodest Boy/Girl
|
||||||
|
// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||||
|
// Everjoy loves treats, walks, and clean Rust code.
|
||||||
|
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
/// Loads external plugins (.wasm, .so, .dll).
|
/// Loads external plugins (.wasm, .so, .dll).
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
// Copyright (c) 2026 Hoshu Chiu, Hochanglo Chiu 🐕, and The Goodest Boy/Girl
|
||||||
|
// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||||
|
// Everjoy loves treats, walks, and clean Rust code.
|
||||||
|
|
||||||
pub mod loader;
|
pub mod loader;
|
||||||
pub mod panic;
|
pub mod panic;
|
||||||
pub mod surface;
|
pub mod surface;
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
// Copyright (c) 2026 Hoshu Chiu, Hochanglo Chiu 🐕, and The Goodest Boy/Girl
|
||||||
|
// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||||
|
// Everjoy loves treats, walks, and clean Rust code.
|
||||||
|
|
||||||
/// Isolates panics from external plugins so they don't crash the runtime.
|
/// Isolates panics from external plugins so they don't crash the runtime.
|
||||||
pub fn catch_plugin_panic<F, R>(plugin_id: &str, f: F) -> Option<R>
|
pub fn catch_plugin_panic<F, R>(plugin_id: &str, f: F) -> Option<R>
|
||||||
where
|
where
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
// Copyright (c) 2026 Hoshu Chiu, Hochanglo Chiu 🐕, and The Goodest Boy/Girl
|
||||||
|
// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||||
|
// Everjoy loves treats, walks, and clean Rust code.
|
||||||
|
|
||||||
/// Cell grid for external plugin rendering.
|
/// Cell grid for external plugin rendering.
|
||||||
/// Runtime allocates, plugin borrows during render() call.
|
/// Runtime allocates, plugin borrows during render() call.
|
||||||
#[repr(C)]
|
#[repr(C)]
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
// Copyright (c) 2026 Hoshu Chiu, Hochanglo Chiu 🐕, and The Goodest Boy/Girl
|
||||||
|
// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||||
|
// Everjoy loves treats, walks, and clean Rust code.
|
||||||
|
|
||||||
/// Translates C-ABI calls to Rust trait objects.
|
/// Translates C-ABI calls to Rust trait objects.
|
||||||
/// Wraps external plugins into Runtime-compatible types.
|
/// Wraps external plugins into Runtime-compatible types.
|
||||||
pub struct Translator;
|
pub struct Translator;
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
// Copyright (c) 2026 Hoshu Chiu, Hochanglo Chiu 🐕, and The Goodest Boy/Girl
|
||||||
|
// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||||
|
// Everjoy loves treats, walks, and clean Rust code.
|
||||||
|
|
||||||
//! ProviderCtx has been removed in favor of the Command pattern.
|
//! ProviderCtx has been removed in favor of the Command pattern.
|
||||||
//! Providers return Commands from handle_event() instead of calling
|
//! Providers return Commands from handle_event() instead of calling
|
||||||
//! into the system via a context trait.
|
//! into the system via a context trait.
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
// Copyright (c) 2026 Hoshu Chiu, Hochanglo Chiu 🐕, and The Goodest Boy/Girl
|
||||||
|
// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||||
|
// Everjoy loves treats, walks, and clean Rust code.
|
||||||
|
|
||||||
/// Manages provider lifecycle: init, hide, show, shutdown.
|
/// Manages provider lifecycle: init, hide, show, shutdown.
|
||||||
pub struct LifecycleManager;
|
pub struct LifecycleManager;
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
// Copyright (c) 2026 Hoshu Chiu, Hochanglo Chiu 🐕, and The Goodest Boy/Girl
|
||||||
|
// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||||
|
// Everjoy loves treats, walks, and clean Rust code.
|
||||||
|
|
||||||
pub mod ctx;
|
pub mod ctx;
|
||||||
pub mod lifecycle;
|
pub mod lifecycle;
|
||||||
pub mod registry;
|
pub mod registry;
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
// Copyright (c) 2026 Hoshu Chiu, Hochanglo Chiu 🐕, and The Goodest Boy/Girl
|
||||||
|
// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||||
|
// Everjoy loves treats, walks, and clean Rust code.
|
||||||
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use ratatui::{buffer::Buffer, layout::Rect};
|
use ratatui::{buffer::Buffer, layout::Rect};
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
// Copyright (c) 2026 Hoshu Chiu, Hochanglo Chiu 🐕, and The Goodest Boy/Girl
|
||||||
|
// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||||
|
// Everjoy loves treats, walks, and clean Rust code.
|
||||||
|
|
||||||
use ratatui::{buffer::Buffer, layout::Rect};
|
use ratatui::{buffer::Buffer, layout::Rect};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
// Copyright (c) 2026 Hoshu Chiu, Hochanglo Chiu 🐕, and The Goodest Boy/Girl
|
||||||
|
// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||||
|
// Everjoy loves treats, walks, and clean Rust code.
|
||||||
|
|
||||||
use crate::runtime::PanelType;
|
use crate::runtime::PanelType;
|
||||||
|
|
||||||
/// Manages focus state across panels and widgets.
|
/// Manages focus state across panels and widgets.
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
// Copyright (c) 2026 Hoshu Chiu, Hochanglo Chiu 🐕, and The Goodest Boy/Girl
|
||||||
|
// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||||
|
// Everjoy loves treats, walks, and clean Rust code.
|
||||||
|
|
||||||
use ratatui::layout::{Constraint, Direction, Layout, Rect};
|
use ratatui::layout::{Constraint, Direction, Layout, Rect};
|
||||||
|
|
||||||
/// Manages panel proportions and handles drag-to-resize.
|
/// Manages panel proportions and handles drag-to-resize.
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
// Copyright (c) 2026 Hoshu Chiu, Hochanglo Chiu 🐕, and The Goodest Boy/Girl
|
||||||
|
// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||||
|
// Everjoy loves treats, walks, and clean Rust code.
|
||||||
|
|
||||||
use crossterm::event::{Event, KeyCode, KeyModifiers};
|
use crossterm::event::{Event, KeyCode, KeyModifiers};
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
buffer::Buffer,
|
buffer::Buffer,
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
// Copyright (c) 2026 Hoshu Chiu, Hochanglo Chiu 🐕, and The Goodest Boy/Girl
|
||||||
|
// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||||
|
// Everjoy loves treats, walks, and clean Rust code.
|
||||||
|
|
||||||
pub mod focus;
|
pub mod focus;
|
||||||
pub mod layout;
|
pub mod layout;
|
||||||
pub mod menubar;
|
pub mod menubar;
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
// Copyright (c) 2026 Hoshu Chiu, Hochanglo Chiu 🐕, and The Goodest Boy/Girl
|
||||||
|
// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||||
|
// Everjoy loves treats, walks, and clean Rust code.
|
||||||
|
|
||||||
use crossterm::event::Event;
|
use crossterm::event::Event;
|
||||||
|
|
||||||
/// Routes input events through the 3-layer system:
|
/// Routes input events through the 3-layer system:
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
// Copyright (c) 2026 Hoshu Chiu, Hochanglo Chiu 🐕, and The Goodest Boy/Girl
|
||||||
|
// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||||
|
// Everjoy loves treats, walks, and clean Rust code.
|
||||||
|
|
||||||
use ratatui::{backend::Backend, Frame, Terminal};
|
use ratatui::{backend::Backend, Frame, Terminal};
|
||||||
|
|
||||||
/// Schedules and executes the per-frame render.
|
/// Schedules and executes the per-frame render.
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
// Copyright (c) 2026 Hoshu Chiu, Hochanglo Chiu 🐕, and The Goodest Boy/Girl
|
||||||
|
// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||||
|
// Everjoy loves treats, walks, and clean Rust code.
|
||||||
|
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
buffer::Buffer,
|
buffer::Buffer,
|
||||||
layout::{Alignment, Constraint, Direction, Layout, Rect},
|
layout::{Alignment, Constraint, Direction, Layout, Rect},
|
||||||
@@ -64,7 +68,7 @@ impl Widget for &SplashScreen {
|
|||||||
subtitle.render(vertical[3], buf);
|
subtitle.render(vertical[3], buf);
|
||||||
|
|
||||||
// Version
|
// Version
|
||||||
let version = Paragraph::new("v0.1.0 — for Hoshu Chiu & Everjoy Chiu 🐕")
|
let version = Paragraph::new("v0.1.0 — for Hoshu Chiu & Hochanglo Chiu 🐕")
|
||||||
.alignment(Alignment::Center)
|
.alignment(Alignment::Center)
|
||||||
.style(Style::default().fg(Color::Rgb(120, 110, 100)));
|
.style(Style::default().fg(Color::Rgb(120, 110, 100)));
|
||||||
version.render(vertical[4], buf);
|
version.render(vertical[4], buf);
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
// Copyright (c) 2026 Hoshu Chiu, Hochanglo Chiu 🐕, and The Goodest Boy/Girl
|
||||||
|
// SPDX-License-Identifier: MIT OR Apache-2.0
|
||||||
|
// Everjoy loves treats, walks, and clean Rust code.
|
||||||
|
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
buffer::Buffer,
|
buffer::Buffer,
|
||||||
layout::Rect,
|
layout::Rect,
|
||||||
|
|||||||
Reference in New Issue
Block a user