diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..4471009 --- /dev/null +++ b/AGENTS.md @@ -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+` — 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)` — 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` 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` / `Box`. + +## 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)` 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 🐕* diff --git a/src/agent/memory.rs b/src/agent/memory.rs index deb947c..facf201 100644 --- a/src/agent/memory.rs +++ b/src/agent/memory.rs @@ -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. pub struct MemorySystem; diff --git a/src/agent/mod.rs b/src/agent/mod.rs index c16a96b..8b129ab 100644 --- a/src/agent/mod.rs +++ b/src/agent/mod.rs @@ -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 orchestrator; pub mod roles; diff --git a/src/agent/orchestrator.rs b/src/agent/orchestrator.rs index c0020f3..f679a0c 100644 --- a/src/agent/orchestrator.rs +++ b/src/agent/orchestrator.rs @@ -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. pub struct Orchestrator; diff --git a/src/agent/roles.rs b/src/agent/roles.rs index a109ccb..f262bb9 100644 --- a/src/agent/roles.rs +++ b/src/agent/roles.rs @@ -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; /// Agent role definition. Managed by core system, not plugins. diff --git a/src/agent/tools.rs b/src/agent/tools.rs index ecd334b..6cdfa16 100644 --- a/src/agent/tools.rs +++ b/src/agent/tools.rs @@ -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}; /// Invokes tools on behalf of agents via LLM function calling. diff --git a/src/app/commands.rs b/src/app/commands.rs new file mode 100644 index 0000000..866d2cb --- /dev/null +++ b/src/app/commands.rs @@ -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)); + } + } + } +} diff --git a/src/app/core.rs b/src/app/core.rs new file mode 100644 index 0000000..f23f46b --- /dev/null +++ b/src/app/core.rs @@ -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, +} + +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 = 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>, + ) -> 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::() + { + 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), + } +} diff --git a/src/app/events.rs b/src/app/events.rs new file mode 100644 index 0000000..11e014d --- /dev/null +++ b/src/app/events.rs @@ -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); + } + } + } +} diff --git a/src/app/mod.rs b/src/app/mod.rs new file mode 100644 index 0000000..58d9186 --- /dev/null +++ b/src/app/mod.rs @@ -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; diff --git a/src/builtin/agentchat/mod.rs b/src/builtin/agentchat/mod.rs index 9bfe8fe..8c8c4c6 100644 --- a/src/builtin/agentchat/mod.rs +++ b/src/builtin/agentchat/mod.rs @@ -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 crossterm::event::{Event, KeyCode, KeyModifiers}; use ratatui::{ diff --git a/src/builtin/agentchat/tests.rs b/src/builtin/agentchat/tests.rs index 25c7cc7..256e508 100644 --- a/src/builtin/agentchat/tests.rs +++ b/src/builtin/agentchat/tests.rs @@ -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::runtime::PanelProvider; use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers}; diff --git a/src/builtin/editor/mod.rs b/src/builtin/editor/mod.rs index 9778bc2..daa7a60 100644 --- a/src/builtin/editor/mod.rs +++ b/src/builtin/editor/mod.rs @@ -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 crossterm::event::{Event, KeyCode, KeyModifiers}; use ratatui::{ diff --git a/src/builtin/editor/tests.rs b/src/builtin/editor/tests.rs index a744b96..0f770d1 100644 --- a/src/builtin/editor/tests.rs +++ b/src/builtin/editor/tests.rs @@ -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::runtime::PanelProvider; use crossterm::event::{Event, KeyCode, KeyEvent}; diff --git a/src/builtin/filetree/mod.rs b/src/builtin/filetree/mod.rs index 7454a89..4b6115f 100644 --- a/src/builtin/filetree/mod.rs +++ b/src/builtin/filetree/mod.rs @@ -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::path::{Path, PathBuf}; diff --git a/src/builtin/filetree/tests.rs b/src/builtin/filetree/tests.rs index 36c03c3..aace4fe 100644 --- a/src/builtin/filetree/tests.rs +++ b/src/builtin/filetree/tests.rs @@ -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::runtime::PanelProvider; use crossterm::event::{Event, KeyCode, KeyEvent}; diff --git a/src/builtin/mod.rs b/src/builtin/mod.rs index 6ac48d9..5be22b8 100644 --- a/src/builtin/mod.rs +++ b/src/builtin/mod.rs @@ -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 editor; pub mod filetree; diff --git a/src/builtin/tasktree/mod.rs b/src/builtin/tasktree/mod.rs index 6fb1a9f..0e6b0b7 100644 --- a/src/builtin/tasktree/mod.rs +++ b/src/builtin/tasktree/mod.rs @@ -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 crossterm::event::Event; use ratatui::{ diff --git a/src/builtin/terminal/mod.rs b/src/builtin/terminal/mod.rs index 6dc50c0..4c91061 100644 --- a/src/builtin/terminal/mod.rs +++ b/src/builtin/terminal/mod.rs @@ -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 crossterm::event::{Event, KeyCode}; use ratatui::{ diff --git a/src/builtin/terminal/tests.rs b/src/builtin/terminal/tests.rs index 4015346..f3ba11f 100644 --- a/src/builtin/terminal/tests.rs +++ b/src/builtin/terminal/tests.rs @@ -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::runtime::PanelProvider; use crossterm::event::{Event, KeyCode, KeyEvent}; diff --git a/src/lib.rs b/src/lib.rs index 742c827..57fc73a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -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 app; pub mod builtin; pub mod plugin; pub mod runtime; diff --git a/src/main.rs b/src/main.rs index f9aa1a7..5bce325 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,413 +1,39 @@ -use std::collections::HashMap; -use std::io::{self, stdout}; +// 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::io::stdout; use anyhow::Result; + +// terminal library use crossterm::{ - event::{self, Event, KeyCode, KeyModifiers}, terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, ExecutableCommand, }; -use ratatui::{ - backend::CrosstermBackend, - layout::{Constraint, Direction, Layout, Rect}, - style::Color, - widgets::Widget, - Terminal, -}; -use everjoy::builtin; -use everjoy::runtime::{ - 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, -} - -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 = 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>) -> 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::() { - 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), - } -} +// TUI library +use ratatui::{backend::CrosstermBackend, Terminal}; fn main() -> Result<()> { + // Passthrough all input and output to application. enable_raw_mode()?; + + // Goto a new Page. stdout().execute(EnterAlternateScreen)?; + 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); - // Shutdown all providers before exit - app.registry.shutdown_all(); + app.shutdown(); + // Recover users' previous environment. disable_raw_mode()?; stdout().execute(LeaveAlternateScreen)?; result diff --git a/src/plugin/loader.rs b/src/plugin/loader.rs index 511fb26..312fc31 100644 --- a/src/plugin/loader.rs +++ b/src/plugin/loader.rs @@ -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}; /// Loads external plugins (.wasm, .so, .dll). diff --git a/src/plugin/mod.rs b/src/plugin/mod.rs index 7728b4b..b8b90d4 100644 --- a/src/plugin/mod.rs +++ b/src/plugin/mod.rs @@ -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 panic; pub mod surface; diff --git a/src/plugin/panic.rs b/src/plugin/panic.rs index f2b167b..7e74b1a 100644 --- a/src/plugin/panic.rs +++ b/src/plugin/panic.rs @@ -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. pub fn catch_plugin_panic(plugin_id: &str, f: F) -> Option where diff --git a/src/plugin/surface.rs b/src/plugin/surface.rs index d266c05..35c3b04 100644 --- a/src/plugin/surface.rs +++ b/src/plugin/surface.rs @@ -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. /// Runtime allocates, plugin borrows during render() call. #[repr(C)] diff --git a/src/plugin/translator.rs b/src/plugin/translator.rs index 181dde8..623f545 100644 --- a/src/plugin/translator.rs +++ b/src/plugin/translator.rs @@ -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. /// Wraps external plugins into Runtime-compatible types. pub struct Translator; diff --git a/src/runtime/ctx.rs b/src/runtime/ctx.rs index 9c9f75f..46501a2 100644 --- a/src/runtime/ctx.rs +++ b/src/runtime/ctx.rs @@ -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. //! Providers return Commands from handle_event() instead of calling //! into the system via a context trait. diff --git a/src/runtime/lifecycle.rs b/src/runtime/lifecycle.rs index d4dd0da..96379ec 100644 --- a/src/runtime/lifecycle.rs +++ b/src/runtime/lifecycle.rs @@ -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. pub struct LifecycleManager; diff --git a/src/runtime/mod.rs b/src/runtime/mod.rs index 87c92e8..a435cee 100644 --- a/src/runtime/mod.rs +++ b/src/runtime/mod.rs @@ -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 lifecycle; pub mod registry; diff --git a/src/runtime/registry.rs b/src/runtime/registry.rs index 36c32b4..d60aab6 100644 --- a/src/runtime/registry.rs +++ b/src/runtime/registry.rs @@ -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 ratatui::{buffer::Buffer, layout::Rect}; diff --git a/src/runtime/traits.rs b/src/runtime/traits.rs index 60f5bb4..9914b3a 100644 --- a/src/runtime/traits.rs +++ b/src/runtime/traits.rs @@ -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 std::collections::HashMap; use std::path::PathBuf; diff --git a/src/ui/focus.rs b/src/ui/focus.rs index 4d0cd59..32c7259 100644 --- a/src/ui/focus.rs +++ b/src/ui/focus.rs @@ -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; /// Manages focus state across panels and widgets. diff --git a/src/ui/layout.rs b/src/ui/layout.rs index 65ba985..5354e60 100644 --- a/src/ui/layout.rs +++ b/src/ui/layout.rs @@ -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}; /// Manages panel proportions and handles drag-to-resize. diff --git a/src/ui/menubar.rs b/src/ui/menubar.rs index d09cd43..6d6dd28 100644 --- a/src/ui/menubar.rs +++ b/src/ui/menubar.rs @@ -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 ratatui::{ buffer::Buffer, diff --git a/src/ui/mod.rs b/src/ui/mod.rs index e6c99c8..27bd187 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -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 layout; pub mod menubar; diff --git a/src/ui/router.rs b/src/ui/router.rs index a37b02f..339626a 100644 --- a/src/ui/router.rs +++ b/src/ui/router.rs @@ -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; /// Routes input events through the 3-layer system: diff --git a/src/ui/scheduler.rs b/src/ui/scheduler.rs index 42f42fa..155a176 100644 --- a/src/ui/scheduler.rs +++ b/src/ui/scheduler.rs @@ -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}; /// Schedules and executes the per-frame render. diff --git a/src/ui/splash.rs b/src/ui/splash.rs index 38ee9c6..92cfdfe 100644 --- a/src/ui/splash.rs +++ b/src/ui/splash.rs @@ -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::{Alignment, Constraint, Direction, Layout, Rect}, @@ -64,7 +68,7 @@ impl Widget for &SplashScreen { subtitle.render(vertical[3], buf); // 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) .style(Style::default().fg(Color::Rgb(120, 110, 100))); version.render(vertical[4], buf); diff --git a/src/ui/statusbar.rs b/src/ui/statusbar.rs index 8a684ed..299da7b 100644 --- a/src/ui/statusbar.rs +++ b/src/ui/statusbar.rs @@ -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,