Compare commits
20 Commits
v0.1.6
...
ff5ea1ded9
| Author | SHA1 | Date | |
|---|---|---|---|
| ff5ea1ded9 | |||
| 5b202fed4f | |||
| 979347bf53 | |||
| 76b55ccff5 | |||
| f0e162d551 | |||
| 6c4571804f | |||
| a0cdcfdf6c | |||
| 96e2482782 | |||
| 6a3f44f911 | |||
| e0e5a2a83d | |||
| 23e86591d1 | |||
| b60a317788 | |||
| 2788e8b7e2 | |||
| 7c186882dc | |||
| bdda669d4d | |||
| 108070db4b | |||
| 08ba04e99f | |||
| e58032deae | |||
| 5c59539120 | |||
| c725bb1ce6 |
5
.gitignore
vendored
5
.gitignore
vendored
@@ -4,6 +4,10 @@
|
||||
debug/
|
||||
target/
|
||||
dev/
|
||||
.agents/
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
|
||||
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
|
||||
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
|
||||
@@ -100,4 +104,3 @@ fabric.properties
|
||||
|
||||
# Android studio 3.1+ serialized cache file
|
||||
.idea/caches/build_file_checksums.ser
|
||||
|
||||
|
||||
@@ -39,41 +39,63 @@ matrix:
|
||||
EXT: ".exe"
|
||||
|
||||
steps:
|
||||
- name: install-deps
|
||||
image: *rust_image
|
||||
commands:
|
||||
- apt-get update
|
||||
- apt-get install -y musl-tools gcc-aarch64-linux-gnu gcc-arm-linux-gnueabihf gcc-mingw-w64-x86-64 zip
|
||||
|
||||
- name: build
|
||||
image: *rust_image
|
||||
commands:
|
||||
# Install cross-compilation tools
|
||||
- apt-get update
|
||||
- apt-get install -y musl-tools gcc-aarch64-linux-gnu g++-aarch64-linux-gnu gcc-arm-linux-gnueabihf g++-arm-linux-gnueabihf mingw-w64 zip
|
||||
|
||||
# Verify cross-compilers are installed
|
||||
- which aarch64-linux-gnu-gcc || echo "aarch64-linux-gnu-gcc not found!"
|
||||
- which arm-linux-gnueabihf-gcc || echo "arm-linux-gnueabihf-gcc not found!"
|
||||
- which x86_64-w64-mingw32-gcc || echo "x86_64-w64-mingw32-gcc not found!"
|
||||
|
||||
# Add rust target
|
||||
- rustup target add ${TARGET}
|
||||
|
||||
# Set up cross-compilation environment variables and build
|
||||
- |
|
||||
case "${TARGET}" in
|
||||
aarch64-unknown-linux-gnu)
|
||||
export CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER=aarch64-linux-gnu-gcc
|
||||
export CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER=/usr/bin/aarch64-linux-gnu-gcc
|
||||
export CC_aarch64_unknown_linux_gnu=/usr/bin/aarch64-linux-gnu-gcc
|
||||
export CXX_aarch64_unknown_linux_gnu=/usr/bin/aarch64-linux-gnu-g++
|
||||
export AR_aarch64_unknown_linux_gnu=/usr/bin/aarch64-linux-gnu-ar
|
||||
;;
|
||||
aarch64-unknown-linux-musl)
|
||||
export CARGO_TARGET_AARCH64_UNKNOWN_LINUX_MUSL_LINKER=aarch64-linux-gnu-gcc
|
||||
export CC_aarch64_unknown_linux_musl=aarch64-linux-gnu-gcc
|
||||
export CARGO_TARGET_AARCH64_UNKNOWN_LINUX_MUSL_LINKER=/usr/bin/aarch64-linux-gnu-gcc
|
||||
export CC_aarch64_unknown_linux_musl=/usr/bin/aarch64-linux-gnu-gcc
|
||||
export CXX_aarch64_unknown_linux_musl=/usr/bin/aarch64-linux-gnu-g++
|
||||
export AR_aarch64_unknown_linux_musl=/usr/bin/aarch64-linux-gnu-ar
|
||||
;;
|
||||
armv7-unknown-linux-gnueabihf)
|
||||
export CARGO_TARGET_ARMV7_UNKNOWN_LINUX_GNUEABIHF_LINKER=arm-linux-gnueabihf-gcc
|
||||
export CARGO_TARGET_ARMV7_UNKNOWN_LINUX_GNUEABIHF_LINKER=/usr/bin/arm-linux-gnueabihf-gcc
|
||||
export CC_armv7_unknown_linux_gnueabihf=/usr/bin/arm-linux-gnueabihf-gcc
|
||||
export CXX_armv7_unknown_linux_gnueabihf=/usr/bin/arm-linux-gnueabihf-g++
|
||||
export AR_armv7_unknown_linux_gnueabihf=/usr/bin/arm-linux-gnueabihf-ar
|
||||
;;
|
||||
armv7-unknown-linux-musleabihf)
|
||||
export CARGO_TARGET_ARMV7_UNKNOWN_LINUX_MUSLEABIHF_LINKER=arm-linux-gnueabihf-gcc
|
||||
export CC_armv7_unknown_linux_musleabihf=arm-linux-gnueabihf-gcc
|
||||
export CARGO_TARGET_ARMV7_UNKNOWN_LINUX_MUSLEABIHF_LINKER=/usr/bin/arm-linux-gnueabihf-gcc
|
||||
export CC_armv7_unknown_linux_musleabihf=/usr/bin/arm-linux-gnueabihf-gcc
|
||||
export CXX_armv7_unknown_linux_musleabihf=/usr/bin/arm-linux-gnueabihf-g++
|
||||
export AR_armv7_unknown_linux_musleabihf=/usr/bin/arm-linux-gnueabihf-ar
|
||||
;;
|
||||
x86_64-pc-windows-gnu)
|
||||
export CARGO_TARGET_X86_64_PC_WINDOWS_GNU_LINKER=x86_64-w64-mingw32-gcc
|
||||
export CARGO_TARGET_X86_64_PC_WINDOWS_GNU_LINKER=/usr/bin/x86_64-w64-mingw32-gcc
|
||||
export CC_x86_64_pc_windows_gnu=/usr/bin/x86_64-w64-mingw32-gcc
|
||||
export CXX_x86_64_pc_windows_gnu=/usr/bin/x86_64-w64-mingw32-g++
|
||||
export AR_x86_64_pc_windows_gnu=/usr/bin/x86_64-w64-mingw32-ar
|
||||
;;
|
||||
esac
|
||||
- cargo build --release --all-features --target ${TARGET}
|
||||
|
||||
# Build the project
|
||||
cargo build --release --all-features --target ${TARGET}
|
||||
|
||||
- name: package
|
||||
image: *rust_image
|
||||
commands:
|
||||
- apt-get update && apt-get install -y zip
|
||||
- mkdir -p dist
|
||||
- |
|
||||
if [ "${PLATFORM}" = "windows" ]; then
|
||||
|
||||
81
CHANGELOG.md
Normal file
81
CHANGELOG.md
Normal file
@@ -0,0 +1,81 @@
|
||||
# Changelog
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
- Comprehensive documentation suite including guides for architecture, configuration, testing, and more.
|
||||
- Rustdoc examples for core components like `Provider` and `SessionController`.
|
||||
- Module-level documentation for `owlen-tui`.
|
||||
|
||||
### Changed
|
||||
- The main `README.md` has been updated to be more concise and link to the new documentation.
|
||||
|
||||
---
|
||||
|
||||
## [0.1.10] - 2025-10-03
|
||||
|
||||
### Added
|
||||
- **Material Light Theme**: A new built-in theme, `material-light`, has been added.
|
||||
|
||||
### Fixed
|
||||
- **UI Readability**: Fixed a bug causing unreadable text in light themes.
|
||||
- **Visual Selection**: The visual selection mode now correctly colors unselected text portions.
|
||||
|
||||
### Changed
|
||||
- **Theme Colors**: The color palettes for `gruvbox`, `rose-pine`, and `monokai` have been corrected.
|
||||
- **In-App Help**: The `:help` menu has been significantly expanded and updated.
|
||||
|
||||
## [0.1.9] - 2025-10-03
|
||||
|
||||
*This version corresponds to the release tagged v0.1.10 in the source repository.*
|
||||
|
||||
### Added
|
||||
- **Material Light Theme**: A new built-in theme, `material-light`, has been added.
|
||||
|
||||
### Fixed
|
||||
- **UI Readability**: Fixed a bug causing unreadable text in light themes.
|
||||
- **Visual Selection**: The visual selection mode now correctly colors unselected text portions.
|
||||
|
||||
### Changed
|
||||
- **Theme Colors**: The color palettes for `gruvbox`, `rose-pine`, and `monokai` have been corrected.
|
||||
- **In-App Help**: The `:help` menu has been significantly expanded and updated.
|
||||
|
||||
## [0.1.8] - 2025-10-02
|
||||
|
||||
### Added
|
||||
- **Command Autocompletion**: Implemented intelligent command suggestions and Tab completion in command mode.
|
||||
|
||||
### Changed
|
||||
- **Build & CI**: Fixed cross-compilation for ARM64, ARMv7, and Windows.
|
||||
|
||||
## [0.1.7] - 2025-10-02
|
||||
|
||||
### Added
|
||||
- **Tabbed Help System**: The help menu is now organized into five tabs for easier navigation.
|
||||
- **Command Aliases**: Added `:o` as a short alias for `:load` / `:open`.
|
||||
|
||||
### Changed
|
||||
- **Session Management**: Improved AI-generated session descriptions.
|
||||
|
||||
## [0.1.6] - 2025-10-02
|
||||
|
||||
### Added
|
||||
- **Platform-Specific Storage**: Sessions are now saved to platform-appropriate directories (e.g., `~/.local/share/owlen` on Linux).
|
||||
- **AI-Generated Session Descriptions**: Conversations can be automatically summarized on save.
|
||||
|
||||
### Changed
|
||||
- **Migration**: Users on older versions can manually move their sessions from `~/.config/owlen/sessions` to the new platform-specific directory.
|
||||
|
||||
## [0.1.4] - 2025-10-01
|
||||
|
||||
### Added
|
||||
- **Multi-Platform Builds**: Pre-built binaries are now provided for Linux (x86_64, aarch64, armv7) and Windows (x86_64).
|
||||
- **AUR Package**: Owlen is now available on the Arch User Repository.
|
||||
|
||||
### Changed
|
||||
- **Build System**: Switched from OpenSSL to rustls for better cross-platform compatibility.
|
||||
121
CODE_OF_CONDUCT.md
Normal file
121
CODE_OF_CONDUCT.md
Normal file
@@ -0,0 +1,121 @@
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
We as members, contributors, and leaders pledge to make participation in our
|
||||
community a harassment-free experience for everyone, regardless of age, body
|
||||
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
||||
identity and expression, level of experience, education, socio-economic status,
|
||||
nationality, personal appearance, race, religion, or sexual identity
|
||||
and orientation.
|
||||
|
||||
We pledge to act and interact in ways that are welcoming, open, and respectful.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to a positive environment for our
|
||||
community include:
|
||||
|
||||
* Demonstrating empathy and kindness toward other people
|
||||
* Being respectful of differing opinions, viewpoints, and experiences
|
||||
* Giving and gracefully accepting constructive feedback
|
||||
* Accepting responsibility and apologizing to those affected by our mistakes,
|
||||
and learning from the experience
|
||||
* Focusing on what is best not just for us as individuals, but for the
|
||||
overall community
|
||||
|
||||
Examples of unacceptable behavior include:
|
||||
|
||||
* The use of sexualized language or imagery, and sexual attention or
|
||||
advances of any kind
|
||||
* Trolling, insulting or derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or email
|
||||
address, without their explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
## Enforcement Responsibilities
|
||||
|
||||
Community leaders are responsible for clarifying and enforcing our standards of
|
||||
acceptable behavior and will take appropriate and fair corrective action in
|
||||
response to any behavior that they deem inappropriate, threatening, offensive,
|
||||
or harmful.
|
||||
|
||||
Community leaders have the right and responsibility to remove, edit, or reject
|
||||
comments, commits, code, wiki edits, issues, and other contributions that are
|
||||
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
||||
decisions when appropriate.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies within all community spaces, and also applies when
|
||||
an individual is officially representing the community in public spaces.
|
||||
Examples of representing our community include using an official e-mail address,
|
||||
posting via an official social media account, or acting as an appointed
|
||||
representative at an online or offline event.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported to the community leaders responsible for enforcement at
|
||||
[security@owlibou.com](mailto:security@owlibou.com). All complaints will be
|
||||
reviewed and investigated promptly and fairly.
|
||||
|
||||
All community leaders are obligated to respect the privacy and security of the
|
||||
reporter of any incident.
|
||||
|
||||
## Enforcement Guidelines
|
||||
|
||||
Community leaders will follow these Community Impact Guidelines in determining
|
||||
the consequences for any action they deem in violation of this Code of Conduct:
|
||||
|
||||
### 1. Correction
|
||||
|
||||
**Community Impact**: Use of inappropriate language or other behavior deemed
|
||||
unprofessional or unwelcome in the community.
|
||||
|
||||
**Consequence**: A private, written warning from community leaders, providing
|
||||
clarity around the nature of the violation and an explanation of why the
|
||||
behavior was inappropriate. A public apology may be requested.
|
||||
|
||||
### 2. Warning
|
||||
|
||||
**Community Impact**: A violation through a single incident or series
|
||||
of actions.
|
||||
|
||||
**Consequence**: A warning with consequences for continued behavior. No
|
||||
interaction with the people involved, including unsolicited interaction with
|
||||
those enforcing the Code of Conduct, for a specified period of time. This
|
||||
includes avoiding interaction in community spaces as well as external channels
|
||||
like social media. Violating these terms may lead to a temporary or
|
||||
permanent ban.
|
||||
|
||||
### 3. Temporary Ban
|
||||
|
||||
**Community Impact**: A serious violation of community standards, including
|
||||
sustained inappropriate behavior.
|
||||
|
||||
**Consequence**: A temporary ban from any sort of interaction or public
|
||||
communication with the community for a specified period of time. No public or
|
||||
private interaction with the people involved, including unsolicited interaction
|
||||
with those enforcing the Code of Conduct, is allowed during this period.
|
||||
Violating these terms may lead to a permanent ban.
|
||||
|
||||
### 4. Permanent Ban
|
||||
|
||||
**Community Impact**: Demonstrating a pattern of violation of community
|
||||
standards, including sustained inappropriate behavior, harassment of an
|
||||
individual, or aggression toward or disparagement of classes of individuals.
|
||||
|
||||
**Consequence**: A permanent ban from any sort of public interaction within
|
||||
the community.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
||||
version 2.1, available at
|
||||
[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
|
||||
82
CONTRIBUTING.md
Normal file
82
CONTRIBUTING.md
Normal file
@@ -0,0 +1,82 @@
|
||||
# Contributing to Owlen
|
||||
|
||||
First off, thank you for considering contributing to Owlen! It's people like you that make Owlen such a great tool.
|
||||
|
||||
Following these guidelines helps to communicate that you respect the time of the developers managing and developing this open source project. In return, they should reciprocate that respect in addressing your issue, assessing changes, and helping you finalize your pull requests.
|
||||
|
||||
## Code of Conduct
|
||||
|
||||
This project and everyone participating in it is governed by the [Owlen Code of Conduct](CODE_OF_CONDUCT.md). By participating, you are expected to uphold this code. Please report unacceptable behavior.
|
||||
|
||||
## How Can I Contribute?
|
||||
|
||||
### Reporting Bugs
|
||||
|
||||
This is one of the most helpful ways you can contribute. Before creating a bug report, please check a few things:
|
||||
|
||||
1. **Check the [troubleshooting guide](docs/troubleshooting.md).** Your issue might be a common one with a known solution.
|
||||
2. **Search the existing issues.** It's possible someone has already reported the same bug. If so, add a comment to the existing issue instead of creating a new one.
|
||||
|
||||
When you are creating a bug report, please include as many details as possible. Fill out the required template, the information it asks for helps us resolve issues faster.
|
||||
|
||||
### Suggesting Enhancements
|
||||
|
||||
If you have an idea for a new feature or an improvement to an existing one, we'd love to hear about it. Please provide as much context as you can about what you're trying to achieve.
|
||||
|
||||
### Your First Code Contribution
|
||||
|
||||
Unsure where to begin contributing to Owlen? You can start by looking through `good first issue` and `help wanted` issues.
|
||||
|
||||
### Pull Requests
|
||||
|
||||
The process for submitting a pull request is as follows:
|
||||
|
||||
1. **Fork the repository** and create your branch from `main`.
|
||||
2. **Make your changes.**
|
||||
3. **Ensure the code lints and formats correctly.**
|
||||
- `cargo fmt --all`
|
||||
- `cargo clippy --all -- -D warnings`
|
||||
4. **Run the tests.**
|
||||
- `cargo test --all`
|
||||
5. **Add a clear, concise commit message.** We follow the [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) specification.
|
||||
6. **Push to your fork** and submit a pull request to Owlen's `main` branch.
|
||||
7. **Include a clear description** of the problem and solution. Include the relevant issue number if applicable.
|
||||
|
||||
## Development Setup
|
||||
|
||||
To get started with the codebase, you'll need to have Rust installed. Then, you can clone the repository and build the project:
|
||||
|
||||
```sh
|
||||
git clone https://github.com/Owlibou/owlen.git
|
||||
cd owlen
|
||||
cargo build
|
||||
```
|
||||
|
||||
## Coding Style
|
||||
|
||||
- We use `cargo fmt` for automated code formatting. Please run it before committing your changes.
|
||||
- We use `cargo clippy` for linting. Your code should be free of any clippy warnings.
|
||||
|
||||
## Commit Message Conventions
|
||||
|
||||
We use [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) for our commit messages. This allows for automated changelog generation and makes the project history easier to read.
|
||||
|
||||
The basic format is:
|
||||
|
||||
```
|
||||
<type>[optional scope]: <description>
|
||||
|
||||
[optional body]
|
||||
|
||||
[optional footer(s)]
|
||||
```
|
||||
|
||||
**Types:** `feat`, `fix`, `docs`, `style`, `refactor`, `test`, `chore`, `build`, `ci`.
|
||||
|
||||
**Example:**
|
||||
|
||||
```
|
||||
feat(provider): add support for Gemini Pro
|
||||
```
|
||||
|
||||
Thank you for your contribution!
|
||||
@@ -9,7 +9,7 @@ members = [
|
||||
exclude = []
|
||||
|
||||
[workspace.package]
|
||||
version = "0.1.5"
|
||||
version = "0.1.9"
|
||||
edition = "2021"
|
||||
authors = ["Owlibou"]
|
||||
license = "AGPL-3.0"
|
||||
|
||||
8
PKGBUILD
8
PKGBUILD
@@ -1,6 +1,6 @@
|
||||
# Maintainer: vikingowl <christian@nachtigall.dev>
|
||||
pkgname=owlen
|
||||
pkgver=0.1.4
|
||||
pkgver=0.1.9
|
||||
pkgrel=1
|
||||
pkgdesc="Terminal User Interface LLM client for Ollama with chat and code assistance features"
|
||||
arch=('x86_64')
|
||||
@@ -40,5 +40,11 @@ package() {
|
||||
|
||||
# Install documentation
|
||||
install -Dm644 README.md "$pkgdir/usr/share/doc/$pkgname/README.md"
|
||||
|
||||
# Install built-in themes for reference
|
||||
install -Dm644 themes/README.md "$pkgdir/usr/share/$pkgname/themes/README.md"
|
||||
for theme in themes/*.toml; do
|
||||
install -Dm644 "$theme" "$pkgdir/usr/share/$pkgname/themes/$(basename $theme)"
|
||||
done
|
||||
}
|
||||
|
||||
|
||||
296
README.md
296
README.md
@@ -3,17 +3,10 @@
|
||||
> Terminal-native assistant for running local language models with a comfortable TUI.
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
## Alpha Status
|
||||
|
||||
- This project is currently in **alpha** (v0.1.5) and under active development.
|
||||
- Core features are functional but expect occasional bugs and missing polish.
|
||||
- Breaking changes may occur between releases as we refine the API.
|
||||
- Feedback, bug reports, and contributions are very welcome!
|
||||
|
||||
## What Is OWLEN?
|
||||
|
||||
OWLEN is a Rust-powered, terminal-first interface for interacting with local large
|
||||
@@ -21,287 +14,90 @@ language models. It provides a responsive chat workflow that runs against
|
||||
[Ollama](https://ollama.com/) with a focus on developer productivity, vim-style navigation,
|
||||
and seamless session management—all without leaving your terminal.
|
||||
|
||||
## Alpha Status
|
||||
|
||||
This project is currently in **alpha** and under active development. Core features are functional, but expect occasional bugs and breaking changes. Feedback, bug reports, and contributions are very welcome!
|
||||
|
||||
## Screenshots
|
||||
|
||||
### Initial Layout
|
||||

|
||||
|
||||
The OWLEN interface features a clean, multi-panel layout with vim-inspired navigation. See more screenshots in the [`images/`](images/) directory including:
|
||||
- Full chat conversations (`chat_view.png`)
|
||||
- Help menu (`help.png`)
|
||||
- Model selection (`model_select.png`)
|
||||
- Visual selection mode (`select_mode.png`)
|
||||
The OWLEN interface features a clean, multi-panel layout with vim-inspired navigation. See more screenshots in the [`images/`](images/) directory.
|
||||
|
||||
## Features
|
||||
|
||||
### Chat Client (`owlen`)
|
||||
- **Vim-style Navigation** - Normal, editing, visual, and command modes
|
||||
- **Streaming Responses** - Real-time token streaming from Ollama
|
||||
- **Multi-Panel Interface** - Separate panels for chat, thinking content, and input
|
||||
- **Advanced Text Editing** - Multi-line input with `tui-textarea`, history navigation
|
||||
- **Visual Selection & Clipboard** - Yank/paste text across panels
|
||||
- **Flexible Scrolling** - Half-page, full-page, and cursor-based navigation
|
||||
- **Model Management** - Interactive model and provider selection (press `m`)
|
||||
- **Session Persistence** - Save and load conversations to/from disk
|
||||
- **AI-Generated Descriptions** - Automatic short summaries for saved sessions
|
||||
- **Session Management** - Start new conversations, clear history, browse saved sessions
|
||||
- **Thinking Mode Support** - Dedicated panel for extended reasoning content
|
||||
- **Bracketed Paste** - Safe paste handling for multi-line content
|
||||
|
||||
### Code Client (`owlen-code`) [Experimental]
|
||||
- All chat client features
|
||||
- Optimized system prompt for programming assistance
|
||||
- Foundation for future code-specific features
|
||||
|
||||
### Core Infrastructure
|
||||
- **Modular Architecture** - Separated core logic, TUI components, and providers
|
||||
- **Provider System** - Extensible provider trait (currently: Ollama)
|
||||
- **Session Controller** - Unified conversation and state management
|
||||
- **Configuration Management** - TOML-based config with sensible defaults
|
||||
- **Message Formatting** - Markdown rendering, thinking content extraction
|
||||
- **Async Runtime** - Built on Tokio for efficient streaming
|
||||
- **Vim-style Navigation**: Normal, editing, visual, and command modes.
|
||||
- **Streaming Responses**: Real-time token streaming from Ollama.
|
||||
- **Advanced Text Editing**: Multi-line input, history, and clipboard support.
|
||||
- **Session Management**: Save, load, and manage conversations.
|
||||
- **Theming System**: 10 built-in themes and support for custom themes.
|
||||
- **Modular Architecture**: Extensible provider system (currently Ollama).
|
||||
|
||||
## Getting Started
|
||||
|
||||
### Prerequisites
|
||||
- Rust 1.75+ and Cargo (`rustup` recommended)
|
||||
- A running Ollama instance with at least one model pulled
|
||||
(defaults to `http://localhost:11434`)
|
||||
- A terminal that supports 256 colors
|
||||
- Rust 1.75+ and Cargo.
|
||||
- A running Ollama instance.
|
||||
- A terminal that supports 256 colors.
|
||||
|
||||
### Clone and Build
|
||||
### Installation
|
||||
|
||||
#### Linux & macOS
|
||||
The recommended way to install on Linux and macOS is to clone the repository and install using `cargo`.
|
||||
|
||||
```bash
|
||||
git clone https://somegit.dev/Owlibou/owlen.git
|
||||
git clone https://github.com/Owlibou/owlen.git
|
||||
cd owlen
|
||||
cargo build --release
|
||||
cargo install --path crates/owlen-cli
|
||||
```
|
||||
**Note for macOS**: While this method works, official binary releases for macOS are planned for the future.
|
||||
|
||||
### Run the Chat Client
|
||||
#### Windows
|
||||
The Windows build has not been thoroughly tested yet. Installation is possible via the same `cargo install` method, but it is considered experimental at this time.
|
||||
|
||||
Make sure Ollama is running, then launch:
|
||||
### Running OWLEN
|
||||
|
||||
Make sure Ollama is running, then launch the application:
|
||||
```bash
|
||||
owlen
|
||||
```
|
||||
If you built from source without installing, you can run it with:
|
||||
```bash
|
||||
./target/release/owlen
|
||||
# or during development:
|
||||
cargo run --bin owlen
|
||||
```
|
||||
|
||||
### (Optional) Try the Code Client
|
||||
|
||||
The coding-focused TUI is experimental:
|
||||
|
||||
```bash
|
||||
cargo build --release --bin owlen-code --features code-client
|
||||
./target/release/owlen-code
|
||||
```
|
||||
|
||||
## Using the TUI
|
||||
|
||||
### Mode System (Vim-inspired)
|
||||
OWLEN uses a modal, vim-inspired interface. Press `?` in Normal mode to view the help screen with all keybindings.
|
||||
|
||||
**Normal Mode** (default):
|
||||
- `i` / `Enter` - Enter editing mode
|
||||
- `a` - Append (move right and enter editing mode)
|
||||
- `A` - Append at end of line
|
||||
- `I` - Insert at start of line
|
||||
- `o` - Insert new line below
|
||||
- `O` - Insert new line above
|
||||
- `v` - Enter visual mode (text selection)
|
||||
- `:` - Enter command mode
|
||||
- `h/j/k/l` - Navigate left/down/up/right
|
||||
- `w/b/e` - Word navigation
|
||||
- `0/$` - Jump to line start/end
|
||||
- `gg` - Jump to top
|
||||
- `G` - Jump to bottom
|
||||
- `Ctrl-d/u` - Half-page scroll
|
||||
- `Ctrl-f/b` - Full-page scroll
|
||||
- `Tab` - Cycle focus between panels
|
||||
- `p` - Paste from clipboard
|
||||
- `dd` - Clear input buffer
|
||||
- `q` - Quit
|
||||
- **Normal Mode**: Navigate with `h/j/k/l`, `w/b`, `gg/G`.
|
||||
- **Editing Mode**: Enter with `i` or `a`. Send messages with `Enter`.
|
||||
- **Command Mode**: Enter with `:`. Access commands like `:quit`, `:save`, `:theme`.
|
||||
|
||||
**Editing Mode**:
|
||||
- `Esc` - Return to normal mode
|
||||
- `Enter` - Send message and return to normal mode
|
||||
- `Ctrl-J` / `Shift-Enter` - Insert newline
|
||||
- `Ctrl-↑/↓` - Navigate input history
|
||||
- Paste events handled automatically
|
||||
## Documentation
|
||||
|
||||
**Visual Mode**:
|
||||
- `j/k/h/l` - Extend selection
|
||||
- `w/b/e` - Word-based selection
|
||||
- `y` - Yank (copy) selection
|
||||
- `d` - Cut selection (Input panel only)
|
||||
- `Esc` - Cancel selection
|
||||
For more detailed information, please refer to the following documents:
|
||||
|
||||
**Command Mode**:
|
||||
- `:q` / `:quit` - Quit application
|
||||
- `:c` / `:clear` - Clear conversation
|
||||
- `:m` / `:model` - Open model selector
|
||||
- `:n` / `:new` - Start new conversation
|
||||
- `:h` / `:help` - Show help
|
||||
- `:save [name]` / `:w [name]` - Save current conversation
|
||||
- `:load` / `:open` - Browse and load saved sessions
|
||||
- `:sessions` / `:ls` - List saved sessions
|
||||
|
||||
**Session Browser** (accessed via `:load` or `:sessions`):
|
||||
- `j` / `k` / `↑` / `↓` - Navigate sessions
|
||||
- `Enter` - Load selected session
|
||||
- `d` - Delete selected session
|
||||
- `Esc` - Close browser
|
||||
|
||||
### Panel Management
|
||||
- Three panels: Chat, Thinking, and Input
|
||||
- `Tab` / `Shift-Tab` - Cycle focus forward/backward
|
||||
- Focused panel receives scroll and navigation commands
|
||||
- Thinking panel appears when extended reasoning is available
|
||||
- **[CONTRIBUTING.md](CONTRIBUTING.md)**: Guidelines for contributing to the project.
|
||||
- **[CHANGELOG.md](CHANGELOG.md)**: A log of changes for each version.
|
||||
- **[docs/architecture.md](docs/architecture.md)**: An overview of the project's architecture.
|
||||
- **[docs/troubleshooting.md](docs/troubleshooting.md)**: Help with common issues.
|
||||
- **[docs/provider-implementation.md](docs/provider-implementation.md)**: A guide for adding new providers.
|
||||
|
||||
## Configuration
|
||||
|
||||
OWLEN stores configuration in `~/.config/owlen/config.toml`. The file is created
|
||||
on first run and can be edited to customize behavior:
|
||||
OWLEN stores its configuration in `~/.config/owlen/config.toml`. This file is created on the first run and can be customized. You can also add custom themes in `~/.config/owlen/themes/`.
|
||||
|
||||
```toml
|
||||
[general]
|
||||
default_model = "llama3.2:latest"
|
||||
default_provider = "ollama"
|
||||
enable_streaming = true
|
||||
project_context_file = "OWLEN.md"
|
||||
|
||||
[providers.ollama]
|
||||
provider_type = "ollama"
|
||||
base_url = "http://localhost:11434"
|
||||
timeout = 300
|
||||
```
|
||||
|
||||
### Storage Settings
|
||||
|
||||
Sessions are saved to platform-specific directories by default:
|
||||
- **Linux**: `~/.local/share/owlen/sessions`
|
||||
- **Windows**: `%APPDATA%\owlen\sessions`
|
||||
- **macOS**: `~/Library/Application Support/owlen/sessions`
|
||||
|
||||
You can customize this in your config:
|
||||
|
||||
```toml
|
||||
[storage]
|
||||
# conversation_dir = "~/custom/path" # Optional: override default location
|
||||
max_saved_sessions = 25
|
||||
generate_descriptions = true # AI-generated summaries for saved sessions
|
||||
```
|
||||
|
||||
Configuration is automatically saved when you change models or providers.
|
||||
|
||||
## Repository Layout
|
||||
|
||||
```
|
||||
owlen/
|
||||
├── crates/
|
||||
│ ├── owlen-core/ # Core types, session management, shared UI components
|
||||
│ ├── owlen-ollama/ # Ollama provider implementation
|
||||
│ ├── owlen-tui/ # TUI components (chat_app, code_app, rendering)
|
||||
│ └── owlen-cli/ # Binary entry points (owlen, owlen-code)
|
||||
├── LICENSE # AGPL-3.0 License
|
||||
├── Cargo.toml # Workspace configuration
|
||||
└── README.md
|
||||
```
|
||||
|
||||
### Architecture Highlights
|
||||
- **owlen-core**: Provider-agnostic core with session controller, UI primitives (AutoScroll, InputMode, FocusedPanel), and shared utilities
|
||||
- **owlen-tui**: Ratatui-based UI implementation with vim-style modal editing
|
||||
- **Separation of Concerns**: Clean boundaries between business logic, presentation, and provider implementations
|
||||
|
||||
## Development
|
||||
|
||||
### Building
|
||||
```bash
|
||||
# Debug build
|
||||
cargo build
|
||||
|
||||
# Release build
|
||||
cargo build --release
|
||||
|
||||
# Build with all features
|
||||
cargo build --all-features
|
||||
|
||||
# Run tests
|
||||
cargo test
|
||||
|
||||
# Check code
|
||||
cargo clippy
|
||||
cargo fmt
|
||||
```
|
||||
|
||||
### Development Notes
|
||||
- Standard Rust workflows apply (`cargo fmt`, `cargo clippy`, `cargo test`)
|
||||
- Codebase uses async Rust (`tokio`) for event handling and streaming
|
||||
- Configuration is cached in `~/.config/owlen` (wipe to reset)
|
||||
- UI components are extensively tested in `owlen-core/src/ui.rs`
|
||||
See the [themes/README.md](themes/README.md) for more details on theming.
|
||||
|
||||
## Roadmap
|
||||
|
||||
### Completed ✓
|
||||
- [x] Streaming responses with real-time display
|
||||
- [x] Autoscroll and viewport management
|
||||
- [x] Push user message before loading LLM response
|
||||
- [x] Thinking mode support with dedicated panel
|
||||
- [x] Vim-style modal editing (Normal, Visual, Command modes)
|
||||
- [x] Multi-panel focus management
|
||||
- [x] Text selection and clipboard functionality
|
||||
- [x] Comprehensive keyboard navigation
|
||||
- [x] Bracketed paste support
|
||||
|
||||
### In Progress
|
||||
- [x] Session persistence (save/load conversations)
|
||||
- [ ] Theming options and color customization
|
||||
- [ ] Enhanced configuration UX (in-app settings)
|
||||
- [ ] Conversation export (Markdown, JSON, plain text)
|
||||
|
||||
### Planned
|
||||
- [ ] Code Client Enhancement
|
||||
- [ ] In-project code navigation
|
||||
- [ ] Syntax highlighting for code blocks
|
||||
- [ ] File tree browser integration
|
||||
- [ ] Project-aware context management
|
||||
- [ ] Code snippets and templates
|
||||
- [ ] Additional LLM Providers
|
||||
- [ ] OpenAI API support
|
||||
- [ ] Anthropic Claude support
|
||||
- [ ] Local model providers (llama.cpp, etc.)
|
||||
- [ ] Advanced Features
|
||||
- [ ] Conversation search and filtering
|
||||
- [ ] Multi-session management
|
||||
- [ ] Export conversations (Markdown, JSON)
|
||||
- [ ] Custom keybindings
|
||||
- [ ] Plugin system
|
||||
We are actively working on enhancing the code client, adding more providers (OpenAI, Anthropic), and improving the overall user experience. See the [Roadmap section in the old README](https://github.com/Owlibou/owlen/blob/main/README.md?plain=1#L295) for more details.
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributions are welcome! Here's how to get started:
|
||||
|
||||
1. Fork the repository
|
||||
2. Create a feature branch (`git checkout -b feature/amazing-feature`)
|
||||
3. Make your changes and add tests
|
||||
4. Run `cargo fmt` and `cargo clippy`
|
||||
5. Commit your changes (`git commit -m 'Add amazing feature'`)
|
||||
6. Push to the branch (`git push origin feature/amazing-feature`)
|
||||
7. Open a Pull Request
|
||||
|
||||
Please open an issue first for significant changes to discuss the approach.
|
||||
Contributions are highly welcome! Please see our **[Contributing Guide](CONTRIBUTING.md)** for details on how to get started, including our code style, commit conventions, and pull request process.
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the GNU Affero General Public License v3.0 (AGPL-3.0) - see the [LICENSE](LICENSE) file for details.
|
||||
|
||||
## Acknowledgments
|
||||
|
||||
Built with:
|
||||
- [ratatui](https://ratatui.rs/) - Terminal UI framework
|
||||
- [crossterm](https://github.com/crossterm-rs/crossterm) - Cross-platform terminal manipulation
|
||||
- [tokio](https://tokio.rs/) - Async runtime
|
||||
- [Ollama](https://ollama.com/) - Local LLM runtime
|
||||
|
||||
---
|
||||
|
||||
**Status**: Alpha v0.1.0 | **License**: AGPL-3.0 | **Made with Rust** 🦀
|
||||
This project is licensed under the GNU Affero General Public License v3.0. See the [LICENSE](LICENSE) file for details.
|
||||
|
||||
19
SECURITY.md
Normal file
19
SECURITY.md
Normal file
@@ -0,0 +1,19 @@
|
||||
# Security Policy
|
||||
|
||||
## Supported Versions
|
||||
|
||||
We are currently in a pre-release phase, so only the latest version is actively supported. As we move towards a 1.0 release, this policy will be updated with specific version support.
|
||||
|
||||
| Version | Supported |
|
||||
| ------- | ------------------ |
|
||||
| < 1.0 | :white_check_mark: |
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
The Owlen team and community take all security vulnerabilities seriously. Thank you for improving the security of our project. We appreciate your efforts and responsible disclosure and will make every effort to acknowledge your contributions.
|
||||
|
||||
To report a security vulnerability, please email the project lead at [security@owlibou.com](mailto:security@owlibou.com) with a detailed description of the issue, the steps to reproduce it, and any affected versions.
|
||||
|
||||
You will receive a response from us within 48 hours. If the issue is confirmed, we will release a patch as soon as possible, depending on the complexity of the issue.
|
||||
|
||||
Please do not report security vulnerabilities through public GitHub issues.
|
||||
5
crates/owlen-anthropic/README.md
Normal file
5
crates/owlen-anthropic/README.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# Owlen Anthropic
|
||||
|
||||
This crate is a placeholder for a future `owlen-core::Provider` implementation for the Anthropic (Claude) API.
|
||||
|
||||
This provider is not yet implemented. Contributions are welcome!
|
||||
15
crates/owlen-cli/README.md
Normal file
15
crates/owlen-cli/README.md
Normal file
@@ -0,0 +1,15 @@
|
||||
# Owlen CLI
|
||||
|
||||
This crate is the command-line entry point for the Owlen application.
|
||||
|
||||
It is responsible for:
|
||||
|
||||
- Parsing command-line arguments.
|
||||
- Loading the configuration.
|
||||
- Initializing the providers.
|
||||
- Starting the `owlen-tui` application.
|
||||
|
||||
There are two binaries:
|
||||
|
||||
- `owlen`: The main chat application.
|
||||
- `owlen-code`: A specialized version for code-related tasks.
|
||||
@@ -24,6 +24,7 @@ async-trait = "0.1.73"
|
||||
toml = "0.8.0"
|
||||
shellexpand = "3.1.0"
|
||||
dirs = "5.0"
|
||||
ratatui = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
tokio-test = { workspace = true }
|
||||
|
||||
12
crates/owlen-core/README.md
Normal file
12
crates/owlen-core/README.md
Normal file
@@ -0,0 +1,12 @@
|
||||
# Owlen Core
|
||||
|
||||
This crate provides the core abstractions and data structures for the Owlen ecosystem.
|
||||
|
||||
It defines the essential traits and types that enable communication with various LLM providers, manage sessions, and handle configuration.
|
||||
|
||||
## Key Components
|
||||
|
||||
- **`Provider` trait**: The fundamental abstraction for all LLM providers. Implement this trait to add support for a new provider.
|
||||
- **`Session`**: Represents a single conversation, managing message history and context.
|
||||
- **`Model`**: Defines the structure for LLM models, including their names and properties.
|
||||
- **Configuration**: Handles loading and parsing of the application's configuration.
|
||||
@@ -202,7 +202,7 @@ pub struct UiSettings {
|
||||
|
||||
impl UiSettings {
|
||||
fn default_theme() -> String {
|
||||
"default".to_string()
|
||||
"default_dark".to_string()
|
||||
}
|
||||
|
||||
fn default_word_wrap() -> bool {
|
||||
@@ -388,7 +388,9 @@ mod tests {
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
// macOS should use ~/Library/Application Support
|
||||
assert!(path.to_string_lossy().contains("Library/Application Support"));
|
||||
assert!(path
|
||||
.to_string_lossy()
|
||||
.contains("Library/Application Support"));
|
||||
}
|
||||
|
||||
println!("Config conversation path: {}", path.display());
|
||||
|
||||
@@ -277,20 +277,26 @@ impl ConversationManager {
|
||||
&self,
|
||||
storage: &StorageManager,
|
||||
name: Option<String>,
|
||||
description: Option<String>
|
||||
description: Option<String>,
|
||||
) -> Result<PathBuf> {
|
||||
storage.save_conversation_with_description(&self.active, name, description)
|
||||
}
|
||||
|
||||
/// Load a conversation from disk and make it active
|
||||
pub fn load_from_disk(&mut self, storage: &StorageManager, path: impl AsRef<Path>) -> Result<()> {
|
||||
pub fn load_from_disk(
|
||||
&mut self,
|
||||
storage: &StorageManager,
|
||||
path: impl AsRef<Path>,
|
||||
) -> Result<()> {
|
||||
let conversation = storage.load_conversation(path)?;
|
||||
self.load(conversation);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// List all saved sessions
|
||||
pub fn list_saved_sessions(storage: &StorageManager) -> Result<Vec<crate::storage::SessionMeta>> {
|
||||
pub fn list_saved_sessions(
|
||||
storage: &StorageManager,
|
||||
) -> Result<Vec<crate::storage::SessionMeta>> {
|
||||
storage.list_sessions()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ pub mod provider;
|
||||
pub mod router;
|
||||
pub mod session;
|
||||
pub mod storage;
|
||||
pub mod theme;
|
||||
pub mod types;
|
||||
pub mod ui;
|
||||
pub mod wrap_cursor;
|
||||
@@ -24,6 +25,7 @@ pub use model::*;
|
||||
pub use provider::*;
|
||||
pub use router::*;
|
||||
pub use session::*;
|
||||
pub use theme::*;
|
||||
|
||||
/// Result type used throughout the OWLEN ecosystem
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
@@ -9,6 +9,72 @@ use std::sync::Arc;
|
||||
pub type ChatStream = Pin<Box<dyn Stream<Item = Result<ChatResponse>> + Send>>;
|
||||
|
||||
/// Trait for LLM providers (Ollama, OpenAI, Anthropic, etc.)
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use std::pin::Pin;
|
||||
/// use std::sync::Arc;
|
||||
/// use futures::Stream;
|
||||
/// use owlen_core::provider::{Provider, ProviderRegistry, ChatStream};
|
||||
/// use owlen_core::types::{ChatRequest, ChatResponse, ModelInfo, Message};
|
||||
/// use owlen_core::Result;
|
||||
///
|
||||
/// // 1. Create a mock provider
|
||||
/// struct MockProvider;
|
||||
///
|
||||
/// #[async_trait::async_trait]
|
||||
/// impl Provider for MockProvider {
|
||||
/// fn name(&self) -> &str {
|
||||
/// "mock"
|
||||
/// }
|
||||
///
|
||||
/// async fn list_models(&self) -> Result<Vec<ModelInfo>> {
|
||||
/// Ok(vec![ModelInfo {
|
||||
/// provider: "mock".to_string(),
|
||||
/// name: "mock-model".to_string(),
|
||||
/// ..Default::default()
|
||||
/// }])
|
||||
/// }
|
||||
///
|
||||
/// async fn chat(&self, request: ChatRequest) -> Result<ChatResponse> {
|
||||
/// let content = format!("Response to: {}", request.messages.last().unwrap().content);
|
||||
/// Ok(ChatResponse {
|
||||
/// model: request.model,
|
||||
/// message: Message { role: "assistant".to_string(), content, ..Default::default() },
|
||||
/// ..Default::default()
|
||||
/// })
|
||||
/// }
|
||||
///
|
||||
/// async fn chat_stream(&self, request: ChatRequest) -> Result<ChatStream> {
|
||||
/// unimplemented!();
|
||||
/// }
|
||||
///
|
||||
/// async fn health_check(&self) -> Result<()> {
|
||||
/// Ok(())
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// // 2. Use the provider with a registry
|
||||
/// #[tokio::main]
|
||||
/// async fn main() {
|
||||
/// let mut registry = ProviderRegistry::new();
|
||||
/// registry.register(MockProvider);
|
||||
///
|
||||
/// let provider = registry.get("mock").unwrap();
|
||||
/// let models = provider.list_models().await.unwrap();
|
||||
/// assert_eq!(models[0].name, "mock-model");
|
||||
///
|
||||
/// let request = ChatRequest {
|
||||
/// model: "mock-model".to_string(),
|
||||
/// messages: vec![Message { role: "user".to_string(), content: "Hello".to_string(), ..Default::default() }],
|
||||
/// ..Default::default()
|
||||
/// };
|
||||
///
|
||||
/// let response = provider.chat(request).await.unwrap();
|
||||
/// assert_eq!(response.message.content, "Response to: Hello");
|
||||
/// }
|
||||
/// ```
|
||||
#[async_trait::async_trait]
|
||||
pub trait Provider: Send + Sync {
|
||||
/// Get the name of this provider
|
||||
|
||||
@@ -20,7 +20,61 @@ pub enum SessionOutcome {
|
||||
},
|
||||
}
|
||||
|
||||
/// High-level controller encapsulating session state and provider interactions
|
||||
/// High-level controller encapsulating session state and provider interactions.
|
||||
///
|
||||
/// This is the main entry point for managing conversations and interacting with LLM providers.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use std::sync::Arc;
|
||||
/// use owlen_core::config::Config;
|
||||
/// use owlen_core::provider::{Provider, ChatStream};
|
||||
/// use owlen_core::session::{SessionController, SessionOutcome};
|
||||
/// use owlen_core::types::{ChatRequest, ChatResponse, ChatParameters, Message, ModelInfo};
|
||||
/// use owlen_core::Result;
|
||||
///
|
||||
/// // Mock provider for the example
|
||||
/// struct MockProvider;
|
||||
/// #[async_trait::async_trait]
|
||||
/// impl Provider for MockProvider {
|
||||
/// fn name(&self) -> &str { "mock" }
|
||||
/// async fn list_models(&self) -> Result<Vec<ModelInfo>> { Ok(vec![]) }
|
||||
/// async fn chat(&self, request: ChatRequest) -> Result<ChatResponse> {
|
||||
/// Ok(ChatResponse {
|
||||
/// model: request.model,
|
||||
/// message: Message::assistant("Hello back!".to_string()),
|
||||
/// ..Default::default()
|
||||
/// })
|
||||
/// }
|
||||
/// async fn chat_stream(&self, request: ChatRequest) -> Result<ChatStream> { unimplemented!() }
|
||||
/// async fn health_check(&self) -> Result<()> { Ok(()) }
|
||||
/// }
|
||||
///
|
||||
/// #[tokio::main]
|
||||
/// async fn main() {
|
||||
/// let provider = Arc::new(MockProvider);
|
||||
/// let config = Config::default();
|
||||
/// let mut session_controller = SessionController::new(provider, config);
|
||||
///
|
||||
/// // Send a message
|
||||
/// let outcome = session_controller.send_message(
|
||||
/// "Hello".to_string(),
|
||||
/// ChatParameters { stream: false, ..Default::default() }
|
||||
/// ).await.unwrap();
|
||||
///
|
||||
/// // Check the response
|
||||
/// if let SessionOutcome::Complete(response) = outcome {
|
||||
/// assert_eq!(response.message.content, "Hello back!");
|
||||
/// }
|
||||
///
|
||||
/// // The conversation now contains both messages
|
||||
/// let messages = session_controller.conversation().messages.clone();
|
||||
/// assert_eq!(messages.len(), 2);
|
||||
/// assert_eq!(messages[0].content, "Hello");
|
||||
/// assert_eq!(messages[1].content, "Hello back!");
|
||||
/// }
|
||||
/// ```
|
||||
pub struct SessionController {
|
||||
provider: Arc<dyn Provider>,
|
||||
conversation: ConversationManager,
|
||||
@@ -231,7 +285,15 @@ impl SessionController {
|
||||
if conv.messages.len() == 1 {
|
||||
let first_msg = &conv.messages[0];
|
||||
let preview = first_msg.content.chars().take(50).collect::<String>();
|
||||
return Ok(format!("{}{}", preview, if first_msg.content.len() > 50 { "..." } else { "" }));
|
||||
return Ok(format!(
|
||||
"{}{}",
|
||||
preview,
|
||||
if first_msg.content.len() > 50 {
|
||||
"..."
|
||||
} else {
|
||||
""
|
||||
}
|
||||
));
|
||||
}
|
||||
|
||||
// Build a summary prompt from the first few and last few messages
|
||||
@@ -240,7 +302,8 @@ impl SessionController {
|
||||
// Add system message to guide the description
|
||||
summary_messages.push(crate::types::Message::system(
|
||||
"Summarize this conversation in 1-2 short sentences (max 100 characters). \
|
||||
Focus on the main topic or question being discussed. Be concise and descriptive.".to_string()
|
||||
Focus on the main topic or question being discussed. Be concise and descriptive."
|
||||
.to_string(),
|
||||
));
|
||||
|
||||
// Include first message
|
||||
@@ -283,7 +346,15 @@ impl SessionController {
|
||||
if description.is_empty() {
|
||||
let first_msg = &conv.messages[0];
|
||||
let preview = first_msg.content.chars().take(50).collect::<String>();
|
||||
return Ok(format!("{}{}", preview, if first_msg.content.len() > 50 { "..." } else { "" }));
|
||||
return Ok(format!(
|
||||
"{}{}",
|
||||
preview,
|
||||
if first_msg.content.len() > 50 {
|
||||
"..."
|
||||
} else {
|
||||
""
|
||||
}
|
||||
));
|
||||
}
|
||||
|
||||
// Truncate if too long
|
||||
@@ -298,7 +369,15 @@ impl SessionController {
|
||||
// Fallback to simple description if AI generation fails
|
||||
let first_msg = &conv.messages[0];
|
||||
let preview = first_msg.content.chars().take(50).collect::<String>();
|
||||
Ok(format!("{}{}", preview, if first_msg.content.len() > 50 { "..." } else { "" }))
|
||||
Ok(format!(
|
||||
"{}{}",
|
||||
preview,
|
||||
if first_msg.content.len() > 50 {
|
||||
"..."
|
||||
} else {
|
||||
""
|
||||
}
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,10 +45,7 @@ impl StorageManager {
|
||||
// Ensure the directory exists
|
||||
if !sessions_dir.exists() {
|
||||
fs::create_dir_all(&sessions_dir).map_err(|e| {
|
||||
Error::Storage(format!(
|
||||
"Failed to create sessions directory: {}",
|
||||
e
|
||||
))
|
||||
Error::Storage(format!("Failed to create sessions directory: {}", e))
|
||||
})?;
|
||||
}
|
||||
|
||||
@@ -66,7 +63,11 @@ impl StorageManager {
|
||||
}
|
||||
|
||||
/// Save a conversation to disk
|
||||
pub fn save_conversation(&self, conversation: &Conversation, name: Option<String>) -> Result<PathBuf> {
|
||||
pub fn save_conversation(
|
||||
&self,
|
||||
conversation: &Conversation,
|
||||
name: Option<String>,
|
||||
) -> Result<PathBuf> {
|
||||
self.save_conversation_with_description(conversation, name, None)
|
||||
}
|
||||
|
||||
@@ -75,7 +76,7 @@ impl StorageManager {
|
||||
&self,
|
||||
conversation: &Conversation,
|
||||
name: Option<String>,
|
||||
description: Option<String>
|
||||
description: Option<String>,
|
||||
) -> Result<PathBuf> {
|
||||
let filename = if let Some(ref session_name) = name {
|
||||
// Use provided name, sanitized
|
||||
@@ -101,26 +102,22 @@ impl StorageManager {
|
||||
save_conv.description = description;
|
||||
}
|
||||
|
||||
let json = serde_json::to_string_pretty(&save_conv).map_err(|e| {
|
||||
Error::Storage(format!("Failed to serialize conversation: {}", e))
|
||||
})?;
|
||||
let json = serde_json::to_string_pretty(&save_conv)
|
||||
.map_err(|e| Error::Storage(format!("Failed to serialize conversation: {}", e)))?;
|
||||
|
||||
fs::write(&path, json).map_err(|e| {
|
||||
Error::Storage(format!("Failed to write session file: {}", e))
|
||||
})?;
|
||||
fs::write(&path, json)
|
||||
.map_err(|e| Error::Storage(format!("Failed to write session file: {}", e)))?;
|
||||
|
||||
Ok(path)
|
||||
}
|
||||
|
||||
/// Load a conversation from disk
|
||||
pub fn load_conversation(&self, path: impl AsRef<Path>) -> Result<Conversation> {
|
||||
let content = fs::read_to_string(path.as_ref()).map_err(|e| {
|
||||
Error::Storage(format!("Failed to read session file: {}", e))
|
||||
})?;
|
||||
let content = fs::read_to_string(path.as_ref())
|
||||
.map_err(|e| Error::Storage(format!("Failed to read session file: {}", e)))?;
|
||||
|
||||
let conversation: Conversation = serde_json::from_str(&content).map_err(|e| {
|
||||
Error::Storage(format!("Failed to parse session file: {}", e))
|
||||
})?;
|
||||
let conversation: Conversation = serde_json::from_str(&content)
|
||||
.map_err(|e| Error::Storage(format!("Failed to parse session file: {}", e)))?;
|
||||
|
||||
Ok(conversation)
|
||||
}
|
||||
@@ -129,14 +126,12 @@ impl StorageManager {
|
||||
pub fn list_sessions(&self) -> Result<Vec<SessionMeta>> {
|
||||
let mut sessions = Vec::new();
|
||||
|
||||
let entries = fs::read_dir(&self.sessions_dir).map_err(|e| {
|
||||
Error::Storage(format!("Failed to read sessions directory: {}", e))
|
||||
})?;
|
||||
let entries = fs::read_dir(&self.sessions_dir)
|
||||
.map_err(|e| Error::Storage(format!("Failed to read sessions directory: {}", e)))?;
|
||||
|
||||
for entry in entries {
|
||||
let entry = entry.map_err(|e| {
|
||||
Error::Storage(format!("Failed to read directory entry: {}", e))
|
||||
})?;
|
||||
let entry = entry
|
||||
.map_err(|e| Error::Storage(format!("Failed to read directory entry: {}", e)))?;
|
||||
|
||||
let path = entry.path();
|
||||
if path.extension().and_then(|s| s.to_str()) != Some("json") {
|
||||
@@ -172,9 +167,8 @@ impl StorageManager {
|
||||
|
||||
/// Delete a saved session
|
||||
pub fn delete_session(&self, path: impl AsRef<Path>) -> Result<()> {
|
||||
fs::remove_file(path.as_ref()).map_err(|e| {
|
||||
Error::Storage(format!("Failed to delete session file: {}", e))
|
||||
})
|
||||
fs::remove_file(path.as_ref())
|
||||
.map_err(|e| Error::Storage(format!("Failed to delete session file: {}", e)))
|
||||
}
|
||||
|
||||
/// Get the sessions directory path
|
||||
@@ -237,7 +231,9 @@ mod tests {
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
// macOS should use ~/Library/Application Support
|
||||
assert!(path.to_string_lossy().contains("Library/Application Support"));
|
||||
assert!(path
|
||||
.to_string_lossy()
|
||||
.contains("Library/Application Support"));
|
||||
}
|
||||
|
||||
println!("Default sessions directory: {}", path.display());
|
||||
@@ -257,10 +253,13 @@ mod tests {
|
||||
|
||||
let mut conv = Conversation::new("test-model".to_string());
|
||||
conv.messages.push(Message::user("Hello".to_string()));
|
||||
conv.messages.push(Message::assistant("Hi there!".to_string()));
|
||||
conv.messages
|
||||
.push(Message::assistant("Hi there!".to_string()));
|
||||
|
||||
// Save conversation
|
||||
let path = storage.save_conversation(&conv, Some("test_session".to_string())).unwrap();
|
||||
let path = storage
|
||||
.save_conversation(&conv, Some("test_session".to_string()))
|
||||
.unwrap();
|
||||
assert!(path.exists());
|
||||
|
||||
// Load conversation
|
||||
@@ -280,7 +279,9 @@ mod tests {
|
||||
for i in 0..3 {
|
||||
let mut conv = Conversation::new("test-model".to_string());
|
||||
conv.messages.push(Message::user(format!("Message {}", i)));
|
||||
storage.save_conversation(&conv, Some(format!("session_{}", i))).unwrap();
|
||||
storage
|
||||
.save_conversation(&conv, Some(format!("session_{}", i)))
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
// List sessions
|
||||
|
||||
645
crates/owlen-core/src/theme.rs
Normal file
645
crates/owlen-core/src/theme.rs
Normal file
@@ -0,0 +1,645 @@
|
||||
//! Theming system for OWLEN TUI
|
||||
//!
|
||||
//! Provides customizable color schemes for all UI components.
|
||||
|
||||
use ratatui::style::Color;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
/// A complete theme definition for OWLEN TUI
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Theme {
|
||||
/// Name of the theme
|
||||
pub name: String,
|
||||
|
||||
/// Default text color
|
||||
#[serde(deserialize_with = "deserialize_color")]
|
||||
#[serde(serialize_with = "serialize_color")]
|
||||
pub text: Color,
|
||||
|
||||
/// Default background color
|
||||
#[serde(deserialize_with = "deserialize_color")]
|
||||
#[serde(serialize_with = "serialize_color")]
|
||||
pub background: Color,
|
||||
|
||||
/// Border color for focused panels
|
||||
#[serde(deserialize_with = "deserialize_color")]
|
||||
#[serde(serialize_with = "serialize_color")]
|
||||
pub focused_panel_border: Color,
|
||||
|
||||
/// Border color for unfocused panels
|
||||
#[serde(deserialize_with = "deserialize_color")]
|
||||
#[serde(serialize_with = "serialize_color")]
|
||||
pub unfocused_panel_border: Color,
|
||||
|
||||
/// Color for user message role indicator
|
||||
#[serde(deserialize_with = "deserialize_color")]
|
||||
#[serde(serialize_with = "serialize_color")]
|
||||
pub user_message_role: Color,
|
||||
|
||||
/// Color for assistant message role indicator
|
||||
#[serde(deserialize_with = "deserialize_color")]
|
||||
#[serde(serialize_with = "serialize_color")]
|
||||
pub assistant_message_role: Color,
|
||||
|
||||
/// Color for thinking panel title
|
||||
#[serde(deserialize_with = "deserialize_color")]
|
||||
#[serde(serialize_with = "serialize_color")]
|
||||
pub thinking_panel_title: Color,
|
||||
|
||||
/// Background color for command bar
|
||||
#[serde(deserialize_with = "deserialize_color")]
|
||||
#[serde(serialize_with = "serialize_color")]
|
||||
pub command_bar_background: Color,
|
||||
|
||||
/// Status line background color
|
||||
#[serde(deserialize_with = "deserialize_color")]
|
||||
#[serde(serialize_with = "serialize_color")]
|
||||
pub status_background: Color,
|
||||
|
||||
/// Color for Normal mode indicator
|
||||
#[serde(deserialize_with = "deserialize_color")]
|
||||
#[serde(serialize_with = "serialize_color")]
|
||||
pub mode_normal: Color,
|
||||
|
||||
/// Color for Editing mode indicator
|
||||
#[serde(deserialize_with = "deserialize_color")]
|
||||
#[serde(serialize_with = "serialize_color")]
|
||||
pub mode_editing: Color,
|
||||
|
||||
/// Color for Model Selection mode indicator
|
||||
#[serde(deserialize_with = "deserialize_color")]
|
||||
#[serde(serialize_with = "serialize_color")]
|
||||
pub mode_model_selection: Color,
|
||||
|
||||
/// Color for Provider Selection mode indicator
|
||||
#[serde(deserialize_with = "deserialize_color")]
|
||||
#[serde(serialize_with = "serialize_color")]
|
||||
pub mode_provider_selection: Color,
|
||||
|
||||
/// Color for Help mode indicator
|
||||
#[serde(deserialize_with = "deserialize_color")]
|
||||
#[serde(serialize_with = "serialize_color")]
|
||||
pub mode_help: Color,
|
||||
|
||||
/// Color for Visual mode indicator
|
||||
#[serde(deserialize_with = "deserialize_color")]
|
||||
#[serde(serialize_with = "serialize_color")]
|
||||
pub mode_visual: Color,
|
||||
|
||||
/// Color for Command mode indicator
|
||||
#[serde(deserialize_with = "deserialize_color")]
|
||||
#[serde(serialize_with = "serialize_color")]
|
||||
pub mode_command: Color,
|
||||
|
||||
/// Selection/highlight background color
|
||||
#[serde(deserialize_with = "deserialize_color")]
|
||||
#[serde(serialize_with = "serialize_color")]
|
||||
pub selection_bg: Color,
|
||||
|
||||
/// Selection/highlight foreground color
|
||||
#[serde(deserialize_with = "deserialize_color")]
|
||||
#[serde(serialize_with = "serialize_color")]
|
||||
pub selection_fg: Color,
|
||||
|
||||
/// Cursor indicator color
|
||||
#[serde(deserialize_with = "deserialize_color")]
|
||||
#[serde(serialize_with = "serialize_color")]
|
||||
pub cursor: Color,
|
||||
|
||||
/// Placeholder text color
|
||||
#[serde(deserialize_with = "deserialize_color")]
|
||||
#[serde(serialize_with = "serialize_color")]
|
||||
pub placeholder: Color,
|
||||
|
||||
/// Warning/error message color
|
||||
#[serde(deserialize_with = "deserialize_color")]
|
||||
#[serde(serialize_with = "serialize_color")]
|
||||
pub error: Color,
|
||||
|
||||
/// Success/info message color
|
||||
#[serde(deserialize_with = "deserialize_color")]
|
||||
#[serde(serialize_with = "serialize_color")]
|
||||
pub info: Color,
|
||||
}
|
||||
|
||||
impl Default for Theme {
|
||||
fn default() -> Self {
|
||||
default_dark()
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the default themes directory path
|
||||
pub fn default_themes_dir() -> PathBuf {
|
||||
let config_dir = PathBuf::from(shellexpand::tilde(crate::config::DEFAULT_CONFIG_PATH).as_ref())
|
||||
.parent()
|
||||
.map(|p| p.to_path_buf())
|
||||
.unwrap_or_else(|| PathBuf::from("~/.config/owlen"));
|
||||
|
||||
config_dir.join("themes")
|
||||
}
|
||||
|
||||
/// Load all available themes (built-in + custom)
|
||||
pub fn load_all_themes() -> HashMap<String, Theme> {
|
||||
let mut themes = HashMap::new();
|
||||
|
||||
// Load built-in themes
|
||||
for (name, theme) in built_in_themes() {
|
||||
themes.insert(name, theme);
|
||||
}
|
||||
|
||||
// Load custom themes from disk
|
||||
let themes_dir = default_themes_dir();
|
||||
if let Ok(entries) = fs::read_dir(&themes_dir) {
|
||||
for entry in entries.flatten() {
|
||||
let path = entry.path();
|
||||
if path.extension().and_then(|s| s.to_str()) == Some("toml") {
|
||||
let name = path
|
||||
.file_stem()
|
||||
.and_then(|s| s.to_str())
|
||||
.unwrap_or("unknown")
|
||||
.to_string();
|
||||
|
||||
match load_theme_from_file(&path) {
|
||||
Ok(theme) => {
|
||||
themes.insert(name.clone(), theme);
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Warning: Failed to load custom theme '{}': {}", name, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
themes
|
||||
}
|
||||
|
||||
/// Load a theme from a TOML file
|
||||
pub fn load_theme_from_file(path: &Path) -> Result<Theme, String> {
|
||||
let content =
|
||||
fs::read_to_string(path).map_err(|e| format!("Failed to read theme file: {}", e))?;
|
||||
|
||||
toml::from_str(&content).map_err(|e| format!("Failed to parse theme file: {}", e))
|
||||
}
|
||||
|
||||
/// Get a theme by name (built-in or custom)
|
||||
pub fn get_theme(name: &str) -> Option<Theme> {
|
||||
load_all_themes().get(name).cloned()
|
||||
}
|
||||
|
||||
/// Get all built-in themes (embedded in the binary)
|
||||
pub fn built_in_themes() -> HashMap<String, Theme> {
|
||||
let mut themes = HashMap::new();
|
||||
|
||||
// Load embedded theme files
|
||||
let embedded_themes = [
|
||||
(
|
||||
"default_dark",
|
||||
include_str!("../../../themes/default_dark.toml"),
|
||||
),
|
||||
(
|
||||
"default_light",
|
||||
include_str!("../../../themes/default_light.toml"),
|
||||
),
|
||||
("gruvbox", include_str!("../../../themes/gruvbox.toml")),
|
||||
("dracula", include_str!("../../../themes/dracula.toml")),
|
||||
("solarized", include_str!("../../../themes/solarized.toml")),
|
||||
(
|
||||
"midnight-ocean",
|
||||
include_str!("../../../themes/midnight-ocean.toml"),
|
||||
),
|
||||
("rose-pine", include_str!("../../../themes/rose-pine.toml")),
|
||||
("monokai", include_str!("../../../themes/monokai.toml")),
|
||||
(
|
||||
"material-dark",
|
||||
include_str!("../../../themes/material-dark.toml"),
|
||||
),
|
||||
(
|
||||
"material-light",
|
||||
include_str!("../../../themes/material-light.toml"),
|
||||
),
|
||||
];
|
||||
|
||||
for (name, content) in embedded_themes {
|
||||
match toml::from_str::<Theme>(content) {
|
||||
Ok(theme) => {
|
||||
themes.insert(name.to_string(), theme);
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Warning: Failed to parse built-in theme '{}': {}", name, e);
|
||||
// Fallback to hardcoded version if parsing fails
|
||||
if let Some(fallback) = get_fallback_theme(name) {
|
||||
themes.insert(name.to_string(), fallback);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
themes
|
||||
}
|
||||
|
||||
/// Get fallback hardcoded theme (used if embedded TOML fails to parse)
|
||||
fn get_fallback_theme(name: &str) -> Option<Theme> {
|
||||
match name {
|
||||
"default_dark" => Some(default_dark()),
|
||||
"default_light" => Some(default_light()),
|
||||
"gruvbox" => Some(gruvbox()),
|
||||
"dracula" => Some(dracula()),
|
||||
"solarized" => Some(solarized()),
|
||||
"midnight-ocean" => Some(midnight_ocean()),
|
||||
"rose-pine" => Some(rose_pine()),
|
||||
"monokai" => Some(monokai()),
|
||||
"material-dark" => Some(material_dark()),
|
||||
"material-light" => Some(material_light()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Default dark theme
|
||||
fn default_dark() -> Theme {
|
||||
Theme {
|
||||
name: "default_dark".to_string(),
|
||||
text: Color::White,
|
||||
background: Color::Black,
|
||||
focused_panel_border: Color::LightMagenta,
|
||||
unfocused_panel_border: Color::Rgb(95, 20, 135),
|
||||
user_message_role: Color::LightBlue,
|
||||
assistant_message_role: Color::Yellow,
|
||||
thinking_panel_title: Color::LightMagenta,
|
||||
command_bar_background: Color::Black,
|
||||
status_background: Color::Black,
|
||||
mode_normal: Color::LightBlue,
|
||||
mode_editing: Color::LightGreen,
|
||||
mode_model_selection: Color::LightYellow,
|
||||
mode_provider_selection: Color::LightCyan,
|
||||
mode_help: Color::LightMagenta,
|
||||
mode_visual: Color::Magenta,
|
||||
mode_command: Color::Yellow,
|
||||
selection_bg: Color::LightBlue,
|
||||
selection_fg: Color::Black,
|
||||
cursor: Color::Magenta,
|
||||
placeholder: Color::DarkGray,
|
||||
error: Color::Red,
|
||||
info: Color::LightGreen,
|
||||
}
|
||||
}
|
||||
|
||||
/// Default light theme
|
||||
fn default_light() -> Theme {
|
||||
Theme {
|
||||
name: "default_light".to_string(),
|
||||
text: Color::Black,
|
||||
background: Color::White,
|
||||
focused_panel_border: Color::Rgb(74, 144, 226),
|
||||
unfocused_panel_border: Color::Rgb(221, 221, 221),
|
||||
user_message_role: Color::Rgb(0, 85, 164),
|
||||
assistant_message_role: Color::Rgb(142, 68, 173),
|
||||
thinking_panel_title: Color::Rgb(142, 68, 173),
|
||||
command_bar_background: Color::White,
|
||||
status_background: Color::White,
|
||||
mode_normal: Color::Rgb(0, 85, 164),
|
||||
mode_editing: Color::Rgb(46, 139, 87),
|
||||
mode_model_selection: Color::Rgb(181, 137, 0),
|
||||
mode_provider_selection: Color::Rgb(0, 139, 139),
|
||||
mode_help: Color::Rgb(142, 68, 173),
|
||||
mode_visual: Color::Rgb(142, 68, 173),
|
||||
mode_command: Color::Rgb(181, 137, 0),
|
||||
selection_bg: Color::Rgb(164, 200, 240),
|
||||
selection_fg: Color::Black,
|
||||
cursor: Color::Rgb(217, 95, 2),
|
||||
placeholder: Color::Gray,
|
||||
error: Color::Rgb(192, 57, 43),
|
||||
info: Color::Green,
|
||||
}
|
||||
}
|
||||
|
||||
/// Gruvbox theme
|
||||
fn gruvbox() -> Theme {
|
||||
Theme {
|
||||
name: "gruvbox".to_string(),
|
||||
text: Color::Rgb(235, 219, 178), // #ebdbb2
|
||||
background: Color::Rgb(40, 40, 40), // #282828
|
||||
focused_panel_border: Color::Rgb(254, 128, 25), // #fe8019 (orange)
|
||||
unfocused_panel_border: Color::Rgb(124, 111, 100), // #7c6f64
|
||||
user_message_role: Color::Rgb(184, 187, 38), // #b8bb26 (green)
|
||||
assistant_message_role: Color::Rgb(131, 165, 152), // #83a598 (blue)
|
||||
thinking_panel_title: Color::Rgb(211, 134, 155), // #d3869b (purple)
|
||||
command_bar_background: Color::Rgb(60, 56, 54), // #3c3836
|
||||
status_background: Color::Rgb(60, 56, 54),
|
||||
mode_normal: Color::Rgb(131, 165, 152), // blue
|
||||
mode_editing: Color::Rgb(184, 187, 38), // green
|
||||
mode_model_selection: Color::Rgb(250, 189, 47), // yellow
|
||||
mode_provider_selection: Color::Rgb(142, 192, 124), // aqua
|
||||
mode_help: Color::Rgb(211, 134, 155), // purple
|
||||
mode_visual: Color::Rgb(254, 128, 25), // orange
|
||||
mode_command: Color::Rgb(250, 189, 47), // yellow
|
||||
selection_bg: Color::Rgb(80, 73, 69),
|
||||
selection_fg: Color::Rgb(235, 219, 178),
|
||||
cursor: Color::Rgb(254, 128, 25),
|
||||
placeholder: Color::Rgb(102, 92, 84),
|
||||
error: Color::Rgb(251, 73, 52), // #fb4934
|
||||
info: Color::Rgb(184, 187, 38),
|
||||
}
|
||||
}
|
||||
|
||||
/// Dracula theme
|
||||
fn dracula() -> Theme {
|
||||
Theme {
|
||||
name: "dracula".to_string(),
|
||||
text: Color::Rgb(248, 248, 242), // #f8f8f2
|
||||
background: Color::Rgb(40, 42, 54), // #282a36
|
||||
focused_panel_border: Color::Rgb(255, 121, 198), // #ff79c6 (pink)
|
||||
unfocused_panel_border: Color::Rgb(68, 71, 90), // #44475a
|
||||
user_message_role: Color::Rgb(139, 233, 253), // #8be9fd (cyan)
|
||||
assistant_message_role: Color::Rgb(255, 121, 198), // #ff79c6 (pink)
|
||||
thinking_panel_title: Color::Rgb(189, 147, 249), // #bd93f9 (purple)
|
||||
command_bar_background: Color::Rgb(68, 71, 90),
|
||||
status_background: Color::Rgb(68, 71, 90),
|
||||
mode_normal: Color::Rgb(139, 233, 253),
|
||||
mode_editing: Color::Rgb(80, 250, 123), // #50fa7b (green)
|
||||
mode_model_selection: Color::Rgb(241, 250, 140), // #f1fa8c (yellow)
|
||||
mode_provider_selection: Color::Rgb(139, 233, 253),
|
||||
mode_help: Color::Rgb(189, 147, 249),
|
||||
mode_visual: Color::Rgb(255, 121, 198),
|
||||
mode_command: Color::Rgb(241, 250, 140),
|
||||
selection_bg: Color::Rgb(68, 71, 90),
|
||||
selection_fg: Color::Rgb(248, 248, 242),
|
||||
cursor: Color::Rgb(255, 121, 198),
|
||||
placeholder: Color::Rgb(98, 114, 164),
|
||||
error: Color::Rgb(255, 85, 85), // #ff5555
|
||||
info: Color::Rgb(80, 250, 123),
|
||||
}
|
||||
}
|
||||
|
||||
/// Solarized Dark theme
|
||||
fn solarized() -> Theme {
|
||||
Theme {
|
||||
name: "solarized".to_string(),
|
||||
text: Color::Rgb(131, 148, 150), // #839496 (base0)
|
||||
background: Color::Rgb(0, 43, 54), // #002b36 (base03)
|
||||
focused_panel_border: Color::Rgb(38, 139, 210), // #268bd2 (blue)
|
||||
unfocused_panel_border: Color::Rgb(7, 54, 66), // #073642 (base02)
|
||||
user_message_role: Color::Rgb(42, 161, 152), // #2aa198 (cyan)
|
||||
assistant_message_role: Color::Rgb(203, 75, 22), // #cb4b16 (orange)
|
||||
thinking_panel_title: Color::Rgb(108, 113, 196), // #6c71c4 (violet)
|
||||
command_bar_background: Color::Rgb(7, 54, 66),
|
||||
status_background: Color::Rgb(7, 54, 66),
|
||||
mode_normal: Color::Rgb(38, 139, 210), // blue
|
||||
mode_editing: Color::Rgb(133, 153, 0), // #859900 (green)
|
||||
mode_model_selection: Color::Rgb(181, 137, 0), // #b58900 (yellow)
|
||||
mode_provider_selection: Color::Rgb(42, 161, 152), // cyan
|
||||
mode_help: Color::Rgb(108, 113, 196), // violet
|
||||
mode_visual: Color::Rgb(211, 54, 130), // #d33682 (magenta)
|
||||
mode_command: Color::Rgb(181, 137, 0), // yellow
|
||||
selection_bg: Color::Rgb(7, 54, 66),
|
||||
selection_fg: Color::Rgb(147, 161, 161),
|
||||
cursor: Color::Rgb(211, 54, 130),
|
||||
placeholder: Color::Rgb(88, 110, 117),
|
||||
error: Color::Rgb(220, 50, 47), // #dc322f (red)
|
||||
info: Color::Rgb(133, 153, 0),
|
||||
}
|
||||
}
|
||||
|
||||
/// Midnight Ocean theme
|
||||
fn midnight_ocean() -> Theme {
|
||||
Theme {
|
||||
name: "midnight-ocean".to_string(),
|
||||
text: Color::Rgb(192, 202, 245),
|
||||
background: Color::Rgb(13, 17, 23),
|
||||
focused_panel_border: Color::Rgb(88, 166, 255),
|
||||
unfocused_panel_border: Color::Rgb(48, 54, 61),
|
||||
user_message_role: Color::Rgb(121, 192, 255),
|
||||
assistant_message_role: Color::Rgb(137, 221, 255),
|
||||
thinking_panel_title: Color::Rgb(158, 206, 106),
|
||||
command_bar_background: Color::Rgb(22, 27, 34),
|
||||
status_background: Color::Rgb(22, 27, 34),
|
||||
mode_normal: Color::Rgb(121, 192, 255),
|
||||
mode_editing: Color::Rgb(158, 206, 106),
|
||||
mode_model_selection: Color::Rgb(255, 212, 59),
|
||||
mode_provider_selection: Color::Rgb(137, 221, 255),
|
||||
mode_help: Color::Rgb(255, 115, 157),
|
||||
mode_visual: Color::Rgb(246, 140, 245),
|
||||
mode_command: Color::Rgb(255, 212, 59),
|
||||
selection_bg: Color::Rgb(56, 139, 253),
|
||||
selection_fg: Color::Rgb(13, 17, 23),
|
||||
cursor: Color::Rgb(246, 140, 245),
|
||||
placeholder: Color::Rgb(110, 118, 129),
|
||||
error: Color::Rgb(248, 81, 73),
|
||||
info: Color::Rgb(158, 206, 106),
|
||||
}
|
||||
}
|
||||
|
||||
/// Rose Pine theme
|
||||
fn rose_pine() -> Theme {
|
||||
Theme {
|
||||
name: "rose-pine".to_string(),
|
||||
text: Color::Rgb(224, 222, 244), // #e0def4
|
||||
background: Color::Rgb(25, 23, 36), // #191724
|
||||
focused_panel_border: Color::Rgb(235, 111, 146), // #eb6f92 (love)
|
||||
unfocused_panel_border: Color::Rgb(38, 35, 58), // #26233a
|
||||
user_message_role: Color::Rgb(49, 116, 143), // #31748f (foam)
|
||||
assistant_message_role: Color::Rgb(156, 207, 216), // #9ccfd8 (foam light)
|
||||
thinking_panel_title: Color::Rgb(196, 167, 231), // #c4a7e7 (iris)
|
||||
command_bar_background: Color::Rgb(38, 35, 58),
|
||||
status_background: Color::Rgb(38, 35, 58),
|
||||
mode_normal: Color::Rgb(156, 207, 216),
|
||||
mode_editing: Color::Rgb(235, 188, 186), // #ebbcba (rose)
|
||||
mode_model_selection: Color::Rgb(246, 193, 119),
|
||||
mode_provider_selection: Color::Rgb(49, 116, 143),
|
||||
mode_help: Color::Rgb(196, 167, 231),
|
||||
mode_visual: Color::Rgb(235, 111, 146),
|
||||
mode_command: Color::Rgb(246, 193, 119),
|
||||
selection_bg: Color::Rgb(64, 61, 82),
|
||||
selection_fg: Color::Rgb(224, 222, 244),
|
||||
cursor: Color::Rgb(235, 111, 146),
|
||||
placeholder: Color::Rgb(110, 106, 134),
|
||||
error: Color::Rgb(235, 111, 146),
|
||||
info: Color::Rgb(156, 207, 216),
|
||||
}
|
||||
}
|
||||
|
||||
/// Monokai theme
|
||||
fn monokai() -> Theme {
|
||||
Theme {
|
||||
name: "monokai".to_string(),
|
||||
text: Color::Rgb(248, 248, 242), // #f8f8f2
|
||||
background: Color::Rgb(39, 40, 34), // #272822
|
||||
focused_panel_border: Color::Rgb(249, 38, 114), // #f92672 (pink)
|
||||
unfocused_panel_border: Color::Rgb(117, 113, 94), // #75715e
|
||||
user_message_role: Color::Rgb(102, 217, 239), // #66d9ef (cyan)
|
||||
assistant_message_role: Color::Rgb(174, 129, 255), // #ae81ff (purple)
|
||||
thinking_panel_title: Color::Rgb(230, 219, 116), // #e6db74 (yellow)
|
||||
command_bar_background: Color::Rgb(39, 40, 34),
|
||||
status_background: Color::Rgb(39, 40, 34),
|
||||
mode_normal: Color::Rgb(102, 217, 239),
|
||||
mode_editing: Color::Rgb(166, 226, 46), // #a6e22e (green)
|
||||
mode_model_selection: Color::Rgb(230, 219, 116),
|
||||
mode_provider_selection: Color::Rgb(102, 217, 239),
|
||||
mode_help: Color::Rgb(174, 129, 255),
|
||||
mode_visual: Color::Rgb(249, 38, 114),
|
||||
mode_command: Color::Rgb(230, 219, 116),
|
||||
selection_bg: Color::Rgb(117, 113, 94),
|
||||
selection_fg: Color::Rgb(248, 248, 242),
|
||||
cursor: Color::Rgb(249, 38, 114),
|
||||
placeholder: Color::Rgb(117, 113, 94),
|
||||
error: Color::Rgb(249, 38, 114),
|
||||
info: Color::Rgb(166, 226, 46),
|
||||
}
|
||||
}
|
||||
|
||||
/// Material Dark theme
|
||||
fn material_dark() -> Theme {
|
||||
Theme {
|
||||
name: "material-dark".to_string(),
|
||||
text: Color::Rgb(238, 255, 255), // #eeffff
|
||||
background: Color::Rgb(38, 50, 56), // #263238
|
||||
focused_panel_border: Color::Rgb(128, 203, 196), // #80cbc4 (cyan)
|
||||
unfocused_panel_border: Color::Rgb(84, 110, 122), // #546e7a
|
||||
user_message_role: Color::Rgb(130, 170, 255), // #82aaff (blue)
|
||||
assistant_message_role: Color::Rgb(199, 146, 234), // #c792ea (purple)
|
||||
thinking_panel_title: Color::Rgb(255, 203, 107), // #ffcb6b (yellow)
|
||||
command_bar_background: Color::Rgb(33, 43, 48),
|
||||
status_background: Color::Rgb(33, 43, 48),
|
||||
mode_normal: Color::Rgb(130, 170, 255),
|
||||
mode_editing: Color::Rgb(195, 232, 141), // #c3e88d (green)
|
||||
mode_model_selection: Color::Rgb(255, 203, 107),
|
||||
mode_provider_selection: Color::Rgb(128, 203, 196),
|
||||
mode_help: Color::Rgb(199, 146, 234),
|
||||
mode_visual: Color::Rgb(240, 113, 120), // #f07178 (red)
|
||||
mode_command: Color::Rgb(255, 203, 107),
|
||||
selection_bg: Color::Rgb(84, 110, 122),
|
||||
selection_fg: Color::Rgb(238, 255, 255),
|
||||
cursor: Color::Rgb(255, 204, 0),
|
||||
placeholder: Color::Rgb(84, 110, 122),
|
||||
error: Color::Rgb(240, 113, 120),
|
||||
info: Color::Rgb(195, 232, 141),
|
||||
}
|
||||
}
|
||||
|
||||
/// Material Light theme
|
||||
fn material_light() -> Theme {
|
||||
Theme {
|
||||
name: "material-light".to_string(),
|
||||
text: Color::Rgb(33, 33, 33),
|
||||
background: Color::Rgb(236, 239, 241),
|
||||
focused_panel_border: Color::Rgb(0, 150, 136),
|
||||
unfocused_panel_border: Color::Rgb(176, 190, 197),
|
||||
user_message_role: Color::Rgb(68, 138, 255),
|
||||
assistant_message_role: Color::Rgb(124, 77, 255),
|
||||
thinking_panel_title: Color::Rgb(245, 124, 0),
|
||||
command_bar_background: Color::Rgb(255, 255, 255),
|
||||
status_background: Color::Rgb(255, 255, 255),
|
||||
mode_normal: Color::Rgb(68, 138, 255),
|
||||
mode_editing: Color::Rgb(56, 142, 60),
|
||||
mode_model_selection: Color::Rgb(245, 124, 0),
|
||||
mode_provider_selection: Color::Rgb(0, 150, 136),
|
||||
mode_help: Color::Rgb(124, 77, 255),
|
||||
mode_visual: Color::Rgb(211, 47, 47),
|
||||
mode_command: Color::Rgb(245, 124, 0),
|
||||
selection_bg: Color::Rgb(176, 190, 197),
|
||||
selection_fg: Color::Rgb(33, 33, 33),
|
||||
cursor: Color::Rgb(194, 24, 91),
|
||||
placeholder: Color::Rgb(144, 164, 174),
|
||||
error: Color::Rgb(211, 47, 47),
|
||||
info: Color::Rgb(56, 142, 60),
|
||||
}
|
||||
}
|
||||
|
||||
// Helper functions for color serialization/deserialization
|
||||
|
||||
fn deserialize_color<'de, D>(deserializer: D) -> Result<Color, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
let s = String::deserialize(deserializer)?;
|
||||
parse_color(&s).map_err(serde::de::Error::custom)
|
||||
}
|
||||
|
||||
fn serialize_color<S>(color: &Color, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
let s = color_to_string(color);
|
||||
serializer.serialize_str(&s)
|
||||
}
|
||||
|
||||
fn parse_color(s: &str) -> Result<Color, String> {
|
||||
if let Some(hex) = s.strip_prefix('#') {
|
||||
if hex.len() == 6 {
|
||||
let r = u8::from_str_radix(&hex[0..2], 16)
|
||||
.map_err(|_| format!("Invalid hex color: {}", s))?;
|
||||
let g = u8::from_str_radix(&hex[2..4], 16)
|
||||
.map_err(|_| format!("Invalid hex color: {}", s))?;
|
||||
let b = u8::from_str_radix(&hex[4..6], 16)
|
||||
.map_err(|_| format!("Invalid hex color: {}", s))?;
|
||||
return Ok(Color::Rgb(r, g, b));
|
||||
}
|
||||
}
|
||||
|
||||
// Try named colors
|
||||
match s.to_lowercase().as_str() {
|
||||
"black" => Ok(Color::Black),
|
||||
"red" => Ok(Color::Red),
|
||||
"green" => Ok(Color::Green),
|
||||
"yellow" => Ok(Color::Yellow),
|
||||
"blue" => Ok(Color::Blue),
|
||||
"magenta" => Ok(Color::Magenta),
|
||||
"cyan" => Ok(Color::Cyan),
|
||||
"gray" | "grey" => Ok(Color::Gray),
|
||||
"darkgray" | "darkgrey" => Ok(Color::DarkGray),
|
||||
"lightred" => Ok(Color::LightRed),
|
||||
"lightgreen" => Ok(Color::LightGreen),
|
||||
"lightyellow" => Ok(Color::LightYellow),
|
||||
"lightblue" => Ok(Color::LightBlue),
|
||||
"lightmagenta" => Ok(Color::LightMagenta),
|
||||
"lightcyan" => Ok(Color::LightCyan),
|
||||
"white" => Ok(Color::White),
|
||||
_ => Err(format!("Unknown color: {}", s)),
|
||||
}
|
||||
}
|
||||
|
||||
fn color_to_string(color: &Color) -> String {
|
||||
match color {
|
||||
Color::Black => "black".to_string(),
|
||||
Color::Red => "red".to_string(),
|
||||
Color::Green => "green".to_string(),
|
||||
Color::Yellow => "yellow".to_string(),
|
||||
Color::Blue => "blue".to_string(),
|
||||
Color::Magenta => "magenta".to_string(),
|
||||
Color::Cyan => "cyan".to_string(),
|
||||
Color::Gray => "gray".to_string(),
|
||||
Color::DarkGray => "darkgray".to_string(),
|
||||
Color::LightRed => "lightred".to_string(),
|
||||
Color::LightGreen => "lightgreen".to_string(),
|
||||
Color::LightYellow => "lightyellow".to_string(),
|
||||
Color::LightBlue => "lightblue".to_string(),
|
||||
Color::LightMagenta => "lightmagenta".to_string(),
|
||||
Color::LightCyan => "lightcyan".to_string(),
|
||||
Color::White => "white".to_string(),
|
||||
Color::Rgb(r, g, b) => format!("#{:02x}{:02x}{:02x}", r, g, b),
|
||||
_ => "#ffffff".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_color_parsing() {
|
||||
assert!(matches!(parse_color("#ff0000"), Ok(Color::Rgb(255, 0, 0))));
|
||||
assert!(matches!(parse_color("red"), Ok(Color::Red)));
|
||||
assert!(matches!(parse_color("lightblue"), Ok(Color::LightBlue)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_built_in_themes() {
|
||||
let themes = built_in_themes();
|
||||
assert!(themes.contains_key("default_dark"));
|
||||
assert!(themes.contains_key("gruvbox"));
|
||||
assert!(themes.contains_key("dracula"));
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,7 @@ pub enum InputMode {
|
||||
Visual,
|
||||
Command,
|
||||
SessionBrowser,
|
||||
ThemeBrowser,
|
||||
}
|
||||
|
||||
impl fmt::Display for InputMode {
|
||||
@@ -36,6 +37,7 @@ impl fmt::Display for InputMode {
|
||||
InputMode::Visual => "Visual",
|
||||
InputMode::Command => "Command",
|
||||
InputMode::SessionBrowser => "Sessions",
|
||||
InputMode::ThemeBrowser => "Themes",
|
||||
};
|
||||
f.write_str(label)
|
||||
}
|
||||
|
||||
5
crates/owlen-gemini/README.md
Normal file
5
crates/owlen-gemini/README.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# Owlen Gemini
|
||||
|
||||
This crate is a placeholder for a future `owlen-core::Provider` implementation for the Google Gemini API.
|
||||
|
||||
This provider is not yet implemented. Contributions are welcome!
|
||||
9
crates/owlen-ollama/README.md
Normal file
9
crates/owlen-ollama/README.md
Normal file
@@ -0,0 +1,9 @@
|
||||
# Owlen Ollama
|
||||
|
||||
This crate provides an implementation of the `owlen-core::Provider` trait for the [Ollama](https://ollama.ai) backend.
|
||||
|
||||
It allows Owlen to communicate with a local Ollama instance, sending requests and receiving responses from locally-run large language models.
|
||||
|
||||
## Configuration
|
||||
|
||||
To use this provider, you need to have Ollama installed and running. The default address is `http://localhost:11434`. You can configure this in your `config.toml` if your Ollama instance is running elsewhere.
|
||||
5
crates/owlen-openai/README.md
Normal file
5
crates/owlen-openai/README.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# Owlen OpenAI
|
||||
|
||||
This crate is a placeholder for a future `owlen-core::Provider` implementation for the OpenAI API.
|
||||
|
||||
This provider is not yet implemented. Contributions are welcome!
|
||||
12
crates/owlen-tui/README.md
Normal file
12
crates/owlen-tui/README.md
Normal file
@@ -0,0 +1,12 @@
|
||||
# Owlen TUI
|
||||
|
||||
This crate contains all the logic for the terminal user interface (TUI) of Owlen.
|
||||
|
||||
It is built using the excellent [`ratatui`](https://ratatui.rs) library and is responsible for rendering the chat interface, handling user input, and managing the application state.
|
||||
|
||||
## Features
|
||||
|
||||
- **Chat View**: A scrollable view of the conversation history.
|
||||
- **Input Box**: A text input area for composing messages.
|
||||
- **Model Selection**: An interface for switching between different models.
|
||||
- **Event Handling**: A system for managing keyboard events and asynchronous operations.
|
||||
@@ -2,6 +2,7 @@ use anyhow::Result;
|
||||
use owlen_core::{
|
||||
session::{SessionController, SessionOutcome},
|
||||
storage::{SessionMeta, StorageManager},
|
||||
theme::Theme,
|
||||
types::{ChatParameters, ChatResponse, Conversation, ModelInfo, Role},
|
||||
ui::{AppState, AutoScroll, FocusedPanel, InputMode},
|
||||
};
|
||||
@@ -51,6 +52,8 @@ pub struct ChatApp {
|
||||
pending_key: Option<char>, // For multi-key sequences like gg, dd
|
||||
clipboard: String, // Vim-style clipboard for yank/paste
|
||||
command_buffer: String, // Buffer for command mode input
|
||||
command_suggestions: Vec<String>, // Filtered command suggestions based on current input
|
||||
selected_suggestion: usize, // Index of selected suggestion
|
||||
visual_start: Option<(usize, usize)>, // Visual mode selection start (row, col) for Input panel
|
||||
visual_end: Option<(usize, usize)>, // Visual mode selection end (row, col) for scrollable panels
|
||||
focused_panel: FocusedPanel, // Currently focused panel for scrolling
|
||||
@@ -59,7 +62,10 @@ pub struct ChatApp {
|
||||
storage: StorageManager, // Storage manager for session persistence
|
||||
saved_sessions: Vec<SessionMeta>, // Cached list of saved sessions
|
||||
selected_session_index: usize, // Index of selected session in browser
|
||||
save_name_buffer: String, // Buffer for entering save name
|
||||
help_tab_index: usize, // Currently selected help tab (0-4)
|
||||
theme: Theme, // Current theme
|
||||
available_themes: Vec<String>, // Cached list of theme names
|
||||
selected_theme_index: usize, // Index of selected theme in browser
|
||||
}
|
||||
|
||||
impl ChatApp {
|
||||
@@ -74,6 +80,13 @@ impl ChatApp {
|
||||
.expect("Failed to create fallback storage")
|
||||
});
|
||||
|
||||
// Load theme based on config
|
||||
let theme_name = &controller.config().ui.theme;
|
||||
let theme = owlen_core::theme::get_theme(theme_name).unwrap_or_else(|| {
|
||||
eprintln!("Warning: Theme '{}' not found, using default", theme_name);
|
||||
Theme::default()
|
||||
});
|
||||
|
||||
let app = Self {
|
||||
controller,
|
||||
mode: InputMode::Normal,
|
||||
@@ -99,6 +112,8 @@ impl ChatApp {
|
||||
pending_key: None,
|
||||
clipboard: String::new(),
|
||||
command_buffer: String::new(),
|
||||
command_suggestions: Vec::new(),
|
||||
selected_suggestion: 0,
|
||||
visual_start: None,
|
||||
visual_end: None,
|
||||
focused_panel: FocusedPanel::Input,
|
||||
@@ -107,7 +122,10 @@ impl ChatApp {
|
||||
storage,
|
||||
saved_sessions: Vec::new(),
|
||||
selected_session_index: 0,
|
||||
save_name_buffer: String::new(),
|
||||
help_tab_index: 0,
|
||||
theme,
|
||||
available_themes: Vec::new(),
|
||||
selected_theme_index: 0,
|
||||
};
|
||||
|
||||
(app, session_rx)
|
||||
@@ -204,6 +222,79 @@ impl ChatApp {
|
||||
&self.command_buffer
|
||||
}
|
||||
|
||||
pub fn command_suggestions(&self) -> &[String] {
|
||||
&self.command_suggestions
|
||||
}
|
||||
|
||||
pub fn selected_suggestion(&self) -> usize {
|
||||
self.selected_suggestion
|
||||
}
|
||||
|
||||
/// Returns all available commands with their aliases
|
||||
fn get_all_commands() -> Vec<(&'static str, &'static str)> {
|
||||
vec![
|
||||
("quit", "Exit the application"),
|
||||
("q", "Alias for quit"),
|
||||
("clear", "Clear the conversation"),
|
||||
("c", "Alias for clear"),
|
||||
("w", "Alias for write"),
|
||||
("save", "Alias for write"),
|
||||
("load", "Load a saved conversation"),
|
||||
("open", "Alias for load"),
|
||||
("o", "Alias for load"),
|
||||
("sessions", "List saved sessions"),
|
||||
("ls", "Alias for sessions"),
|
||||
("help", "Show help documentation"),
|
||||
("h", "Alias for help"),
|
||||
("model", "Select a model"),
|
||||
("m", "Alias for model"),
|
||||
("new", "Start a new conversation"),
|
||||
("n", "Alias for new"),
|
||||
("theme", "Switch theme"),
|
||||
("themes", "List available themes"),
|
||||
("reload", "Reload configuration and themes"),
|
||||
]
|
||||
}
|
||||
|
||||
/// Update command suggestions based on current input
|
||||
fn update_command_suggestions(&mut self) {
|
||||
let input = self.command_buffer.trim();
|
||||
|
||||
if input.is_empty() {
|
||||
// Show all commands when input is empty
|
||||
self.command_suggestions = Self::get_all_commands()
|
||||
.iter()
|
||||
.map(|(cmd, _)| cmd.to_string())
|
||||
.collect();
|
||||
} else {
|
||||
// Filter commands that start with the input
|
||||
self.command_suggestions = Self::get_all_commands()
|
||||
.iter()
|
||||
.filter_map(|(cmd, _)| {
|
||||
if cmd.starts_with(input) {
|
||||
Some(cmd.to_string())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
}
|
||||
|
||||
// Reset selection if out of bounds
|
||||
if self.selected_suggestion >= self.command_suggestions.len() {
|
||||
self.selected_suggestion = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/// Complete the current command with the selected suggestion
|
||||
fn complete_command(&mut self) {
|
||||
if let Some(suggestion) = self.command_suggestions.get(self.selected_suggestion) {
|
||||
self.command_buffer = suggestion.clone();
|
||||
self.update_command_suggestions();
|
||||
self.status = format!(":{}", self.command_buffer);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn focused_panel(&self) -> FocusedPanel {
|
||||
self.focused_panel
|
||||
}
|
||||
@@ -232,6 +323,43 @@ impl ChatApp {
|
||||
self.selected_session_index
|
||||
}
|
||||
|
||||
pub fn help_tab_index(&self) -> usize {
|
||||
self.help_tab_index
|
||||
}
|
||||
|
||||
pub fn available_themes(&self) -> &[String] {
|
||||
&self.available_themes
|
||||
}
|
||||
|
||||
pub fn selected_theme_index(&self) -> usize {
|
||||
self.selected_theme_index
|
||||
}
|
||||
|
||||
pub fn theme(&self) -> &Theme {
|
||||
&self.theme
|
||||
}
|
||||
|
||||
pub fn set_theme(&mut self, theme: Theme) {
|
||||
self.theme = theme;
|
||||
}
|
||||
|
||||
pub fn switch_theme(&mut self, theme_name: &str) -> Result<()> {
|
||||
if let Some(theme) = owlen_core::theme::get_theme(theme_name) {
|
||||
self.theme = theme;
|
||||
// Save theme to config
|
||||
self.controller.config_mut().ui.theme = theme_name.to_string();
|
||||
if let Err(err) = config::save_config(self.controller.config()) {
|
||||
self.error = Some(format!("Failed to save theme config: {}", err));
|
||||
} else {
|
||||
self.status = format!("Switched to theme: {}", theme_name);
|
||||
}
|
||||
Ok(())
|
||||
} else {
|
||||
self.error = Some(format!("Theme '{}' not found", theme_name));
|
||||
Err(anyhow::anyhow!("Theme '{}' not found", theme_name))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn cycle_focus_forward(&mut self) {
|
||||
self.focused_panel = match self.focused_panel {
|
||||
FocusedPanel::Chat => {
|
||||
@@ -410,6 +538,8 @@ impl ChatApp {
|
||||
(KeyCode::Char(':'), KeyModifiers::NONE) => {
|
||||
self.mode = InputMode::Command;
|
||||
self.command_buffer.clear();
|
||||
self.selected_suggestion = 0;
|
||||
self.update_command_suggestions();
|
||||
self.status = ":".to_string();
|
||||
}
|
||||
// Enter editing mode
|
||||
@@ -994,8 +1124,26 @@ impl ChatApp {
|
||||
(KeyCode::Esc, _) => {
|
||||
self.mode = InputMode::Normal;
|
||||
self.command_buffer.clear();
|
||||
self.command_suggestions.clear();
|
||||
self.reset_status();
|
||||
}
|
||||
(KeyCode::Tab, _) => {
|
||||
// Tab completion
|
||||
self.complete_command();
|
||||
}
|
||||
(KeyCode::Up, _) | (KeyCode::Char('k'), KeyModifiers::CONTROL) => {
|
||||
// Navigate up in suggestions
|
||||
if !self.command_suggestions.is_empty() {
|
||||
self.selected_suggestion = self.selected_suggestion.saturating_sub(1);
|
||||
}
|
||||
}
|
||||
(KeyCode::Down, _) | (KeyCode::Char('j'), KeyModifiers::CONTROL) => {
|
||||
// Navigate down in suggestions
|
||||
if !self.command_suggestions.is_empty() {
|
||||
self.selected_suggestion = (self.selected_suggestion + 1)
|
||||
.min(self.command_suggestions.len().saturating_sub(1));
|
||||
}
|
||||
}
|
||||
(KeyCode::Enter, _) => {
|
||||
// Execute command
|
||||
let cmd = self.command_buffer.trim();
|
||||
@@ -1020,18 +1168,30 @@ impl ChatApp {
|
||||
};
|
||||
|
||||
// Generate description if enabled in config
|
||||
let description = if self.controller.config().storage.generate_descriptions {
|
||||
self.status = "Generating description...".to_string();
|
||||
match self.controller.generate_conversation_description().await {
|
||||
Ok(desc) => Some(desc),
|
||||
Err(_) => None,
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let description =
|
||||
if self.controller.config().storage.generate_descriptions {
|
||||
self.status = "Generating description...".to_string();
|
||||
match self
|
||||
.controller
|
||||
.generate_conversation_description()
|
||||
.await
|
||||
{
|
||||
Ok(desc) => Some(desc),
|
||||
Err(_) => None,
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// Save the conversation with description
|
||||
match self.controller.conversation_mut().save_active_with_description(&self.storage, name.clone(), description) {
|
||||
match self
|
||||
.controller
|
||||
.conversation_mut()
|
||||
.save_active_with_description(
|
||||
&self.storage,
|
||||
name.clone(),
|
||||
description,
|
||||
) {
|
||||
Ok(path) => {
|
||||
self.status = format!("Session saved: {}", path.display());
|
||||
self.error = None;
|
||||
@@ -1041,7 +1201,7 @@ impl ChatApp {
|
||||
}
|
||||
}
|
||||
}
|
||||
"load" | "open" => {
|
||||
"load" | "open" | "o" => {
|
||||
// Load saved sessions and enter browser mode
|
||||
match self.storage.list_sessions() {
|
||||
Ok(sessions) => {
|
||||
@@ -1049,10 +1209,12 @@ impl ChatApp {
|
||||
self.selected_session_index = 0;
|
||||
self.mode = InputMode::SessionBrowser;
|
||||
self.command_buffer.clear();
|
||||
self.command_suggestions.clear();
|
||||
return Ok(AppState::Running);
|
||||
}
|
||||
Err(e) => {
|
||||
self.error = Some(format!("Failed to list sessions: {}", e));
|
||||
self.error =
|
||||
Some(format!("Failed to list sessions: {}", e));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1064,42 +1226,113 @@ impl ChatApp {
|
||||
self.selected_session_index = 0;
|
||||
self.mode = InputMode::SessionBrowser;
|
||||
self.command_buffer.clear();
|
||||
self.command_suggestions.clear();
|
||||
return Ok(AppState::Running);
|
||||
}
|
||||
Err(e) => {
|
||||
self.error = Some(format!("Failed to list sessions: {}", e));
|
||||
self.error =
|
||||
Some(format!("Failed to list sessions: {}", e));
|
||||
}
|
||||
}
|
||||
}
|
||||
"h" | "help" => {
|
||||
self.mode = InputMode::Help;
|
||||
self.command_buffer.clear();
|
||||
self.command_suggestions.clear();
|
||||
return Ok(AppState::Running);
|
||||
}
|
||||
"m" | "model" => {
|
||||
self.refresh_models().await?;
|
||||
self.mode = InputMode::ProviderSelection;
|
||||
self.command_buffer.clear();
|
||||
self.command_suggestions.clear();
|
||||
return Ok(AppState::Running);
|
||||
}
|
||||
"n" | "new" => {
|
||||
self.controller.start_new_conversation(None, None);
|
||||
self.status = "Started new conversation".to_string();
|
||||
}
|
||||
"theme" => {
|
||||
if args.is_empty() {
|
||||
self.error = Some("Usage: :theme <name>".to_string());
|
||||
} else {
|
||||
let theme_name = args.join(" ");
|
||||
match self.switch_theme(&theme_name) {
|
||||
Ok(_) => {
|
||||
// Success message already set by switch_theme
|
||||
}
|
||||
Err(_) => {
|
||||
// Error message already set by switch_theme
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"themes" => {
|
||||
// Load all themes and enter browser mode
|
||||
let themes = owlen_core::theme::load_all_themes();
|
||||
let mut theme_list: Vec<String> = themes.keys().cloned().collect();
|
||||
theme_list.sort();
|
||||
|
||||
self.available_themes = theme_list;
|
||||
|
||||
// Set selected index to current theme
|
||||
let current_theme = &self.theme.name;
|
||||
self.selected_theme_index = self
|
||||
.available_themes
|
||||
.iter()
|
||||
.position(|name| name == current_theme)
|
||||
.unwrap_or(0);
|
||||
|
||||
self.mode = InputMode::ThemeBrowser;
|
||||
self.command_buffer.clear();
|
||||
self.command_suggestions.clear();
|
||||
return Ok(AppState::Running);
|
||||
}
|
||||
"reload" => {
|
||||
// Reload config
|
||||
match owlen_core::config::Config::load(None) {
|
||||
Ok(new_config) => {
|
||||
// Update controller config
|
||||
*self.controller.config_mut() = new_config.clone();
|
||||
|
||||
// Reload theme based on updated config
|
||||
let theme_name = &new_config.ui.theme;
|
||||
if let Some(new_theme) =
|
||||
owlen_core::theme::get_theme(theme_name)
|
||||
{
|
||||
self.theme = new_theme;
|
||||
self.status = format!(
|
||||
"Configuration and theme reloaded (theme: {})",
|
||||
theme_name
|
||||
);
|
||||
} else {
|
||||
self.status = "Configuration reloaded, but theme not found. Using current theme.".to_string();
|
||||
}
|
||||
self.error = None;
|
||||
}
|
||||
Err(e) => {
|
||||
self.error =
|
||||
Some(format!("Failed to reload config: {}", e));
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
self.error = Some(format!("Unknown command: {}", cmd));
|
||||
}
|
||||
}
|
||||
self.command_buffer.clear();
|
||||
self.command_suggestions.clear();
|
||||
self.mode = InputMode::Normal;
|
||||
}
|
||||
(KeyCode::Char(c), KeyModifiers::NONE)
|
||||
| (KeyCode::Char(c), KeyModifiers::SHIFT) => {
|
||||
self.command_buffer.push(c);
|
||||
self.update_command_suggestions();
|
||||
self.status = format!(":{}", self.command_buffer);
|
||||
}
|
||||
(KeyCode::Backspace, _) => {
|
||||
self.command_buffer.pop();
|
||||
self.update_command_suggestions();
|
||||
self.status = format!(":{}", self.command_buffer);
|
||||
}
|
||||
_ => {}
|
||||
@@ -1175,9 +1408,28 @@ impl ChatApp {
|
||||
_ => {}
|
||||
},
|
||||
InputMode::Help => match key.code {
|
||||
KeyCode::Esc | KeyCode::Enter => {
|
||||
KeyCode::Esc | KeyCode::Enter | KeyCode::Char('q') => {
|
||||
self.mode = InputMode::Normal;
|
||||
self.help_tab_index = 0; // Reset to first tab
|
||||
}
|
||||
KeyCode::Tab | KeyCode::Right | KeyCode::Char('l') => {
|
||||
// Next tab
|
||||
if self.help_tab_index < 5 {
|
||||
self.help_tab_index += 1;
|
||||
}
|
||||
}
|
||||
KeyCode::BackTab | KeyCode::Left | KeyCode::Char('h') => {
|
||||
// Previous tab
|
||||
if self.help_tab_index > 0 {
|
||||
self.help_tab_index -= 1;
|
||||
}
|
||||
}
|
||||
KeyCode::Char('1') => self.help_tab_index = 0,
|
||||
KeyCode::Char('2') => self.help_tab_index = 1,
|
||||
KeyCode::Char('3') => self.help_tab_index = 2,
|
||||
KeyCode::Char('4') => self.help_tab_index = 3,
|
||||
KeyCode::Char('5') => self.help_tab_index = 4,
|
||||
KeyCode::Char('6') => self.help_tab_index = 5,
|
||||
_ => {}
|
||||
},
|
||||
InputMode::SessionBrowser => match key.code {
|
||||
@@ -1186,10 +1438,18 @@ impl ChatApp {
|
||||
}
|
||||
KeyCode::Enter => {
|
||||
// Load selected session
|
||||
if let Some(session) = self.saved_sessions.get(self.selected_session_index) {
|
||||
match self.controller.conversation_mut().load_from_disk(&self.storage, &session.path) {
|
||||
if let Some(session) = self.saved_sessions.get(self.selected_session_index)
|
||||
{
|
||||
match self
|
||||
.controller
|
||||
.conversation_mut()
|
||||
.load_from_disk(&self.storage, &session.path)
|
||||
{
|
||||
Ok(_) => {
|
||||
self.status = format!("Loaded session: {}", session.name.as_deref().unwrap_or("Unnamed"));
|
||||
self.status = format!(
|
||||
"Loaded session: {}",
|
||||
session.name.as_deref().unwrap_or("Unnamed")
|
||||
);
|
||||
self.error = None;
|
||||
// Update thinking panel
|
||||
self.update_thinking_from_last_message();
|
||||
@@ -1213,11 +1473,14 @@ impl ChatApp {
|
||||
}
|
||||
KeyCode::Char('d') => {
|
||||
// Delete selected session
|
||||
if let Some(session) = self.saved_sessions.get(self.selected_session_index) {
|
||||
if let Some(session) = self.saved_sessions.get(self.selected_session_index)
|
||||
{
|
||||
match self.storage.delete_session(&session.path) {
|
||||
Ok(_) => {
|
||||
self.saved_sessions.remove(self.selected_session_index);
|
||||
if self.selected_session_index >= self.saved_sessions.len() && !self.saved_sessions.is_empty() {
|
||||
if self.selected_session_index >= self.saved_sessions.len()
|
||||
&& !self.saved_sessions.is_empty()
|
||||
{
|
||||
self.selected_session_index = self.saved_sessions.len() - 1;
|
||||
}
|
||||
self.status = "Session deleted".to_string();
|
||||
@@ -1230,6 +1493,48 @@ impl ChatApp {
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
InputMode::ThemeBrowser => match key.code {
|
||||
KeyCode::Esc | KeyCode::Char('q') => {
|
||||
self.mode = InputMode::Normal;
|
||||
}
|
||||
KeyCode::Enter => {
|
||||
// Apply selected theme
|
||||
if let Some(theme_name) = self
|
||||
.available_themes
|
||||
.get(self.selected_theme_index)
|
||||
.cloned()
|
||||
{
|
||||
match self.switch_theme(&theme_name) {
|
||||
Ok(_) => {
|
||||
// Success message already set by switch_theme
|
||||
}
|
||||
Err(_) => {
|
||||
// Error message already set by switch_theme
|
||||
}
|
||||
}
|
||||
}
|
||||
self.mode = InputMode::Normal;
|
||||
}
|
||||
KeyCode::Up | KeyCode::Char('k') => {
|
||||
if self.selected_theme_index > 0 {
|
||||
self.selected_theme_index -= 1;
|
||||
}
|
||||
}
|
||||
KeyCode::Down | KeyCode::Char('j') => {
|
||||
if self.selected_theme_index + 1 < self.available_themes.len() {
|
||||
self.selected_theme_index += 1;
|
||||
}
|
||||
}
|
||||
KeyCode::Home | KeyCode::Char('g') => {
|
||||
self.selected_theme_index = 0;
|
||||
}
|
||||
KeyCode::End | KeyCode::Char('G') => {
|
||||
if !self.available_themes.is_empty() {
|
||||
self.selected_theme_index = self.available_themes.len() - 1;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,17 @@
|
||||
//! # Owlen TUI
|
||||
//!
|
||||
//! This crate contains all the logic for the terminal user interface (TUI) of Owlen.
|
||||
//!
|
||||
//! It is built using the excellent [`ratatui`](https://ratatui.rs) library and is responsible for
|
||||
//! rendering the chat interface, handling user input, and managing the application state.
|
||||
//!
|
||||
//! ## Modules
|
||||
//! - `chat_app`: The main application logic for the chat client.
|
||||
//! - `code_app`: The main application logic for the experimental code client.
|
||||
//! - `config`: TUI-specific configuration.
|
||||
//! - `events`: Event handling for user input and other asynchronous actions.
|
||||
//! - `ui`: The rendering logic for all TUI components.
|
||||
|
||||
pub mod chat_app;
|
||||
pub mod code_app;
|
||||
pub mod config;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
71
docs/architecture.md
Normal file
71
docs/architecture.md
Normal file
@@ -0,0 +1,71 @@
|
||||
# Owlen Architecture
|
||||
|
||||
This document provides a high-level overview of the Owlen architecture. Its purpose is to help developers understand how the different parts of the application fit together.
|
||||
|
||||
## Core Concepts
|
||||
|
||||
The architecture is designed to be modular and extensible, centered around a few key concepts:
|
||||
|
||||
- **Providers**: Connect to various LLM APIs (Ollama, OpenAI, etc.).
|
||||
- **Session**: Manages the conversation history and state.
|
||||
- **TUI**: The terminal user interface, built with `ratatui`.
|
||||
- **Events**: A system for handling user input and other events.
|
||||
|
||||
## Component Interaction
|
||||
|
||||
A simplified diagram of how components interact:
|
||||
|
||||
```
|
||||
[User Input] -> [Event Loop] -> [Session Controller] -> [Provider]
|
||||
^ |
|
||||
| v
|
||||
[TUI Renderer] <------------------------------------ [API Response]
|
||||
```
|
||||
|
||||
1. **User Input**: The user interacts with the TUI, generating events (e.g., key presses).
|
||||
2. **Event Loop**: The main event loop in `owlen-tui` captures these events.
|
||||
3. **Session Controller**: The event is processed, and if it's a prompt, the session controller sends a request to the current provider.
|
||||
4. **Provider**: The provider formats the request for the specific LLM API and sends it.
|
||||
5. **API Response**: The LLM API returns a response.
|
||||
6. **TUI Renderer**: The response is processed, the session state is updated, and the TUI is re-rendered to display the new information.
|
||||
|
||||
## Crate Breakdown
|
||||
|
||||
- `owlen-core`: Defines the core traits and data structures, like `Provider` and `Session`.
|
||||
- `owlen-tui`: Contains all the logic for the terminal user interface, including event handling and rendering.
|
||||
- `owlen-cli`: The command-line entry point, responsible for parsing arguments and starting the TUI.
|
||||
- `owlen-ollama` / `owlen-openai` / etc.: Implementations of the `Provider` trait for specific services.
|
||||
|
||||
## Session Management
|
||||
|
||||
The session management system is responsible for tracking the state of a conversation. The two main structs are:
|
||||
|
||||
- **`Conversation`**: Found in `owlen-core`, this struct holds the messages of a single conversation, the model being used, and other metadata. It is a simple data container.
|
||||
- **`SessionController`**: This is the high-level controller that manages the active conversation. It handles:
|
||||
- Storing and retrieving conversation history via the `ConversationManager`.
|
||||
- Managing the context that is sent to the LLM provider.
|
||||
- Switching between different models.
|
||||
- Sending requests to the provider and handling the responses (both streaming and complete).
|
||||
|
||||
When a user sends a message, the `SessionController` adds the message to the current `Conversation`, sends the updated message list to the `Provider`, and then adds the provider's response to the `Conversation`.
|
||||
|
||||
## Event Flow
|
||||
|
||||
The event flow is managed by the `EventHandler` in `owlen-tui`. It operates in a loop, waiting for events and dispatching them to the active application (`ChatApp` or `CodeApp`).
|
||||
|
||||
1. **Event Source**: Events are primarily generated by `crossterm` from user keyboard input. Asynchronous events, like responses from a `Provider`, are also fed into the event system via a `tokio::mpsc` channel.
|
||||
2. **`EventHandler::next()`**: The main application loop calls this method to wait for the next event.
|
||||
3. **Event Enum**: Events are defined in the `owlen_tui::events::Event` enum. This includes `Key` events, `Tick` events (for UI updates), and `Message` events (for async provider data).
|
||||
4. **Dispatch**: The application's `run` method matches on the `Event` type and calls the appropriate handler function (e.g., `dispatch_key_event`).
|
||||
5. **State Update**: The handler function updates the application state based on the event. For example, a key press might change the `InputMode` or modify the text in the input buffer.
|
||||
6. **Re-render**: After the state is updated, the UI is re-rendered to reflect the changes.
|
||||
|
||||
## TUI Rendering Pipeline
|
||||
|
||||
The TUI is rendered on each iteration of the main application loop in `owlen-tui`. The process is as follows:
|
||||
|
||||
1. **`tui.draw()`**: The main loop calls this method, passing the current application state.
|
||||
2. **`Terminal::draw()`**: This method, from `ratatui`, takes a closure that receives a `Frame`.
|
||||
3. **UI Composition**: Inside the closure, the UI is built by composing `ratatui` widgets. The root UI is defined in `owlen_tui::ui::render`, which builds the main layout and calls other functions to render specific components (like the chat panel, input box, etc.).
|
||||
4. **State-Driven Rendering**: Each rendering function takes the current application state as an argument. It uses this state to decide what and how to render. For example, the border color of a panel might change if it is focused.
|
||||
5. **Buffer and Diff**: `ratatui` does not draw directly to the terminal. Instead, it renders the widgets to an in-memory buffer. It then compares this buffer to the previous buffer and only sends the necessary changes to the terminal. This is highly efficient and prevents flickering.
|
||||
118
docs/configuration.md
Normal file
118
docs/configuration.md
Normal file
@@ -0,0 +1,118 @@
|
||||
# Owlen Configuration
|
||||
|
||||
Owlen uses a TOML file for configuration, allowing you to customize its behavior to your liking. This document details all the available options.
|
||||
|
||||
## File Location
|
||||
|
||||
By default, Owlen looks for its configuration file at `~/.config/owlen/config.toml`.
|
||||
|
||||
A default configuration file is created on the first run if one doesn't exist.
|
||||
|
||||
## Configuration Precedence
|
||||
|
||||
Configuration values are resolved in the following order:
|
||||
|
||||
1. **Defaults**: The application has hard-coded default values for all settings.
|
||||
2. **Configuration File**: Any values set in `config.toml` will override the defaults.
|
||||
3. **Command-Line Arguments / In-App Changes**: Any settings changed during runtime (e.g., via the `:theme` or `:model` commands) will override the configuration file for the current session. Some of these changes (like theme and model) are automatically saved back to the configuration file.
|
||||
|
||||
---
|
||||
|
||||
## General Settings (`[general]`)
|
||||
|
||||
These settings control the core behavior of the application.
|
||||
|
||||
- `default_provider` (string, default: `"ollama"`)
|
||||
The name of the provider to use by default.
|
||||
|
||||
- `default_model` (string, optional, default: `"llama3.2:latest"`)
|
||||
The default model to use for new conversations.
|
||||
|
||||
- `enable_streaming` (boolean, default: `true`)
|
||||
Whether to stream responses from the provider by default.
|
||||
|
||||
- `project_context_file` (string, optional, default: `"OWLEN.md"`)
|
||||
Path to a file whose content will be automatically injected as a system prompt. This is useful for providing project-specific context.
|
||||
|
||||
- `model_cache_ttl_secs` (integer, default: `60`)
|
||||
Time-to-live in seconds for the cached list of available models.
|
||||
|
||||
## UI Settings (`[ui]`)
|
||||
|
||||
These settings customize the look and feel of the terminal interface.
|
||||
|
||||
- `theme` (string, default: `"default_dark"`)
|
||||
The name of the theme to use. See the [Theming Guide](https://github.com/Owlibou/owlen/blob/main/themes/README.md) for available themes.
|
||||
|
||||
- `word_wrap` (boolean, default: `true`)
|
||||
Whether to wrap long lines in the chat view.
|
||||
|
||||
- `max_history_lines` (integer, default: `2000`)
|
||||
The maximum number of lines to keep in the scrollback buffer for the chat history.
|
||||
|
||||
- `show_role_labels` (boolean, default: `true`)
|
||||
Whether to show the `user` and `bot` role labels next to messages.
|
||||
|
||||
- `wrap_column` (integer, default: `100`)
|
||||
The column at which to wrap text if `word_wrap` is enabled.
|
||||
|
||||
## Storage Settings (`[storage]`)
|
||||
|
||||
These settings control how conversations are saved and loaded.
|
||||
|
||||
- `conversation_dir` (string, optional, default: platform-specific)
|
||||
The directory where conversation sessions are saved. If not set, a default directory is used:
|
||||
- **Linux**: `~/.local/share/owlen/sessions`
|
||||
- **Windows**: `%APPDATA%\owlen\sessions`
|
||||
- **macOS**: `~/Library/Application Support/owlen/sessions`
|
||||
|
||||
- `auto_save_sessions` (boolean, default: `true`)
|
||||
Whether to automatically save the session when the application exits.
|
||||
|
||||
- `max_saved_sessions` (integer, default: `25`)
|
||||
The maximum number of saved sessions to keep.
|
||||
|
||||
- `session_timeout_minutes` (integer, default: `120`)
|
||||
The number of minutes of inactivity before a session is considered for auto-saving as a new session.
|
||||
|
||||
- `generate_descriptions` (boolean, default: `true`)
|
||||
Whether to automatically generate a short summary of a conversation when saving it.
|
||||
|
||||
## Input Settings (`[input]`)
|
||||
|
||||
These settings control the behavior of the text input area.
|
||||
|
||||
- `multiline` (boolean, default: `true`)
|
||||
Whether to allow multi-line input.
|
||||
|
||||
- `history_size` (integer, default: `100`)
|
||||
The number of sent messages to keep in the input history (accessible with `Ctrl-Up/Down`).
|
||||
|
||||
- `tab_width` (integer, default: `4`)
|
||||
The number of spaces to insert when the `Tab` key is pressed.
|
||||
|
||||
- `confirm_send` (boolean, default: `false`)
|
||||
If true, requires an additional confirmation before sending a message.
|
||||
|
||||
## Provider Settings (`[providers]`)
|
||||
|
||||
This section contains a table for each provider you want to configure. The key is the provider name (e.g., `ollama`).
|
||||
|
||||
```toml
|
||||
[providers.ollama]
|
||||
provider_type = "ollama"
|
||||
base_url = "http://localhost:11434"
|
||||
# api_key = "..."
|
||||
```
|
||||
|
||||
- `provider_type` (string, required)
|
||||
The type of the provider. Currently, only `"ollama"` is built-in.
|
||||
|
||||
- `base_url` (string, optional)
|
||||
The base URL of the provider's API.
|
||||
|
||||
- `api_key` (string, optional)
|
||||
The API key to use for authentication, if required.
|
||||
|
||||
- `extra` (table, optional)
|
||||
Any additional, provider-specific parameters can be added here.
|
||||
42
docs/faq.md
Normal file
42
docs/faq.md
Normal file
@@ -0,0 +1,42 @@
|
||||
# Frequently Asked Questions (FAQ)
|
||||
|
||||
### What is the difference between `owlen` and `owlen-code`?
|
||||
|
||||
- `owlen` is the general-purpose chat client.
|
||||
- `owlen-code` is an experimental client with a system prompt that is optimized for programming and code-related questions. In the future, it will include more code-specific features like file context and syntax highlighting.
|
||||
|
||||
### How do I use Owlen with a different terminal?
|
||||
|
||||
Owlen is designed to work with most modern terminals that support 256 colors and Unicode. If you experience rendering issues, you might try:
|
||||
|
||||
- **WezTerm**: Excellent cross-platform, GPU-accelerated terminal.
|
||||
- **Alacritty**: Another fast, GPU-accelerated terminal.
|
||||
- **Kitty**: A feature-rich terminal emulator.
|
||||
|
||||
If issues persist, please open an issue and let us know what terminal you are using.
|
||||
|
||||
### What is the setup for Windows?
|
||||
|
||||
The Windows build is currently experimental. However, you can install it from source using `cargo` if you have the Rust toolchain installed.
|
||||
|
||||
1. Install Rust from [rustup.rs](https://rustup.rs).
|
||||
2. Install Git for Windows.
|
||||
3. Clone the repository: `git clone https://github.com/Owlibou/owlen.git`
|
||||
4. Install: `cd owlen && cargo install --path crates/owlen-cli`
|
||||
|
||||
Official binary releases for Windows are planned for the future.
|
||||
|
||||
### What is the setup for macOS?
|
||||
|
||||
Similar to Windows, the recommended installation method for macOS is to build from source using `cargo`.
|
||||
|
||||
1. Install the Xcode command-line tools: `xcode-select --install`
|
||||
2. Install Rust from [rustup.rs](https://rustup.rs).
|
||||
3. Clone the repository: `git clone https://github.com/Owlibou/owlen.git`
|
||||
4. Install: `cd owlen && cargo install --path crates/owlen-cli`
|
||||
|
||||
Official binary releases for macOS are planned.
|
||||
|
||||
### I'm getting connection failures to Ollama.
|
||||
|
||||
Please see the [Troubleshooting Guide](troubleshooting.md#connection-failures-to-ollama) for help with this common issue.
|
||||
34
docs/migration-guide.md
Normal file
34
docs/migration-guide.md
Normal file
@@ -0,0 +1,34 @@
|
||||
# Migration Guide
|
||||
|
||||
This guide documents breaking changes between versions of Owlen and provides instructions on how to migrate your configuration or usage.
|
||||
|
||||
As Owlen is currently in its alpha phase (pre-v1.0), breaking changes may occur more frequently. We will do our best to document them here.
|
||||
|
||||
---
|
||||
|
||||
## Migrating from v0.1.x to v0.2.x (Example)
|
||||
|
||||
*This is a template for a future migration. No breaking changes have occurred yet.*
|
||||
|
||||
Version 0.2.0 introduces a new configuration structure for providers.
|
||||
|
||||
### Configuration File Changes
|
||||
|
||||
Previously, your `config.toml` might have looked like this:
|
||||
|
||||
```toml
|
||||
# old config.toml (pre-v0.2.0)
|
||||
ollama_base_url = "http://localhost:11434"
|
||||
```
|
||||
|
||||
In v0.2.0, all provider settings are now nested under a `[providers]` table. You will need to update your `config.toml` to the new format:
|
||||
|
||||
```toml
|
||||
# new config.toml (v0.2.0+)
|
||||
[providers.ollama]
|
||||
base_url = "http://localhost:11434"
|
||||
```
|
||||
|
||||
### Action Required
|
||||
|
||||
Update your `~/.config/owlen/config.toml` to match the new structure. If you do not, Owlen will fall back to its default provider configuration.
|
||||
75
docs/provider-implementation.md
Normal file
75
docs/provider-implementation.md
Normal file
@@ -0,0 +1,75 @@
|
||||
# Provider Implementation Guide
|
||||
|
||||
This guide explains how to implement a new provider for Owlen. Providers are the components that connect to different LLM APIs.
|
||||
|
||||
## The `Provider` Trait
|
||||
|
||||
The core of the provider system is the `Provider` trait, located in `owlen-core`. Any new provider must implement this trait.
|
||||
|
||||
Here is a simplified version of the trait:
|
||||
|
||||
```rust
|
||||
use async_trait::async_trait;
|
||||
use owlen_core::model::Model;
|
||||
use owlen_core::session::Session;
|
||||
|
||||
#[async_trait]
|
||||
pub trait Provider {
|
||||
/// Returns the name of the provider.
|
||||
fn name(&self) -> &str;
|
||||
|
||||
/// Sends the session to the provider and returns the response.
|
||||
async fn chat(&self, session: &Session, model: &Model) -> Result<String, anyhow::Error>;
|
||||
}
|
||||
```
|
||||
|
||||
## Creating a New Crate
|
||||
|
||||
1. **Create a new crate** in the `crates/` directory. For example, `owlen-myprovider`.
|
||||
2. **Add dependencies** to your new crate's `Cargo.toml`. You will need `owlen-core`, `async-trait`, `tokio`, and any crates required for interacting with the new API (e.g., `reqwest`).
|
||||
3. **Add the new crate to the workspace** in the root `Cargo.toml`.
|
||||
|
||||
## Implementing the Trait
|
||||
|
||||
In your new crate's `lib.rs`, you will define a struct for your provider and implement the `Provider` trait for it.
|
||||
|
||||
```rust
|
||||
use async_trait::async_trait;
|
||||
use owlen_core::model::Model;
|
||||
use owlen_core::provider::Provider;
|
||||
use owlen_core::session::Session;
|
||||
|
||||
pub struct MyProvider;
|
||||
|
||||
#[async_trait]
|
||||
impl Provider for MyProvider {
|
||||
fn name(&self) -> &str {
|
||||
"my-provider"
|
||||
}
|
||||
|
||||
async fn chat(&self, session: &Session, model: &Model) -> Result<String, anyhow::Error> {
|
||||
// 1. Get the conversation history from the session.
|
||||
let history = session.get_messages();
|
||||
|
||||
// 2. Format the request for your provider's API.
|
||||
// This might involve creating a JSON body with the messages.
|
||||
|
||||
// 3. Send the request to the API using a client like reqwest.
|
||||
|
||||
// 4. Parse the response from the API.
|
||||
|
||||
// 5. Return the content of the response as a String.
|
||||
|
||||
Ok("Hello from my provider!".to_string())
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Integrating with Owlen
|
||||
|
||||
Once your provider is implemented, you will need to integrate it into the main Owlen application.
|
||||
|
||||
1. **Add your provider crate** as a dependency to `owlen-cli`.
|
||||
2. **In `owlen-cli`, modify the provider registration** to include your new provider. This will likely involve adding it to a list of available providers that the user can select from in the configuration.
|
||||
|
||||
This guide provides a basic outline. For more detailed examples, you can look at the existing provider implementations, such as `owlen-ollama`.
|
||||
58
docs/testing.md
Normal file
58
docs/testing.md
Normal file
@@ -0,0 +1,58 @@
|
||||
# Testing Guide
|
||||
|
||||
This guide provides instructions on how to run existing tests and how to write new tests for Owlen.
|
||||
|
||||
## Running Tests
|
||||
|
||||
The entire test suite can be run from the root of the repository using the standard `cargo test` command.
|
||||
|
||||
```sh
|
||||
# Run all tests in the workspace
|
||||
cargo test --all
|
||||
|
||||
# Run tests for a specific crate
|
||||
cargo test -p owlen-core
|
||||
```
|
||||
|
||||
We use `cargo clippy` for linting and `cargo fmt` for formatting. Please run these before submitting a pull request.
|
||||
|
||||
```sh
|
||||
cargo clippy --all -- -D warnings
|
||||
cargo fmt --all -- --check
|
||||
```
|
||||
|
||||
## Writing New Tests
|
||||
|
||||
Tests are located in the `tests/` directory within each crate, or in a `tests` module at the bottom of the file they are testing. We follow standard Rust testing practices.
|
||||
|
||||
### Unit Tests
|
||||
|
||||
For testing specific functions or components in isolation, use unit tests. These should be placed in a `#[cfg(test)]` module in the same file as the code being tested.
|
||||
|
||||
```rust
|
||||
// in src/my_module.rs
|
||||
|
||||
pub fn add(a: i32, b: i32) -> i32 {
|
||||
a + b
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_add() {
|
||||
assert_eq!(add(2, 2), 4);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Integration Tests
|
||||
|
||||
For testing how different parts of the application work together, use integration tests. These should be placed in the `tests/` directory of the crate.
|
||||
|
||||
For example, to test the `SessionController`, you might create a mock `Provider` and simulate sending messages, as seen in the `SessionController` documentation example.
|
||||
|
||||
### TUI and UI Component Tests
|
||||
|
||||
Testing TUI components can be challenging. For UI logic in `owlen-core` (like `wrap_cursor`), we have detailed unit tests that manipulate the component's state and assert the results. For higher-level TUI components in `owlen-tui`, the focus is on testing the state management logic rather than the visual output.
|
||||
40
docs/troubleshooting.md
Normal file
40
docs/troubleshooting.md
Normal file
@@ -0,0 +1,40 @@
|
||||
# Troubleshooting Guide
|
||||
|
||||
This guide is intended to help you with common issues you might encounter while using Owlen.
|
||||
|
||||
## Connection Failures to Ollama
|
||||
|
||||
If you are unable to connect to a local Ollama instance, here are a few things to check:
|
||||
|
||||
1. **Is Ollama running?** Make sure the Ollama service is active. You can usually check this with `ollama list`.
|
||||
2. **Is the address correct?** By default, Owlen tries to connect to `http://localhost:11434`. If your Ollama instance is running on a different address or port, you will need to configure it in your `config.toml` file.
|
||||
3. **Firewall issues:** Ensure that your firewall is not blocking the connection.
|
||||
|
||||
## Model Not Found Errors
|
||||
|
||||
If you get a "model not found" error, it means that the model you are trying to use is not available. For local providers like Ollama, you can use `ollama list` to see the models you have downloaded. Make sure the model name in your Owlen configuration matches one of the available models.
|
||||
|
||||
## Terminal Compatibility Issues
|
||||
|
||||
Owlen is built with `ratatui`, which supports most modern terminals. However, if you are experiencing rendering issues, please check the following:
|
||||
|
||||
- Your terminal supports Unicode.
|
||||
- You are using a font that includes the characters being displayed.
|
||||
- Try a different terminal emulator to see if the issue persists.
|
||||
|
||||
## Configuration File Problems
|
||||
|
||||
If Owlen is not behaving as you expect, there might be an issue with your configuration file.
|
||||
|
||||
- **Location:** The configuration file is typically located at `~/.config/owlen/config.toml`.
|
||||
- **Syntax:** The configuration file is in TOML format. Make sure the syntax is correct.
|
||||
- **Values:** Check that the values for your models, providers, and other settings are correct.
|
||||
|
||||
## Performance Tuning
|
||||
|
||||
If you are experiencing performance issues, you can try the following:
|
||||
|
||||
- **Reduce context size:** A smaller context size will result in faster responses from the LLM.
|
||||
- **Use a less resource-intensive model:** Some models are faster but less capable than others.
|
||||
|
||||
If you are still having trouble, please [open an issue](https://github.com/Owlibou/owlen/issues) on our GitHub repository.
|
||||
30
examples/basic_chat.rs
Normal file
30
examples/basic_chat.rs
Normal file
@@ -0,0 +1,30 @@
|
||||
// This example demonstrates a basic chat interaction without the TUI.
|
||||
|
||||
use owlen_core::model::Model;
|
||||
use owlen_core::session::Session;
|
||||
use owlen_ollama::OllamaProvider; // Assuming you have an Ollama provider
|
||||
use owlen_core::provider::Provider;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), anyhow::Error> {
|
||||
// This example requires a running Ollama instance.
|
||||
// Make sure you have a model available, e.g., `ollama pull llama2`
|
||||
|
||||
let provider = OllamaProvider;
|
||||
let model = Model::new("llama2"); // Change to a model you have
|
||||
let mut session = Session::new("basic-chat-session");
|
||||
|
||||
println!("Starting basic chat with model: {}", model.name);
|
||||
|
||||
let user_message = "What is the capital of France?";
|
||||
session.add_message("user", user_message);
|
||||
println!("User: {}", user_message);
|
||||
|
||||
// Send the chat to the provider
|
||||
let response = provider.chat(&session, &model).await?;
|
||||
|
||||
session.add_message("bot", &response);
|
||||
println!("Bot: {}", response);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
39
examples/custom_provider.rs
Normal file
39
examples/custom_provider.rs
Normal file
@@ -0,0 +1,39 @@
|
||||
// This example demonstrates how to implement a custom provider.
|
||||
|
||||
use async_trait::async_trait;
|
||||
use owlen_core::model::Model;
|
||||
use owlen_core::provider::Provider;
|
||||
use owlen_core::session::Session;
|
||||
|
||||
// Define a struct for your custom provider.
|
||||
pub struct MyCustomProvider;
|
||||
|
||||
// Implement the `Provider` trait for your struct.
|
||||
#[async_trait]
|
||||
impl Provider for MyCustomProvider {
|
||||
fn name(&self) -> &str {
|
||||
"custom-provider"
|
||||
}
|
||||
|
||||
async fn chat(&self, session: &Session, model: &Model) -> Result<String, anyhow::Error> {
|
||||
println!("Custom provider received chat request for model: {}", model.name);
|
||||
// In a real implementation, you would send the session data to an API.
|
||||
let message_count = session.get_messages().len();
|
||||
Ok(format!("This is a custom response. You have {} messages in your session.", message_count))
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), anyhow::Error> {
|
||||
let provider = MyCustomProvider;
|
||||
let model = Model::new("custom-model");
|
||||
let mut session = Session::new("custom-session");
|
||||
|
||||
session.add_message("user", "Hello, custom provider!");
|
||||
|
||||
let response = provider.chat(&session, &model).await?;
|
||||
|
||||
println!("Provider response: {}", response);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
28
examples/custom_theme.rs
Normal file
28
examples/custom_theme.rs
Normal file
@@ -0,0 +1,28 @@
|
||||
// This example demonstrates how to create a custom theme programmatically.
|
||||
|
||||
use owlen_core::theme::Theme;
|
||||
use ratatui::style::{Color, Style};
|
||||
|
||||
fn create_custom_theme() -> Theme {
|
||||
Theme {
|
||||
name: "My Custom Theme".to_string(),
|
||||
author: "Your Name".to_string(),
|
||||
comment: "A simple custom theme".to_string(),
|
||||
base: Style::default().fg(Color::White).bg(Color::Black),
|
||||
user_chat: Style::default().fg(Color::Green),
|
||||
bot_chat: Style::default().fg(Color::Cyan),
|
||||
error: Style::default().fg(Color::Red),
|
||||
info: Style::default().fg(Color::Yellow),
|
||||
border: Style::default().fg(Color::Gray),
|
||||
input: Style::default().fg(Color::White),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let custom_theme = create_custom_theme();
|
||||
|
||||
println!("Created custom theme: {}", custom_theme.name);
|
||||
println!("Author: {}", custom_theme.author);
|
||||
println!("User chat color: {:?}", custom_theme.user_chat.fg);
|
||||
}
|
||||
28
examples/session_management.rs
Normal file
28
examples/session_management.rs
Normal file
@@ -0,0 +1,28 @@
|
||||
// This example demonstrates how to use the session controller.
|
||||
|
||||
use owlen_core::session::Session;
|
||||
|
||||
fn main() {
|
||||
// Create a new session.
|
||||
let mut session = Session::new("my-session");
|
||||
println!("Created new session: {}", session.name);
|
||||
|
||||
// Add messages to the session.
|
||||
session.add_message("user", "Hello, Owlen!");
|
||||
session.add_message("bot", "Hello, user! How can I help you today?");
|
||||
|
||||
// Get the messages from the session.
|
||||
let messages = session.get_messages();
|
||||
println!("\nMessages in session:");
|
||||
for message in messages {
|
||||
println!(" {}: {}", message.role, message.content);
|
||||
}
|
||||
|
||||
// Clear the session.
|
||||
session.clear_messages();
|
||||
println!("\nSession cleared.");
|
||||
|
||||
let messages_after_clear = session.get_messages();
|
||||
println!("Messages in session after clear: {}", messages_after_clear.len());
|
||||
}
|
||||
|
||||
89
themes/README.md
Normal file
89
themes/README.md
Normal file
@@ -0,0 +1,89 @@
|
||||
# OWLEN Built-in Themes
|
||||
|
||||
This directory contains the built-in themes that are embedded into the OWLEN binary.
|
||||
|
||||
## Available Themes
|
||||
|
||||
- **default_dark** - High-contrast dark theme (default)
|
||||
- **default_light** - Clean light theme
|
||||
- **gruvbox** - Popular retro color scheme with warm tones
|
||||
- **dracula** - Dark theme with vibrant purple and cyan colors
|
||||
- **solarized** - Precision colors for optimal readability
|
||||
- **midnight-ocean** - Deep blue oceanic theme
|
||||
- **rose-pine** - Soho vibes with muted pastels
|
||||
- **monokai** - Classic code editor theme
|
||||
- **material-dark** - Google's Material Design dark variant
|
||||
- **material-light** - Google's Material Design light variant
|
||||
|
||||
## Theme File Format
|
||||
|
||||
Each theme is defined in TOML format with the following structure:
|
||||
|
||||
```toml
|
||||
name = "theme-name"
|
||||
|
||||
# Text colors
|
||||
text = "#ffffff" # Main text color
|
||||
placeholder = "#808080" # Placeholder/muted text
|
||||
|
||||
# Background colors
|
||||
background = "#000000" # Main background
|
||||
command_bar_background = "#111111"
|
||||
status_background = "#111111"
|
||||
|
||||
# Border colors
|
||||
focused_panel_border = "#ff00ff" # Active panel border
|
||||
unfocused_panel_border = "#800080" # Inactive panel border
|
||||
|
||||
# Message role colors
|
||||
user_message_role = "#00ffff" # User messages
|
||||
assistant_message_role = "#ffff00" # Assistant messages
|
||||
thinking_panel_title = "#ff00ff" # Thinking panel title
|
||||
|
||||
# Mode indicator colors (status bar)
|
||||
mode_normal = "#00ffff"
|
||||
mode_editing = "#00ff00"
|
||||
mode_model_selection = "#ffff00"
|
||||
mode_provider_selection = "#00ffff"
|
||||
mode_help = "#ff00ff"
|
||||
mode_visual = "#ff0080"
|
||||
mode_command = "#ffff00"
|
||||
|
||||
# Selection and cursor
|
||||
selection_bg = "#0000ff" # Selection background
|
||||
selection_fg = "#ffffff" # Selection foreground
|
||||
cursor = "#ff0080" # Cursor color
|
||||
|
||||
# Status colors
|
||||
error = "#ff0000" # Error messages
|
||||
info = "#00ff00" # Info/success messages
|
||||
```
|
||||
|
||||
## Color Format
|
||||
|
||||
Colors can be specified in two formats:
|
||||
|
||||
1. **Hex RGB**: `#rrggbb` (e.g., `#ff0000` for red, `#ff8800` for orange)
|
||||
2. **Named colors** (case-insensitive):
|
||||
- **Basic**: `black`, `red`, `green`, `yellow`, `blue`, `magenta`, `cyan`, `white`
|
||||
- **Gray variants**: `gray`, `grey`, `darkgray`, `darkgrey`
|
||||
- **Light variants**: `lightred`, `lightgreen`, `lightyellow`, `lightblue`, `lightmagenta`, `lightcyan`
|
||||
|
||||
**Note**: For colors not in the named list (like orange, purple, brown), use hex RGB format.
|
||||
|
||||
OWLEN will display an error message on startup if a custom theme has invalid colors.
|
||||
|
||||
## Creating Custom Themes
|
||||
|
||||
To create your own theme:
|
||||
|
||||
1. Copy one of these files to `~/.config/owlen/themes/`
|
||||
2. Rename and modify the colors
|
||||
3. Set `theme = "your-theme-name"` in `~/.config/owlen/config.toml`
|
||||
4. Or use `:theme your-theme-name` in OWLEN to switch
|
||||
|
||||
## Embedding in Binary
|
||||
|
||||
These theme files are embedded into the OWLEN binary at compile time using Rust's `include_str!()` macro. This ensures they're always available, even if the files are deleted from disk.
|
||||
|
||||
Custom themes placed in `~/.config/owlen/themes/` will override built-in themes with the same name.
|
||||
23
themes/default_dark.toml
Normal file
23
themes/default_dark.toml
Normal file
@@ -0,0 +1,23 @@
|
||||
name = "default_dark"
|
||||
text = "white"
|
||||
background = "black"
|
||||
focused_panel_border = "lightmagenta"
|
||||
unfocused_panel_border = "#5f1487"
|
||||
user_message_role = "lightblue"
|
||||
assistant_message_role = "yellow"
|
||||
thinking_panel_title = "lightmagenta"
|
||||
command_bar_background = "black"
|
||||
status_background = "black"
|
||||
mode_normal = "lightblue"
|
||||
mode_editing = "lightgreen"
|
||||
mode_model_selection = "lightyellow"
|
||||
mode_provider_selection = "lightcyan"
|
||||
mode_help = "lightmagenta"
|
||||
mode_visual = "magenta"
|
||||
mode_command = "yellow"
|
||||
selection_bg = "lightblue"
|
||||
selection_fg = "black"
|
||||
cursor = "magenta"
|
||||
placeholder = "darkgray"
|
||||
error = "red"
|
||||
info = "lightgreen"
|
||||
23
themes/default_light.toml
Normal file
23
themes/default_light.toml
Normal file
@@ -0,0 +1,23 @@
|
||||
name = "default_light"
|
||||
text = "black"
|
||||
background = "white"
|
||||
focused_panel_border = "#4a90e2"
|
||||
unfocused_panel_border = "#dddddd"
|
||||
user_message_role = "#0055a4"
|
||||
assistant_message_role = "#8e44ad"
|
||||
thinking_panel_title = "#8e44ad"
|
||||
command_bar_background = "white"
|
||||
status_background = "white"
|
||||
mode_normal = "#0055a4"
|
||||
mode_editing = "#2e8b57"
|
||||
mode_model_selection = "#b58900"
|
||||
mode_provider_selection = "#008b8b"
|
||||
mode_help = "#8e44ad"
|
||||
mode_visual = "#8e44ad"
|
||||
mode_command = "#b58900"
|
||||
selection_bg = "#a4c8f0"
|
||||
selection_fg = "black"
|
||||
cursor = "#d95f02"
|
||||
placeholder = "gray"
|
||||
error = "#c0392b"
|
||||
info = "green"
|
||||
23
themes/dracula.toml
Normal file
23
themes/dracula.toml
Normal file
@@ -0,0 +1,23 @@
|
||||
name = "dracula"
|
||||
text = "#f8f8f2"
|
||||
background = "#282a36"
|
||||
focused_panel_border = "#ff79c6"
|
||||
unfocused_panel_border = "#44475a"
|
||||
user_message_role = "#8be9fd"
|
||||
assistant_message_role = "#ff79c6"
|
||||
thinking_panel_title = "#bd93f9"
|
||||
command_bar_background = "#44475a"
|
||||
status_background = "#44475a"
|
||||
mode_normal = "#8be9fd"
|
||||
mode_editing = "#50fa7b"
|
||||
mode_model_selection = "#f1fa8c"
|
||||
mode_provider_selection = "#8be9fd"
|
||||
mode_help = "#bd93f9"
|
||||
mode_visual = "#ff79c6"
|
||||
mode_command = "#f1fa8c"
|
||||
selection_bg = "#44475a"
|
||||
selection_fg = "#f8f8f2"
|
||||
cursor = "#ff79c6"
|
||||
placeholder = "#6272a4"
|
||||
error = "#ff5555"
|
||||
info = "#50fa7b"
|
||||
23
themes/gruvbox.toml
Normal file
23
themes/gruvbox.toml
Normal file
@@ -0,0 +1,23 @@
|
||||
name = "gruvbox"
|
||||
text = "#ebdbb2"
|
||||
background = "#282828"
|
||||
focused_panel_border = "#fe8019"
|
||||
unfocused_panel_border = "#7c6f64"
|
||||
user_message_role = "#b8bb26"
|
||||
assistant_message_role = "#83a598"
|
||||
thinking_panel_title = "#d3869b"
|
||||
command_bar_background = "#3c3836"
|
||||
status_background = "#3c3836"
|
||||
mode_normal = "#83a598"
|
||||
mode_editing = "#b8bb26"
|
||||
mode_model_selection = "#fabd2f"
|
||||
mode_provider_selection = "#8ec07c"
|
||||
mode_help = "#d3869b"
|
||||
mode_visual = "#fe8019"
|
||||
mode_command = "#fabd2f"
|
||||
selection_bg = "#504945"
|
||||
selection_fg = "#ebdbb2"
|
||||
cursor = "#fe8019"
|
||||
placeholder = "#665c54"
|
||||
error = "#fb4934"
|
||||
info = "#b8bb26"
|
||||
23
themes/material-dark.toml
Normal file
23
themes/material-dark.toml
Normal file
@@ -0,0 +1,23 @@
|
||||
name = "material-dark"
|
||||
text = "#eeffff"
|
||||
background = "#263238"
|
||||
focused_panel_border = "#80cbc4"
|
||||
unfocused_panel_border = "#546e7a"
|
||||
user_message_role = "#82aaff"
|
||||
assistant_message_role = "#c792ea"
|
||||
thinking_panel_title = "#ffcb6b"
|
||||
command_bar_background = "#212b30"
|
||||
status_background = "#212b30"
|
||||
mode_normal = "#82aaff"
|
||||
mode_editing = "#c3e88d"
|
||||
mode_model_selection = "#ffcb6b"
|
||||
mode_provider_selection = "#80cbc4"
|
||||
mode_help = "#c792ea"
|
||||
mode_visual = "#f07178"
|
||||
mode_command = "#ffcb6b"
|
||||
selection_bg = "#546e7a"
|
||||
selection_fg = "#eeffff"
|
||||
cursor = "#ffcc00"
|
||||
placeholder = "#546e7a"
|
||||
error = "#f07178"
|
||||
info = "#c3e88d"
|
||||
23
themes/material-light.toml
Normal file
23
themes/material-light.toml
Normal file
@@ -0,0 +1,23 @@
|
||||
name = "material-light"
|
||||
text = "#212121"
|
||||
background = "#eceff1"
|
||||
focused_panel_border = "#009688"
|
||||
unfocused_panel_border = "#b0bec5"
|
||||
user_message_role = "#448aff"
|
||||
assistant_message_role = "#7c4dff"
|
||||
thinking_panel_title = "#f57c00"
|
||||
command_bar_background = "#ffffff"
|
||||
status_background = "#ffffff"
|
||||
mode_normal = "#448aff"
|
||||
mode_editing = "#388e3c"
|
||||
mode_model_selection = "#f57c00"
|
||||
mode_provider_selection = "#009688"
|
||||
mode_help = "#7c4dff"
|
||||
mode_visual = "#d32f2f"
|
||||
mode_command = "#f57c00"
|
||||
selection_bg = "#b0bec5"
|
||||
selection_fg = "#212121"
|
||||
cursor = "#c2185b"
|
||||
placeholder = "#90a4ae"
|
||||
error = "#d32f2f"
|
||||
info = "#388e3c"
|
||||
23
themes/midnight-ocean.toml
Normal file
23
themes/midnight-ocean.toml
Normal file
@@ -0,0 +1,23 @@
|
||||
name = "midnight-ocean"
|
||||
text = "#c0caf5"
|
||||
background = "#0d1117"
|
||||
focused_panel_border = "#58a6ff"
|
||||
unfocused_panel_border = "#30363d"
|
||||
user_message_role = "#79c0ff"
|
||||
assistant_message_role = "#89ddff"
|
||||
thinking_panel_title = "#9ece6a"
|
||||
command_bar_background = "#161b22"
|
||||
status_background = "#161b22"
|
||||
mode_normal = "#79c0ff"
|
||||
mode_editing = "#9ece6a"
|
||||
mode_model_selection = "#ffd43b"
|
||||
mode_provider_selection = "#89ddff"
|
||||
mode_help = "#ff739d"
|
||||
mode_visual = "#f68cf5"
|
||||
mode_command = "#ffd43b"
|
||||
selection_bg = "#388bfd"
|
||||
selection_fg = "#0d1117"
|
||||
cursor = "#f68cf5"
|
||||
placeholder = "#6e7681"
|
||||
error = "#f85149"
|
||||
info = "#9ece6a"
|
||||
23
themes/monokai.toml
Normal file
23
themes/monokai.toml
Normal file
@@ -0,0 +1,23 @@
|
||||
name = "monokai"
|
||||
text = "#f8f8f2"
|
||||
background = "#272822"
|
||||
focused_panel_border = "#f92672"
|
||||
unfocused_panel_border = "#75715e"
|
||||
user_message_role = "#66d9ef"
|
||||
assistant_message_role = "#ae81ff"
|
||||
thinking_panel_title = "#e6db74"
|
||||
command_bar_background = "#272822"
|
||||
status_background = "#272822"
|
||||
mode_normal = "#66d9ef"
|
||||
mode_editing = "#a6e22e"
|
||||
mode_model_selection = "#e6db74"
|
||||
mode_provider_selection = "#66d9ef"
|
||||
mode_help = "#ae81ff"
|
||||
mode_visual = "#f92672"
|
||||
mode_command = "#e6db74"
|
||||
selection_bg = "#75715e"
|
||||
selection_fg = "#f8f8f2"
|
||||
cursor = "#f92672"
|
||||
placeholder = "#75715e"
|
||||
error = "#f92672"
|
||||
info = "#a6e22e"
|
||||
23
themes/rose-pine.toml
Normal file
23
themes/rose-pine.toml
Normal file
@@ -0,0 +1,23 @@
|
||||
name = "rose-pine"
|
||||
text = "#e0def4"
|
||||
background = "#191724"
|
||||
focused_panel_border = "#eb6f92"
|
||||
unfocused_panel_border = "#26233a"
|
||||
user_message_role = "#31748f"
|
||||
assistant_message_role = "#9ccfd8"
|
||||
thinking_panel_title = "#c4a7e7"
|
||||
command_bar_background = "#26233a"
|
||||
status_background = "#26233a"
|
||||
mode_normal = "#9ccfd8"
|
||||
mode_editing = "#ebbcba"
|
||||
mode_model_selection = "#f6c177"
|
||||
mode_provider_selection = "#31748f"
|
||||
mode_help = "#c4a7e7"
|
||||
mode_visual = "#eb6f92"
|
||||
mode_command = "#f6c177"
|
||||
selection_bg = "#403d52"
|
||||
selection_fg = "#e0def4"
|
||||
cursor = "#eb6f92"
|
||||
placeholder = "#6e6a86"
|
||||
error = "#eb6f92"
|
||||
info = "#9ccfd8"
|
||||
23
themes/solarized.toml
Normal file
23
themes/solarized.toml
Normal file
@@ -0,0 +1,23 @@
|
||||
name = "solarized"
|
||||
text = "#839496"
|
||||
background = "#002b36"
|
||||
focused_panel_border = "#268bd2"
|
||||
unfocused_panel_border = "#073642"
|
||||
user_message_role = "#2aa198"
|
||||
assistant_message_role = "#cb4b16"
|
||||
thinking_panel_title = "#6c71c4"
|
||||
command_bar_background = "#073642"
|
||||
status_background = "#073642"
|
||||
mode_normal = "#268bd2"
|
||||
mode_editing = "#859900"
|
||||
mode_model_selection = "#b58900"
|
||||
mode_provider_selection = "#2aa198"
|
||||
mode_help = "#6c71c4"
|
||||
mode_visual = "#d33682"
|
||||
mode_command = "#b58900"
|
||||
selection_bg = "#073642"
|
||||
selection_fg = "#93a1a1"
|
||||
cursor = "#d33682"
|
||||
placeholder = "#586e75"
|
||||
error = "#dc322f"
|
||||
info = "#859900"
|
||||
Reference in New Issue
Block a user