Compare commits

..
15 Commits
Author SHA1 Message Date
Aiden ClineandGitHub e14de7a211 fix: tweak the logic in config to ensure that env vs file configurations merge properly (#115)
release / goreleaser (push) Has been cancelled
2025-05-01 13:22:48 +02:00
Garrett LadleyandGitHub 004cfe7e8e feat: test for getContextFromPaths (#105)
release / goreleaser (push) Has been cancelled
* feat: test for getContextFromPaths

* fix: use testify
2025-05-01 12:55:28 +02:00
AdamandGitHub 58705a1352 fix: more intuitive keybinds (#121) 2025-05-01 12:51:07 +02:00
82de14371d feat: themes (#113)
* feat: themes

* feat: flexoki theme

* feat: onedark theme

* feat: monokai pro theme

* feat: opencode theme (default)

* feat: dracula theme

* feat: tokyonight theme

* feat: tron theme

* some small fixes

---------

Co-authored-by: Kujtim Hoxha <kujtimii.h@gmail.com>
2025-05-01 12:49:26 +02:00
AdamandGitHub 61d9dc9511 fix: allow text selection (#127) 2025-04-30 12:52:30 +02:00
Hunter CastenandGitHub 76275e533e fix(openrouter): set api key from env (#129) 2025-04-30 12:50:57 +02:00
Isaac ScarrottandGitHub 98e2910e82 feat: Add support for OpenRouter (#92)
release / goreleaser (push) Has been cancelled
* Add support for OpenRouter as a new model provider

- Introduced `ProviderOpenRouter` in the `models` package.
- Added OpenRouter-specific models, including `GPT41`, `GPT41Mini`, `GPT4o`, and others, with their configurations and costs.
- Updated `generateSchema` to include OpenRouter as a provider.
- Added OpenRouter-specific environment variable handling (`OPENROUTER_API_KEY`) in `config.go`.
- Implemented default model settings for OpenRouter agents in `setDefaultModelForAgent`.
- Updated `getProviderAPIKey` to retrieve the OpenRouter API key.
- Extended `SupportedModels` to include OpenRouter models.
- Added OpenRouter client initialization in the `provider` package.
- Modified `processGeneration` to handle `FinishReasonUnknown` in addition to `FinishReasonToolUse`.

* [feature/openrouter-provider] Add new models and provider to schema

- Added "deepseek-chat-free" and "deepseek-r1-free" to the list of supported models in `opencode-schema.json`.

* [feature/openrouter-provider] Add OpenRouter provider support and integrate new models

- Updated README.md to include OpenRouter as a supported provider and its configuration details.
- Added `OPENROUTER_API_KEY` to environment variable configuration.
- Introduced OpenRouter-specific models in `internal/llm/models/openrouter.go` with mappings to existing cost and token configurations.
- Updated `internal/config/config.go` to set default models for OpenRouter agents.
- Extended `opencode-schema.json` to include OpenRouter models in the schema definitions.
- Refactored model IDs and names to align with OpenRouter naming conventions.

* [feature/openrouter-provider] Refactor finish reason handling and tool call logic in agent and OpenAI provider

- Simplified finish reason check in `agent.go` by removing redundant variable assignment.
- Updated `openai.go` to override the finish reason to `FinishReasonToolUse` when tool calls are present.
- Ensured consistent finish reason handling in both `send` and `stream` methods of the OpenAI provider.

[feature/openrouter-provider] Refactor finish reason handling and tool call logic in agent and OpenAI provider

- Simplified finish reason check in `agent.go` by removing redundant variable assignment.
- Updated `openai.go` to override the finish reason to `FinishReasonToolUse` when tool calls are present.
- Ensured consistent finish reason handling in both `send` and `stream` methods of the OpenAI provider.

* **[feature/openrouter-provider] Add support for custom headers in OpenAI client configuration**

- Introduced a new `extraHeaders` field in the `openaiOptions` struct to allow specifying additional HTTP headers.
- Added logic in `newOpenAIClient` to apply `extraHeaders` to the OpenAI client configuration.
- Implemented a new option function `WithOpenAIExtraHeaders` to set custom headers in `openaiOptions`.
- Updated the OpenRouter provider configuration in `NewProvider` to include default headers (`HTTP-Referer` and `X-Title`) for OpenRouter API requests.

* Update OpenRouter model config and remove unsupported models

* [feature/openrouter-provider] Update OpenRouter models and default configurations

- Added new OpenRouter models: `claude-3.5-sonnet`, `claude-3-haiku`, `claude-3.7-sonnet`, `claude-3.5-haiku`, and `claude-3-opus` in `openrouter.go`.
- Updated default agent models in `config.go`:
  - `agents.coder.model` now uses `claude-3.7-sonnet`.
  - `agents.task.model` now uses `claude-3.7-sonnet`.
  - `agents.title.model` now uses `claude-3.5-haiku`.
- Updated `opencode-schema.json` to include the new models in the allowed list for schema validation.
- Adjusted logic in `setDefaultModelForAgent` to reflect the new default models.

* [feature/openrouter-provider] Remove unused ProviderEvent emission in stream function

The changes remove the emission of a `ProviderEvent` with type `EventContentStop` in the `stream` function of the `openaiClient` implementation. This event was sent upon successful stream completion but is no longer used.
2025-04-29 13:56:49 +02:00
Kujtim Hoxha 2941137416 fix diagnostics for deleted files 2025-04-28 19:37:42 +02:00
Aiden ClineandGitHub b3c0285db3 feat: model selection for given provider (#57)
release / goreleaser (push) Has been cancelled
* feat: model selection for given provider

* tweak: adjust cfg validation func, remove duplicated logic, consolidate agent updating into agent.go

* tweak: make the model dialog scrollable, adjust padding slightly for modal"

* feat: add provider selection, add hints, simplify some logic, add horizontal scrolling support, additional scroll indicators"

* remove nav help

* update docs

* increase number of visible models, make horizontal scroll "wrap"

* add provider popularity rankings
2025-04-28 19:25:06 +02:00
YJGandGitHub 805aeff83c feat: add azure openai models (#74) 2025-04-28 15:42:57 +02:00
Kujtim Hoxha bce2ec5c10 fix duplicate context
release / goreleaser (push) Has been cancelled
2025-04-27 20:43:27 +02:00
Kujtim Hoxha 292e9d90ca remove unnecessary var 2025-04-27 20:34:20 +02:00
Kujtim Hoxha 2b4441a0d1 fix context
release / goreleaser (push) Has been cancelled
2025-04-27 20:31:53 +02:00
Garrett LadleyandGitHub 8f3a94df92 feat: configure context paths (#86)
release / goreleaser (push) Has been cancelled
2025-04-27 20:11:09 +02:00
Kujtim Hoxha 4415220555 fix minor issue
release / goreleaser (push) Has been cancelled
2025-04-27 19:24:46 +02:00
59 changed files with 6089 additions and 2069 deletions
+41 -13
View File
@@ -11,7 +11,7 @@ OpenCode is a Go-based CLI application that brings AI assistance to your termina
## Features
- **Interactive TUI**: Built with [Bubble Tea](https://github.com/charmbracelet/bubbletea) for a smooth terminal experience
- **Multiple AI Providers**: Support for OpenAI, Anthropic Claude, Google Gemini, AWS Bedrock, and Groq
- **Multiple AI Providers**: Support for OpenAI, Anthropic Claude, Google Gemini, AWS Bedrock, Groq, Azure OpenAI, and OpenRouter
- **Session Management**: Save and manage multiple conversation sessions
- **Tool Integration**: AI can execute commands, search files, and modify code
- **Vim-like Editor**: Integrated editor with text input capabilities
@@ -66,15 +66,19 @@ OpenCode looks for configuration in the following locations:
You can configure OpenCode using environment variables:
| Environment Variable | Purpose |
| ----------------------- | ------------------------ |
| `ANTHROPIC_API_KEY` | For Claude models |
| `OPENAI_API_KEY` | For OpenAI models |
| `GEMINI_API_KEY` | For Google Gemini models |
| `GROQ_API_KEY` | For Groq models |
| `AWS_ACCESS_KEY_ID` | For AWS Bedrock (Claude) |
| `AWS_SECRET_ACCESS_KEY` | For AWS Bedrock (Claude) |
| `AWS_REGION` | For AWS Bedrock (Claude) |
| Environment Variable | Purpose |
|----------------------------|--------------------------------------------------------|
| `ANTHROPIC_API_KEY` | For Claude models |
| `OPENAI_API_KEY` | For OpenAI models |
| `GEMINI_API_KEY` | For Google Gemini models |
| `GROQ_API_KEY` | For Groq models |
| `AWS_ACCESS_KEY_ID` | For AWS Bedrock (Claude) |
| `AWS_SECRET_ACCESS_KEY` | For AWS Bedrock (Claude) |
| `AWS_REGION` | For AWS Bedrock (Claude) |
| `AZURE_OPENAI_ENDPOINT` | For Azure OpenAI models |
| `AZURE_OPENAI_API_KEY` | For Azure OpenAI models (optional when using Entra ID) |
| `AZURE_OPENAI_API_VERSION` | For Azure OpenAI models |
### Configuration File Structure
@@ -93,8 +97,12 @@ You can configure OpenCode using environment variables:
"disabled": false
},
"groq": {
"apiKey": "your-api-key",
"disabled": false
"apiKey": "your-api-key",
"disabled": false
},
"openrouter": {
"apiKey": "your-api-key",
"disabled": false
}
},
"agents": {
@@ -164,12 +172,21 @@ OpenCode supports a variety of AI models from different providers:
### Groq
- Llama 4 Maverick (17b-128e-instruct)
- Llama 4 Maverick (17b-128e-instruct)
- Llama 4 Scout (17b-16e-instruct)
- QWEN QWQ-32b
- Deepseek R1 distill Llama 70b
- Llama 3.3 70b Versatile
### Azure OpenAI
- GPT-4.1 family (gpt-4.1, gpt-4.1-mini, gpt-4.1-nano)
- GPT-4.5 Preview
- GPT-4o family (gpt-4o, gpt-4o-mini)
- O1 family (o1, o1-mini)
- O3 family (o3, o3-mini)
- O4 Mini
## Usage
```bash
@@ -203,6 +220,7 @@ opencode -c /path/to/project
| `Ctrl+L` | View logs |
| `Ctrl+A` | Switch session |
| `Ctrl+K` | Command dialog |
| `Ctrl+O` | Toggle model selection dialog |
| `Esc` | Close current overlay/dialog or return to previous mode |
### Chat Page Shortcuts
@@ -232,6 +250,16 @@ opencode -c /path/to/project
| `Enter` | Select session |
| `Esc` | Close dialog |
### Model Dialog Shortcuts
| Shortcut | Action |
| ---------- | ----------------- |
| `↑` or `k` | Move up |
| `↓` or `j` | Move down |
| `←` or `h` | Previous provider |
| `→` or `l` | Next provider |
| `Esc` | Close dialog |
### Permission Dialog Shortcuts
| Shortcut | Action |
+1 -2
View File
@@ -8,6 +8,7 @@ import (
"time"
tea "github.com/charmbracelet/bubbletea"
zone "github.com/lrstanley/bubblezone"
"github.com/opencode-ai/opencode/internal/app"
"github.com/opencode-ai/opencode/internal/config"
"github.com/opencode-ai/opencode/internal/db"
@@ -16,7 +17,6 @@ import (
"github.com/opencode-ai/opencode/internal/pubsub"
"github.com/opencode-ai/opencode/internal/tui"
"github.com/opencode-ai/opencode/internal/version"
zone "github.com/lrstanley/bubblezone"
"github.com/spf13/cobra"
)
@@ -79,7 +79,6 @@ to assist developers in writing, debugging, and understanding code directly from
program := tea.NewProgram(
tui.New(app),
tea.WithAltScreen(),
tea.WithMouseCellMotion(),
)
// Initialize MCP tools in the background
+46 -1
View File
@@ -77,6 +77,50 @@ func generateSchema() map[string]any {
"default": false,
}
schema["properties"].(map[string]any)["contextPaths"] = map[string]any{
"type": "array",
"description": "Context paths for the application",
"items": map[string]any{
"type": "string",
},
"default": []string{
".github/copilot-instructions.md",
".cursorrules",
".cursor/rules/",
"CLAUDE.md",
"CLAUDE.local.md",
"opencode.md",
"opencode.local.md",
"OpenCode.md",
"OpenCode.local.md",
"OPENCODE.md",
"OPENCODE.local.md",
},
}
schema["properties"].(map[string]any)["tui"] = map[string]any{
"type": "object",
"description": "Terminal User Interface configuration",
"properties": map[string]any{
"theme": map[string]any{
"type": "string",
"description": "TUI theme name",
"default": "opencode",
"enum": []string{
"opencode",
"catppuccin",
"dracula",
"flexoki",
"gruvbox",
"monokai",
"onedark",
"tokyonight",
"tron",
},
},
},
}
// Add MCP servers
schema["properties"].(map[string]any)["mcpServers"] = map[string]any{
"type": "object",
@@ -152,7 +196,9 @@ func generateSchema() map[string]any {
string(models.ProviderOpenAI),
string(models.ProviderGemini),
string(models.ProviderGROQ),
string(models.ProviderOpenRouter),
string(models.ProviderBedrock),
string(models.ProviderAzure),
}
providerSchema["additionalProperties"].(map[string]any)["properties"].(map[string]any)["provider"] = map[string]any{
@@ -259,4 +305,3 @@ func generateSchema() map[string]any {
return schema
}
+7
View File
@@ -5,6 +5,7 @@ go 1.24.0
toolchain go1.24.2
require (
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.7.0
github.com/JohannesKaufmann/html-to-markdown v1.6.0
github.com/PuerkitoBio/goquery v1.9.2
github.com/alecthomas/chroma/v2 v2.15.0
@@ -44,6 +45,9 @@ require (
cloud.google.com/go/auth/oauth2adapt v0.2.6 // indirect
cloud.google.com/go/compute/metadata v0.6.0 // indirect
cloud.google.com/go/longrunning v0.5.7 // indirect
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 // indirect
github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 // indirect
github.com/andybalholm/cascadia v1.3.2 // indirect
github.com/atotto/clipboard v0.1.4 // indirect
github.com/aws/aws-sdk-go-v2 v1.30.3 // indirect
@@ -74,11 +78,13 @@ require (
github.com/go-logr/logr v1.4.2 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
github.com/golang-jwt/jwt/v5 v5.2.2 // indirect
github.com/google/s2a-go v0.1.8 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect
github.com/googleapis/gax-go/v2 v2.14.1 // indirect
github.com/gorilla/css v1.0.1 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/kylelemons/godebug v1.1.0 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-localereader v0.0.1 // indirect
@@ -89,6 +95,7 @@ require (
github.com/muesli/cancelreader v0.2.2 // indirect
github.com/ncruces/julianday v1.0.0 // indirect
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/rogpeppe/go-internal v1.14.1 // indirect
+15
View File
@@ -10,6 +10,14 @@ cloud.google.com/go/compute/metadata v0.6.0 h1:A6hENjEsCDtC1k8byVsgwvVcioamEHvZ4
cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg=
cloud.google.com/go/longrunning v0.5.7 h1:WLbHekDbjK1fVFD3ibpFFVoyizlLRl73I7YKuAKilhU=
cloud.google.com/go/longrunning v0.5.7/go.mod h1:8GClkudohy1Fxm3owmBGid8W0pSgodEMwEAztp38Xng=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.0 h1:g0EZJwz7xkXQiZAI5xi9f3WWFYBlX1CPTrR+NDToRkQ=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.0/go.mod h1:XCW7KnZet0Opnr7HccfUw1PLc4CjHqpcaxW8DHklNkQ=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.7.0 h1:tfLQ34V6F7tVSwoTf/4lH5sE0o6eCJuNDTmH09nDpbc=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.7.0/go.mod h1:9kIvujWAA58nmPmWB1m23fyWic1kYZMxD9CxaWn4Qpg=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 h1:ywEEhmNahHBihViHepv3xPBn1663uRv2t2q/ESv9seY=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0/go.mod h1:iZDifYGJTIgIIkYRNWPENUnqx6bJ2xnSDFI2tjwZNuY=
github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 h1:XHOnouVk1mxXfQidrMEnLlPk9UMeRtyBTnEFtxkV0kU=
github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
github.com/JohannesKaufmann/html-to-markdown v1.6.0 h1:04VXMiE50YYfCfLboJCLcgqF5x+rHJnb1ssNmqpLH/k=
github.com/JohannesKaufmann/html-to-markdown v1.6.0/go.mod h1:NUI78lGg/a7vpEJTz/0uOcYMaibytE4BUOQS8k78yPQ=
github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ=
@@ -113,6 +121,8 @@ github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss=
github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/generative-ai-go v0.19.0 h1:R71szggh8wHMCUlEMsW2A/3T+5LdEIkiaHSYgSpUgdg=
@@ -140,6 +150,8 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/lrstanley/bubblezone v0.0.0-20250315020633-c249a3fe1231 h1:9rjt7AfnrXKNSZhp36A3/4QAZAwGGCGD/p8Bse26zms=
github.com/lrstanley/bubblezone v0.0.0-20250315020633-c249a3fe1231/go.mod h1:S5etECMx+sZnW0Gm100Ma9J1PgVCTgNyFaqGu2b08b4=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
@@ -177,6 +189,8 @@ github.com/openai/openai-go v0.1.0-beta.2 h1:Ra5nCFkbEl9w+UJwAciC4kqnIBUCcJazhmM
github.com/openai/openai-go v0.1.0-beta.2/go.mod h1:g461MYGXEXBVdV5SaR/5tNzNbSfwTBBefwc+LlDCK0Y=
github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
@@ -295,6 +309,7 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+20
View File
@@ -16,6 +16,7 @@ import (
"github.com/opencode-ai/opencode/internal/message"
"github.com/opencode-ai/opencode/internal/permission"
"github.com/opencode-ai/opencode/internal/session"
"github.com/opencode-ai/opencode/internal/tui/theme"
)
type App struct {
@@ -49,6 +50,9 @@ func New(ctx context.Context, conn *sql.DB) (*App, error) {
LSPClients: make(map[string]*lsp.Client),
}
// Initialize theme based on configuration
app.initTheme()
// Initialize LSP clients in the background
go app.initLSPClients(ctx)
@@ -73,6 +77,22 @@ func New(ctx context.Context, conn *sql.DB) (*App, error) {
return app, nil
}
// initTheme sets the application theme based on the configuration
func (app *App) initTheme() {
cfg := config.Get()
if cfg == nil || cfg.TUI.Theme == "" {
return // Use default theme
}
// Try to set the theme from config
err := theme.SetTheme(cfg.TUI.Theme)
if err != nil {
logging.Warn("Failed to set theme from config, using default theme", "theme", cfg.TUI.Theme, "error", err)
} else {
logging.Debug("Set theme from config", "theme", cfg.TUI.Theme)
}
}
// Shutdown performs a clean shutdown of the application
func (app *App) Shutdown() {
// Cancel all watcher goroutines
+308 -124
View File
@@ -2,9 +2,11 @@
package config
import (
"encoding/json"
"fmt"
"log/slog"
"os"
"path/filepath"
"strings"
"github.com/opencode-ai/opencode/internal/llm/models"
@@ -65,16 +67,23 @@ type LSPConfig struct {
Options any `json:"options"`
}
// TUIConfig defines the configuration for the Terminal User Interface.
type TUIConfig struct {
Theme string `json:"theme,omitempty"`
}
// Config is the main configuration structure for the application.
type Config struct {
Data Data `json:"data"`
WorkingDir string `json:"wd,omitempty"`
MCPServers map[string]MCPServer `json:"mcpServers,omitempty"`
Providers map[models.ModelProvider]Provider `json:"providers,omitempty"`
LSP map[string]LSPConfig `json:"lsp,omitempty"`
Agents map[AgentName]Agent `json:"agents"`
Debug bool `json:"debug,omitempty"`
DebugLSP bool `json:"debugLSP,omitempty"`
Data Data `json:"data"`
WorkingDir string `json:"wd,omitempty"`
MCPServers map[string]MCPServer `json:"mcpServers,omitempty"`
Providers map[models.ModelProvider]Provider `json:"providers,omitempty"`
LSP map[string]LSPConfig `json:"lsp,omitempty"`
Agents map[AgentName]Agent `json:"agents"`
Debug bool `json:"debug,omitempty"`
DebugLSP bool `json:"debugLSP,omitempty"`
ContextPaths []string `json:"contextPaths,omitempty"`
TUI TUIConfig `json:"tui"`
}
// Application constants
@@ -82,8 +91,24 @@ const (
defaultDataDirectory = ".opencode"
defaultLogLevel = "info"
appName = "opencode"
MaxTokensFallbackDefault = 4096
)
var defaultContextPaths = []string{
".github/copilot-instructions.md",
".cursorrules",
".cursor/rules/",
"CLAUDE.md",
"CLAUDE.local.md",
"opencode.md",
"opencode.local.md",
"OpenCode.md",
"OpenCode.local.md",
"OPENCODE.md",
"OPENCODE.local.md",
}
// Global configuration instance
var cfg *Config
@@ -104,7 +129,6 @@ func Load(workingDir string, debug bool) (*Config, error) {
configureViper()
setDefaults(debug)
setProviderDefaults()
// Read global config
if err := readConfig(viper.ReadInConfig()); err != nil {
@@ -114,6 +138,8 @@ func Load(workingDir string, debug bool) (*Config, error) {
// Load and merge local config
mergeLocalConfig(workingDir)
setProviderDefaults()
// Apply configuration to the struct
if err := viper.Unmarshal(cfg); err != nil {
return cfg, fmt.Errorf("failed to unmarshal config: %w", err)
@@ -185,6 +211,8 @@ func configureViper() {
// setDefaults configures default values for configuration options.
func setDefaults(debug bool) {
viper.SetDefault("data.directory", defaultDataDirectory)
viper.SetDefault("contextPaths", defaultContextPaths)
viper.SetDefault("tui.theme", "opencode")
if debug {
viper.SetDefault("debug", true)
@@ -195,7 +223,8 @@ func setDefaults(debug bool) {
}
}
// setProviderDefaults configures LLM provider defaults based on environment variables.
// setProviderDefaults configures LLM provider defaults based on provider provided by
// environment variables and configuration file.
func setProviderDefaults() {
// Set all API keys we can find in the environment
if apiKey := os.Getenv("ANTHROPIC_API_KEY"); apiKey != "" {
@@ -210,15 +239,21 @@ func setProviderDefaults() {
if apiKey := os.Getenv("GROQ_API_KEY"); apiKey != "" {
viper.SetDefault("providers.groq.apiKey", apiKey)
}
if apiKey := os.Getenv("OPENROUTER_API_KEY"); apiKey != "" {
viper.SetDefault("providers.openrouter.apiKey", apiKey)
}
// Use this order to set the default models
// 1. Anthropic
// 2. OpenAI
// 3. Google Gemini
// 4. Groq
// 5. AWS Bedrock
// 5. OpenRouter
// 6. AWS Bedrock
// 7. Azure
// Anthropic configuration
if apiKey := os.Getenv("ANTHROPIC_API_KEY"); apiKey != "" {
if viper.Get("providers.anthropic.apiKey") != "" {
viper.SetDefault("agents.coder.model", models.Claude37Sonnet)
viper.SetDefault("agents.task.model", models.Claude37Sonnet)
viper.SetDefault("agents.title.model", models.Claude37Sonnet)
@@ -226,7 +261,7 @@ func setProviderDefaults() {
}
// OpenAI configuration
if apiKey := os.Getenv("OPENAI_API_KEY"); apiKey != "" {
if viper.Get("providers.openai.apiKey") != "" {
viper.SetDefault("agents.coder.model", models.GPT41)
viper.SetDefault("agents.task.model", models.GPT41Mini)
viper.SetDefault("agents.title.model", models.GPT41Mini)
@@ -234,7 +269,7 @@ func setProviderDefaults() {
}
// Google Gemini configuration
if apiKey := os.Getenv("GEMINI_API_KEY"); apiKey != "" {
if viper.Get("providers.google.gemini.apiKey") != "" {
viper.SetDefault("agents.coder.model", models.Gemini25)
viper.SetDefault("agents.task.model", models.Gemini25Flash)
viper.SetDefault("agents.title.model", models.Gemini25Flash)
@@ -242,13 +277,21 @@ func setProviderDefaults() {
}
// Groq configuration
if apiKey := os.Getenv("GROQ_API_KEY"); apiKey != "" {
if viper.Get("providers.groq.apiKey") != "" {
viper.SetDefault("agents.coder.model", models.QWENQwq)
viper.SetDefault("agents.task.model", models.QWENQwq)
viper.SetDefault("agents.title.model", models.QWENQwq)
return
}
// OpenRouter configuration
if viper.Get("providers.openrouter.apiKey") != "" {
viper.SetDefault("agents.coder.model", models.OpenRouterClaude37Sonnet)
viper.SetDefault("agents.task.model", models.OpenRouterClaude37Sonnet)
viper.SetDefault("agents.title.model", models.OpenRouterClaude35Haiku)
return
}
// AWS Bedrock configuration
if hasAWSCredentials() {
viper.SetDefault("agents.coder.model", models.BedrockClaude37Sonnet)
@@ -256,6 +299,15 @@ func setProviderDefaults() {
viper.SetDefault("agents.title.model", models.BedrockClaude37Sonnet)
return
}
if os.Getenv("AZURE_OPENAI_ENDPOINT") != "" {
// api-key may be empty when using Entra ID credentials that's okay
viper.SetDefault("providers.azure.apiKey", os.Getenv("AZURE_OPENAI_API_KEY"))
viper.SetDefault("agents.coder.model", models.AzureGPT41)
viper.SetDefault("agents.task.model", models.AzureGPT41Mini)
viper.SetDefault("agents.title.model", models.AzureGPT41Mini)
return
}
}
// hasAWSCredentials checks if AWS credentials are available in the environment.
@@ -322,60 +374,33 @@ func applyDefaultValues() {
}
}
// Validate checks if the configuration is valid and applies defaults where needed.
// It validates model IDs and providers, ensuring they are supported.
func Validate() error {
if cfg == nil {
return fmt.Errorf("config not loaded")
func validateAgent(cfg *Config, name AgentName, agent Agent) error {
// Check if model exists
model, modelExists := models.SupportedModels[agent.Model]
if !modelExists {
logging.Warn("unsupported model configured, reverting to default",
"agent", name,
"configured_model", agent.Model)
// Set default model based on available providers
if setDefaultModelForAgent(name) {
logging.Info("set default model for agent", "agent", name, "model", cfg.Agents[name].Model)
} else {
return fmt.Errorf("no valid provider available for agent %s", name)
}
return nil
}
// Validate agent models
for name, agent := range cfg.Agents {
// Check if model exists
model, modelExists := models.SupportedModels[agent.Model]
if !modelExists {
logging.Warn("unsupported model configured, reverting to default",
"agent", name,
"configured_model", agent.Model)
// Check if provider for the model is configured
provider := model.Provider
providerCfg, providerExists := cfg.Providers[provider]
// Set default model based on available providers
if setDefaultModelForAgent(name) {
logging.Info("set default model for agent", "agent", name, "model", cfg.Agents[name].Model)
} else {
return fmt.Errorf("no valid provider available for agent %s", name)
}
continue
}
// Check if provider for the model is configured
provider := model.Provider
providerCfg, providerExists := cfg.Providers[provider]
if !providerExists {
// Provider not configured, check if we have environment variables
apiKey := getProviderAPIKey(provider)
if apiKey == "" {
logging.Warn("provider not configured for model, reverting to default",
"agent", name,
"model", agent.Model,
"provider", provider)
// Set default model based on available providers
if setDefaultModelForAgent(name) {
logging.Info("set default model for agent", "agent", name, "model", cfg.Agents[name].Model)
} else {
return fmt.Errorf("no valid provider available for agent %s", name)
}
} else {
// Add provider with API key from environment
cfg.Providers[provider] = Provider{
APIKey: apiKey,
}
logging.Info("added provider from environment", "provider", provider)
}
} else if providerCfg.Disabled || providerCfg.APIKey == "" {
// Provider is disabled or has no API key
logging.Warn("provider is disabled or has no API key, reverting to default",
if !providerExists {
// Provider not configured, check if we have environment variables
apiKey := getProviderAPIKey(provider)
if apiKey == "" {
logging.Warn("provider not configured for model, reverting to default",
"agent", name,
"model", agent.Model,
"provider", provider)
@@ -386,75 +411,110 @@ func Validate() error {
} else {
return fmt.Errorf("no valid provider available for agent %s", name)
}
}
// Validate max tokens
if agent.MaxTokens <= 0 {
logging.Warn("invalid max tokens, setting to default",
"agent", name,
"model", agent.Model,
"max_tokens", agent.MaxTokens)
// Update the agent with default max tokens
updatedAgent := cfg.Agents[name]
if model.DefaultMaxTokens > 0 {
updatedAgent.MaxTokens = model.DefaultMaxTokens
} else {
updatedAgent.MaxTokens = 4096 // Fallback default
} else {
// Add provider with API key from environment
cfg.Providers[provider] = Provider{
APIKey: apiKey,
}
cfg.Agents[name] = updatedAgent
} else if model.ContextWindow > 0 && agent.MaxTokens > model.ContextWindow/2 {
// Ensure max tokens doesn't exceed half the context window (reasonable limit)
logging.Warn("max tokens exceeds half the context window, adjusting",
"agent", name,
"model", agent.Model,
"max_tokens", agent.MaxTokens,
"context_window", model.ContextWindow)
// Update the agent with adjusted max tokens
updatedAgent := cfg.Agents[name]
updatedAgent.MaxTokens = model.ContextWindow / 2
cfg.Agents[name] = updatedAgent
logging.Info("added provider from environment", "provider", provider)
}
} else if providerCfg.Disabled || providerCfg.APIKey == "" {
// Provider is disabled or has no API key
logging.Warn("provider is disabled or has no API key, reverting to default",
"agent", name,
"model", agent.Model,
"provider", provider)
// Validate reasoning effort for models that support reasoning
if model.CanReason && provider == models.ProviderOpenAI {
if agent.ReasoningEffort == "" {
// Set default reasoning effort for models that support it
logging.Info("setting default reasoning effort for model that supports reasoning",
// Set default model based on available providers
if setDefaultModelForAgent(name) {
logging.Info("set default model for agent", "agent", name, "model", cfg.Agents[name].Model)
} else {
return fmt.Errorf("no valid provider available for agent %s", name)
}
}
// Validate max tokens
if agent.MaxTokens <= 0 {
logging.Warn("invalid max tokens, setting to default",
"agent", name,
"model", agent.Model,
"max_tokens", agent.MaxTokens)
// Update the agent with default max tokens
updatedAgent := cfg.Agents[name]
if model.DefaultMaxTokens > 0 {
updatedAgent.MaxTokens = model.DefaultMaxTokens
} else {
updatedAgent.MaxTokens = MaxTokensFallbackDefault
}
cfg.Agents[name] = updatedAgent
} else if model.ContextWindow > 0 && agent.MaxTokens > model.ContextWindow/2 {
// Ensure max tokens doesn't exceed half the context window (reasonable limit)
logging.Warn("max tokens exceeds half the context window, adjusting",
"agent", name,
"model", agent.Model,
"max_tokens", agent.MaxTokens,
"context_window", model.ContextWindow)
// Update the agent with adjusted max tokens
updatedAgent := cfg.Agents[name]
updatedAgent.MaxTokens = model.ContextWindow / 2
cfg.Agents[name] = updatedAgent
}
// Validate reasoning effort for models that support reasoning
if model.CanReason && provider == models.ProviderOpenAI {
if agent.ReasoningEffort == "" {
// Set default reasoning effort for models that support it
logging.Info("setting default reasoning effort for model that supports reasoning",
"agent", name,
"model", agent.Model)
// Update the agent with default reasoning effort
updatedAgent := cfg.Agents[name]
updatedAgent.ReasoningEffort = "medium"
cfg.Agents[name] = updatedAgent
} else {
// Check if reasoning effort is valid (low, medium, high)
effort := strings.ToLower(agent.ReasoningEffort)
if effort != "low" && effort != "medium" && effort != "high" {
logging.Warn("invalid reasoning effort, setting to medium",
"agent", name,
"model", agent.Model)
"model", agent.Model,
"reasoning_effort", agent.ReasoningEffort)
// Update the agent with default reasoning effort
// Update the agent with valid reasoning effort
updatedAgent := cfg.Agents[name]
updatedAgent.ReasoningEffort = "medium"
cfg.Agents[name] = updatedAgent
} else {
// Check if reasoning effort is valid (low, medium, high)
effort := strings.ToLower(agent.ReasoningEffort)
if effort != "low" && effort != "medium" && effort != "high" {
logging.Warn("invalid reasoning effort, setting to medium",
"agent", name,
"model", agent.Model,
"reasoning_effort", agent.ReasoningEffort)
// Update the agent with valid reasoning effort
updatedAgent := cfg.Agents[name]
updatedAgent.ReasoningEffort = "medium"
cfg.Agents[name] = updatedAgent
}
}
} else if !model.CanReason && agent.ReasoningEffort != "" {
// Model doesn't support reasoning but reasoning effort is set
logging.Warn("model doesn't support reasoning but reasoning effort is set, ignoring",
"agent", name,
"model", agent.Model,
"reasoning_effort", agent.ReasoningEffort)
}
} else if !model.CanReason && agent.ReasoningEffort != "" {
// Model doesn't support reasoning but reasoning effort is set
logging.Warn("model doesn't support reasoning but reasoning effort is set, ignoring",
"agent", name,
"model", agent.Model,
"reasoning_effort", agent.ReasoningEffort)
// Update the agent to remove reasoning effort
updatedAgent := cfg.Agents[name]
updatedAgent.ReasoningEffort = ""
cfg.Agents[name] = updatedAgent
// Update the agent to remove reasoning effort
updatedAgent := cfg.Agents[name]
updatedAgent.ReasoningEffort = ""
cfg.Agents[name] = updatedAgent
}
return nil
}
// Validate checks if the configuration is valid and applies defaults where needed.
func Validate() error {
if cfg == nil {
return fmt.Errorf("config not loaded")
}
// Validate agent models
for name, agent := range cfg.Agents {
if err := validateAgent(cfg, name, agent); err != nil {
return err
}
}
@@ -490,6 +550,10 @@ func getProviderAPIKey(provider models.ModelProvider) string {
return os.Getenv("GEMINI_API_KEY")
case models.ProviderGROQ:
return os.Getenv("GROQ_API_KEY")
case models.ProviderAzure:
return os.Getenv("AZURE_OPENAI_API_KEY")
case models.ProviderOpenRouter:
return os.Getenv("OPENROUTER_API_KEY")
case models.ProviderBedrock:
if hasAWSCredentials() {
return "aws-credentials-available"
@@ -541,6 +605,34 @@ func setDefaultModelForAgent(agent AgentName) bool {
return true
}
if apiKey := os.Getenv("OPENROUTER_API_KEY"); apiKey != "" {
var model models.ModelID
maxTokens := int64(5000)
reasoningEffort := ""
switch agent {
case AgentTitle:
model = models.OpenRouterClaude35Haiku
maxTokens = 80
case AgentTask:
model = models.OpenRouterClaude37Sonnet
default:
model = models.OpenRouterClaude37Sonnet
}
// Check if model supports reasoning
if modelInfo, ok := models.SupportedModels[model]; ok && modelInfo.CanReason {
reasoningEffort = "medium"
}
cfg.Agents[agent] = Agent{
Model: model,
MaxTokens: maxTokens,
ReasoningEffort: reasoningEffort,
}
return true
}
if apiKey := os.Getenv("GEMINI_API_KEY"); apiKey != "" {
var model models.ModelID
maxTokens := int64(5000)
@@ -602,3 +694,95 @@ func WorkingDirectory() string {
}
return cfg.WorkingDir
}
func UpdateAgentModel(agentName AgentName, modelID models.ModelID) error {
if cfg == nil {
panic("config not loaded")
}
existingAgentCfg := cfg.Agents[agentName]
model, ok := models.SupportedModels[modelID]
if !ok {
return fmt.Errorf("model %s not supported", modelID)
}
maxTokens := existingAgentCfg.MaxTokens
if model.DefaultMaxTokens > 0 {
maxTokens = model.DefaultMaxTokens
}
newAgentCfg := Agent{
Model: modelID,
MaxTokens: maxTokens,
ReasoningEffort: existingAgentCfg.ReasoningEffort,
}
cfg.Agents[agentName] = newAgentCfg
if err := validateAgent(cfg, agentName, newAgentCfg); err != nil {
// revert config update on failure
cfg.Agents[agentName] = existingAgentCfg
return fmt.Errorf("failed to update agent model: %w", err)
}
return nil
}
// UpdateTheme updates the theme in the configuration and writes it to the config file.
func UpdateTheme(themeName string) error {
if cfg == nil {
return fmt.Errorf("config not loaded")
}
// Update the in-memory config
cfg.TUI.Theme = themeName
// Get the config file path
configFile := viper.ConfigFileUsed()
var configData []byte
if configFile == "" {
homeDir, err := os.UserHomeDir()
if err != nil {
return fmt.Errorf("failed to get home directory: %w", err)
}
configFile = filepath.Join(homeDir, fmt.Sprintf(".%s.json", appName))
logging.Info("config file not found, creating new one", "path", configFile)
configData = []byte(`{}`)
} else {
// Read the existing config file
data, err := os.ReadFile(configFile)
if err != nil {
return fmt.Errorf("failed to read config file: %w", err)
}
configData = data
}
// Parse the JSON
var configMap map[string]interface{}
if err := json.Unmarshal(configData, &configMap); err != nil {
return fmt.Errorf("failed to parse config file: %w", err)
}
// Update just the theme value
tuiConfig, ok := configMap["tui"].(map[string]interface{})
if !ok {
// TUI config doesn't exist yet, create it
configMap["tui"] = map[string]interface{}{"theme": themeName}
} else {
// Update existing TUI config
tuiConfig["theme"] = themeName
configMap["tui"] = tuiConfig
}
// Write the updated config back to file
updatedData, err := json.MarshalIndent(configMap, "", " ")
if err != nil {
return fmt.Errorf("failed to marshal config: %w", err)
}
if err := os.WriteFile(configFile, updatedData, 0o644); err != nil {
return fmt.Errorf("failed to write config file: %w", err)
}
return nil
}
+231 -315
View File
@@ -16,6 +16,7 @@ import (
"github.com/charmbracelet/lipgloss"
"github.com/charmbracelet/x/ansi"
"github.com/opencode-ai/opencode/internal/config"
"github.com/opencode-ai/opencode/internal/tui/theme"
"github.com/sergi/go-diff/diffmatchpatch"
)
@@ -68,143 +69,6 @@ type linePair struct {
right *DiffLine
}
// -------------------------------------------------------------------------
// Style Configuration
// -------------------------------------------------------------------------
// StyleConfig defines styling for diff rendering
type StyleConfig struct {
ShowHeader bool
ShowHunkHeader bool
FileNameFg lipgloss.Color
// Background colors
RemovedLineBg lipgloss.Color
AddedLineBg lipgloss.Color
ContextLineBg lipgloss.Color
HunkLineBg lipgloss.Color
RemovedLineNumberBg lipgloss.Color
AddedLineNamerBg lipgloss.Color
// Foreground colors
HunkLineFg lipgloss.Color
RemovedFg lipgloss.Color
AddedFg lipgloss.Color
LineNumberFg lipgloss.Color
RemovedHighlightFg lipgloss.Color
AddedHighlightFg lipgloss.Color
// Highlight settings
HighlightStyle string
RemovedHighlightBg lipgloss.Color
AddedHighlightBg lipgloss.Color
}
// StyleOption is a function that modifies a StyleConfig
type StyleOption func(*StyleConfig)
// NewStyleConfig creates a StyleConfig with default values
func NewStyleConfig(opts ...StyleOption) StyleConfig {
// Default color scheme
config := StyleConfig{
ShowHeader: true,
ShowHunkHeader: true,
FileNameFg: lipgloss.Color("#a0a0a0"),
RemovedLineBg: lipgloss.Color("#3A3030"),
AddedLineBg: lipgloss.Color("#303A30"),
ContextLineBg: lipgloss.Color("#212121"),
HunkLineBg: lipgloss.Color("#212121"),
HunkLineFg: lipgloss.Color("#a0a0a0"),
RemovedFg: lipgloss.Color("#7C4444"),
AddedFg: lipgloss.Color("#478247"),
LineNumberFg: lipgloss.Color("#888888"),
HighlightStyle: "dracula",
RemovedHighlightBg: lipgloss.Color("#612726"),
AddedHighlightBg: lipgloss.Color("#256125"),
RemovedLineNumberBg: lipgloss.Color("#332929"),
AddedLineNamerBg: lipgloss.Color("#293229"),
RemovedHighlightFg: lipgloss.Color("#FADADD"),
AddedHighlightFg: lipgloss.Color("#DAFADA"),
}
// Apply all provided options
for _, opt := range opts {
opt(&config)
}
return config
}
// Style option functions
func WithFileNameFg(color lipgloss.Color) StyleOption {
return func(s *StyleConfig) { s.FileNameFg = color }
}
func WithRemovedLineBg(color lipgloss.Color) StyleOption {
return func(s *StyleConfig) { s.RemovedLineBg = color }
}
func WithAddedLineBg(color lipgloss.Color) StyleOption {
return func(s *StyleConfig) { s.AddedLineBg = color }
}
func WithContextLineBg(color lipgloss.Color) StyleOption {
return func(s *StyleConfig) { s.ContextLineBg = color }
}
func WithRemovedFg(color lipgloss.Color) StyleOption {
return func(s *StyleConfig) { s.RemovedFg = color }
}
func WithAddedFg(color lipgloss.Color) StyleOption {
return func(s *StyleConfig) { s.AddedFg = color }
}
func WithLineNumberFg(color lipgloss.Color) StyleOption {
return func(s *StyleConfig) { s.LineNumberFg = color }
}
func WithHighlightStyle(style string) StyleOption {
return func(s *StyleConfig) { s.HighlightStyle = style }
}
func WithRemovedHighlightColors(bg, fg lipgloss.Color) StyleOption {
return func(s *StyleConfig) {
s.RemovedHighlightBg = bg
s.RemovedHighlightFg = fg
}
}
func WithAddedHighlightColors(bg, fg lipgloss.Color) StyleOption {
return func(s *StyleConfig) {
s.AddedHighlightBg = bg
s.AddedHighlightFg = fg
}
}
func WithRemovedLineNumberBg(color lipgloss.Color) StyleOption {
return func(s *StyleConfig) { s.RemovedLineNumberBg = color }
}
func WithAddedLineNumberBg(color lipgloss.Color) StyleOption {
return func(s *StyleConfig) { s.AddedLineNamerBg = color }
}
func WithHunkLineBg(color lipgloss.Color) StyleOption {
return func(s *StyleConfig) { s.HunkLineBg = color }
}
func WithHunkLineFg(color lipgloss.Color) StyleOption {
return func(s *StyleConfig) { s.HunkLineFg = color }
}
func WithShowHeader(show bool) StyleOption {
return func(s *StyleConfig) { s.ShowHeader = show }
}
func WithShowHunkHeader(show bool) StyleOption {
return func(s *StyleConfig) { s.ShowHunkHeader = show }
}
// -------------------------------------------------------------------------
// Parse Configuration
// -------------------------------------------------------------------------
@@ -233,7 +97,6 @@ func WithContextSize(size int) ParseOption {
// SideBySideConfig configures the rendering of side-by-side diffs
type SideBySideConfig struct {
TotalWidth int
Style StyleConfig
}
// SideBySideOption modifies a SideBySideConfig
@@ -243,7 +106,6 @@ type SideBySideOption func(*SideBySideConfig)
func NewSideBySideConfig(opts ...SideBySideOption) SideBySideConfig {
config := SideBySideConfig{
TotalWidth: 160, // Default width for side-by-side view
Style: NewStyleConfig(),
}
for _, opt := range opts {
@@ -262,20 +124,6 @@ func WithTotalWidth(width int) SideBySideOption {
}
}
// WithStyle sets the styling configuration
func WithStyle(style StyleConfig) SideBySideOption {
return func(s *SideBySideConfig) {
s.Style = style
}
}
// WithStyleOptions applies the specified style options
func WithStyleOptions(opts ...StyleOption) SideBySideOption {
return func(s *SideBySideConfig) {
s.Style = NewStyleConfig(opts...)
}
}
// -------------------------------------------------------------------------
// Diff Parsing
// -------------------------------------------------------------------------
@@ -382,7 +230,7 @@ func ParseUnifiedDiff(diff string) (DiffResult, error) {
}
// HighlightIntralineChanges updates lines in a hunk to show character-level differences
func HighlightIntralineChanges(h *Hunk, style StyleConfig) {
func HighlightIntralineChanges(h *Hunk) {
var updated []DiffLine
dmp := diffmatchpatch.New()
@@ -476,6 +324,8 @@ func pairLines(lines []DiffLine) []linePair {
// SyntaxHighlight applies syntax highlighting to text based on file extension
func SyntaxHighlight(w io.Writer, source, fileName, formatter string, bg lipgloss.TerminalColor) error {
t := theme.CurrentTheme()
// Determine the language lexer to use
l := lexers.Match(fileName)
if l == nil {
@@ -491,93 +341,175 @@ func SyntaxHighlight(w io.Writer, source, fileName, formatter string, bg lipglos
if f == nil {
f = formatters.Fallback
}
theme := `
<style name="vscode-dark-plus">
<!-- Base colors -->
<entry type="Background" style="bg:#1E1E1E"/>
<entry type="Text" style="#D4D4D4"/>
<entry type="Other" style="#D4D4D4"/>
<entry type="Error" style="#F44747"/>
<!-- Keywords - using the Control flow / Special keywords color -->
<entry type="Keyword" style="#C586C0"/>
<entry type="KeywordConstant" style="#4FC1FF"/>
<entry type="KeywordDeclaration" style="#C586C0"/>
<entry type="KeywordNamespace" style="#C586C0"/>
<entry type="KeywordPseudo" style="#C586C0"/>
<entry type="KeywordReserved" style="#C586C0"/>
<entry type="KeywordType" style="#4EC9B0"/>
<!-- Names -->
<entry type="Name" style="#D4D4D4"/>
<entry type="NameAttribute" style="#9CDCFE"/>
<entry type="NameBuiltin" style="#4EC9B0"/>
<entry type="NameBuiltinPseudo" style="#9CDCFE"/>
<entry type="NameClass" style="#4EC9B0"/>
<entry type="NameConstant" style="#4FC1FF"/>
<entry type="NameDecorator" style="#DCDCAA"/>
<entry type="NameEntity" style="#9CDCFE"/>
<entry type="NameException" style="#4EC9B0"/>
<entry type="NameFunction" style="#DCDCAA"/>
<entry type="NameLabel" style="#C8C8C8"/>
<entry type="NameNamespace" style="#4EC9B0"/>
<entry type="NameOther" style="#9CDCFE"/>
<entry type="NameTag" style="#569CD6"/>
<entry type="NameVariable" style="#9CDCFE"/>
<entry type="NameVariableClass" style="#9CDCFE"/>
<entry type="NameVariableGlobal" style="#9CDCFE"/>
<entry type="NameVariableInstance" style="#9CDCFE"/>
<!-- Literals -->
<entry type="Literal" style="#CE9178"/>
<entry type="LiteralDate" style="#CE9178"/>
<entry type="LiteralString" style="#CE9178"/>
<entry type="LiteralStringBacktick" style="#CE9178"/>
<entry type="LiteralStringChar" style="#CE9178"/>
<entry type="LiteralStringDoc" style="#CE9178"/>
<entry type="LiteralStringDouble" style="#CE9178"/>
<entry type="LiteralStringEscape" style="#d7ba7d"/>
<entry type="LiteralStringHeredoc" style="#CE9178"/>
<entry type="LiteralStringInterpol" style="#CE9178"/>
<entry type="LiteralStringOther" style="#CE9178"/>
<entry type="LiteralStringRegex" style="#d16969"/>
<entry type="LiteralStringSingle" style="#CE9178"/>
<entry type="LiteralStringSymbol" style="#CE9178"/>
<!-- Numbers - using the numberLiteral color -->
<entry type="LiteralNumber" style="#b5cea8"/>
<entry type="LiteralNumberBin" style="#b5cea8"/>
<entry type="LiteralNumberFloat" style="#b5cea8"/>
<entry type="LiteralNumberHex" style="#b5cea8"/>
<entry type="LiteralNumberInteger" style="#b5cea8"/>
<entry type="LiteralNumberIntegerLong" style="#b5cea8"/>
<entry type="LiteralNumberOct" style="#b5cea8"/>
<!-- Operators -->
<entry type="Operator" style="#D4D4D4"/>
<entry type="OperatorWord" style="#C586C0"/>
<entry type="Punctuation" style="#D4D4D4"/>
<!-- Comments - standard VSCode Dark+ comment color -->
<entry type="Comment" style="#6A9955"/>
<entry type="CommentHashbang" style="#6A9955"/>
<entry type="CommentMultiline" style="#6A9955"/>
<entry type="CommentSingle" style="#6A9955"/>
<entry type="CommentSpecial" style="#6A9955"/>
<entry type="CommentPreproc" style="#C586C0"/>
<!-- Generic styles -->
<entry type="Generic" style="#D4D4D4"/>
<entry type="GenericDeleted" style="#F44747"/>
<entry type="GenericEmph" style="italic #D4D4D4"/>
<entry type="GenericError" style="#F44747"/>
<entry type="GenericHeading" style="bold #D4D4D4"/>
<entry type="GenericInserted" style="#b5cea8"/>
<entry type="GenericOutput" style="#808080"/>
<entry type="GenericPrompt" style="#D4D4D4"/>
<entry type="GenericStrong" style="bold #D4D4D4"/>
<entry type="GenericSubheading" style="bold #D4D4D4"/>
<entry type="GenericTraceback" style="#F44747"/>
<entry type="GenericUnderline" style="underline"/>
<entry type="TextWhitespace" style="#D4D4D4"/>
</style>
`
r := strings.NewReader(theme)
// Dynamic theme based on current theme values
syntaxThemeXml := fmt.Sprintf(`
<style name="opencode-theme">
<!-- Base colors -->
<entry type="Background" style="bg:%s"/>
<entry type="Text" style="%s"/>
<entry type="Other" style="%s"/>
<entry type="Error" style="%s"/>
<!-- Keywords -->
<entry type="Keyword" style="%s"/>
<entry type="KeywordConstant" style="%s"/>
<entry type="KeywordDeclaration" style="%s"/>
<entry type="KeywordNamespace" style="%s"/>
<entry type="KeywordPseudo" style="%s"/>
<entry type="KeywordReserved" style="%s"/>
<entry type="KeywordType" style="%s"/>
<!-- Names -->
<entry type="Name" style="%s"/>
<entry type="NameAttribute" style="%s"/>
<entry type="NameBuiltin" style="%s"/>
<entry type="NameBuiltinPseudo" style="%s"/>
<entry type="NameClass" style="%s"/>
<entry type="NameConstant" style="%s"/>
<entry type="NameDecorator" style="%s"/>
<entry type="NameEntity" style="%s"/>
<entry type="NameException" style="%s"/>
<entry type="NameFunction" style="%s"/>
<entry type="NameLabel" style="%s"/>
<entry type="NameNamespace" style="%s"/>
<entry type="NameOther" style="%s"/>
<entry type="NameTag" style="%s"/>
<entry type="NameVariable" style="%s"/>
<entry type="NameVariableClass" style="%s"/>
<entry type="NameVariableGlobal" style="%s"/>
<entry type="NameVariableInstance" style="%s"/>
<!-- Literals -->
<entry type="Literal" style="%s"/>
<entry type="LiteralDate" style="%s"/>
<entry type="LiteralString" style="%s"/>
<entry type="LiteralStringBacktick" style="%s"/>
<entry type="LiteralStringChar" style="%s"/>
<entry type="LiteralStringDoc" style="%s"/>
<entry type="LiteralStringDouble" style="%s"/>
<entry type="LiteralStringEscape" style="%s"/>
<entry type="LiteralStringHeredoc" style="%s"/>
<entry type="LiteralStringInterpol" style="%s"/>
<entry type="LiteralStringOther" style="%s"/>
<entry type="LiteralStringRegex" style="%s"/>
<entry type="LiteralStringSingle" style="%s"/>
<entry type="LiteralStringSymbol" style="%s"/>
<!-- Numbers -->
<entry type="LiteralNumber" style="%s"/>
<entry type="LiteralNumberBin" style="%s"/>
<entry type="LiteralNumberFloat" style="%s"/>
<entry type="LiteralNumberHex" style="%s"/>
<entry type="LiteralNumberInteger" style="%s"/>
<entry type="LiteralNumberIntegerLong" style="%s"/>
<entry type="LiteralNumberOct" style="%s"/>
<!-- Operators -->
<entry type="Operator" style="%s"/>
<entry type="OperatorWord" style="%s"/>
<entry type="Punctuation" style="%s"/>
<!-- Comments -->
<entry type="Comment" style="%s"/>
<entry type="CommentHashbang" style="%s"/>
<entry type="CommentMultiline" style="%s"/>
<entry type="CommentSingle" style="%s"/>
<entry type="CommentSpecial" style="%s"/>
<entry type="CommentPreproc" style="%s"/>
<!-- Generic styles -->
<entry type="Generic" style="%s"/>
<entry type="GenericDeleted" style="%s"/>
<entry type="GenericEmph" style="italic %s"/>
<entry type="GenericError" style="%s"/>
<entry type="GenericHeading" style="bold %s"/>
<entry type="GenericInserted" style="%s"/>
<entry type="GenericOutput" style="%s"/>
<entry type="GenericPrompt" style="%s"/>
<entry type="GenericStrong" style="bold %s"/>
<entry type="GenericSubheading" style="bold %s"/>
<entry type="GenericTraceback" style="%s"/>
<entry type="GenericUnderline" style="underline"/>
<entry type="TextWhitespace" style="%s"/>
</style>
`,
getColor(t.Background()), // Background
getColor(t.Text()), // Text
getColor(t.Text()), // Other
getColor(t.Error()), // Error
getColor(t.SyntaxKeyword()), // Keyword
getColor(t.SyntaxKeyword()), // KeywordConstant
getColor(t.SyntaxKeyword()), // KeywordDeclaration
getColor(t.SyntaxKeyword()), // KeywordNamespace
getColor(t.SyntaxKeyword()), // KeywordPseudo
getColor(t.SyntaxKeyword()), // KeywordReserved
getColor(t.SyntaxType()), // KeywordType
getColor(t.Text()), // Name
getColor(t.SyntaxVariable()), // NameAttribute
getColor(t.SyntaxType()), // NameBuiltin
getColor(t.SyntaxVariable()), // NameBuiltinPseudo
getColor(t.SyntaxType()), // NameClass
getColor(t.SyntaxVariable()), // NameConstant
getColor(t.SyntaxFunction()), // NameDecorator
getColor(t.SyntaxVariable()), // NameEntity
getColor(t.SyntaxType()), // NameException
getColor(t.SyntaxFunction()), // NameFunction
getColor(t.Text()), // NameLabel
getColor(t.SyntaxType()), // NameNamespace
getColor(t.SyntaxVariable()), // NameOther
getColor(t.SyntaxKeyword()), // NameTag
getColor(t.SyntaxVariable()), // NameVariable
getColor(t.SyntaxVariable()), // NameVariableClass
getColor(t.SyntaxVariable()), // NameVariableGlobal
getColor(t.SyntaxVariable()), // NameVariableInstance
getColor(t.SyntaxString()), // Literal
getColor(t.SyntaxString()), // LiteralDate
getColor(t.SyntaxString()), // LiteralString
getColor(t.SyntaxString()), // LiteralStringBacktick
getColor(t.SyntaxString()), // LiteralStringChar
getColor(t.SyntaxString()), // LiteralStringDoc
getColor(t.SyntaxString()), // LiteralStringDouble
getColor(t.SyntaxString()), // LiteralStringEscape
getColor(t.SyntaxString()), // LiteralStringHeredoc
getColor(t.SyntaxString()), // LiteralStringInterpol
getColor(t.SyntaxString()), // LiteralStringOther
getColor(t.SyntaxString()), // LiteralStringRegex
getColor(t.SyntaxString()), // LiteralStringSingle
getColor(t.SyntaxString()), // LiteralStringSymbol
getColor(t.SyntaxNumber()), // LiteralNumber
getColor(t.SyntaxNumber()), // LiteralNumberBin
getColor(t.SyntaxNumber()), // LiteralNumberFloat
getColor(t.SyntaxNumber()), // LiteralNumberHex
getColor(t.SyntaxNumber()), // LiteralNumberInteger
getColor(t.SyntaxNumber()), // LiteralNumberIntegerLong
getColor(t.SyntaxNumber()), // LiteralNumberOct
getColor(t.SyntaxOperator()), // Operator
getColor(t.SyntaxKeyword()), // OperatorWord
getColor(t.SyntaxPunctuation()), // Punctuation
getColor(t.SyntaxComment()), // Comment
getColor(t.SyntaxComment()), // CommentHashbang
getColor(t.SyntaxComment()), // CommentMultiline
getColor(t.SyntaxComment()), // CommentSingle
getColor(t.SyntaxComment()), // CommentSpecial
getColor(t.SyntaxKeyword()), // CommentPreproc
getColor(t.Text()), // Generic
getColor(t.Error()), // GenericDeleted
getColor(t.Text()), // GenericEmph
getColor(t.Error()), // GenericError
getColor(t.Text()), // GenericHeading
getColor(t.Success()), // GenericInserted
getColor(t.TextMuted()), // GenericOutput
getColor(t.Text()), // GenericPrompt
getColor(t.Text()), // GenericStrong
getColor(t.Text()), // GenericSubheading
getColor(t.Error()), // GenericTraceback
getColor(t.Text()), // TextWhitespace
)
r := strings.NewReader(syntaxThemeXml)
style := chroma.MustNewXMLStyle(r)
// Modify the style to use the provided background
s, err := style.Builder().Transform(
func(t chroma.StyleEntry) chroma.StyleEntry {
@@ -599,6 +531,14 @@ func SyntaxHighlight(w io.Writer, source, fileName, formatter string, bg lipglos
return f.Format(w, s, it)
}
// getColor returns the appropriate hex color string based on terminal background
func getColor(adaptiveColor lipgloss.AdaptiveColor) string {
if lipgloss.HasDarkBackground() {
return adaptiveColor.Dark
}
return adaptiveColor.Light
}
// highlightLine applies syntax highlighting to a single line
func highlightLine(fileName string, line string, bg lipgloss.TerminalColor) string {
var buf bytes.Buffer
@@ -610,11 +550,11 @@ func highlightLine(fileName string, line string, bg lipgloss.TerminalColor) stri
}
// createStyles generates the lipgloss styles needed for rendering diffs
func createStyles(config StyleConfig) (removedLineStyle, addedLineStyle, contextLineStyle, lineNumberStyle lipgloss.Style) {
removedLineStyle = lipgloss.NewStyle().Background(config.RemovedLineBg)
addedLineStyle = lipgloss.NewStyle().Background(config.AddedLineBg)
contextLineStyle = lipgloss.NewStyle().Background(config.ContextLineBg)
lineNumberStyle = lipgloss.NewStyle().Foreground(config.LineNumberFg)
func createStyles(t theme.Theme) (removedLineStyle, addedLineStyle, contextLineStyle, lineNumberStyle lipgloss.Style) {
removedLineStyle = lipgloss.NewStyle().Background(t.DiffRemovedBg())
addedLineStyle = lipgloss.NewStyle().Background(t.DiffAddedBg())
contextLineStyle = lipgloss.NewStyle().Background(t.DiffContextBg())
lineNumberStyle = lipgloss.NewStyle().Foreground(t.DiffLineNumber())
return
}
@@ -623,9 +563,20 @@ func createStyles(config StyleConfig) (removedLineStyle, addedLineStyle, context
// Rendering Functions
// -------------------------------------------------------------------------
func lipglossToHex(color lipgloss.Color) string {
r, g, b, a := color.RGBA()
// Scale uint32 values (0-65535) to uint8 (0-255).
r8 := uint8(r >> 8)
g8 := uint8(g >> 8)
b8 := uint8(b >> 8)
a8 := uint8(a >> 8)
return fmt.Sprintf("#%02x%02x%02x%02x", r8, g8, b8, a8)
}
// applyHighlighting applies intra-line highlighting to a piece of text
func applyHighlighting(content string, segments []Segment, segmentType LineType, highlightBg lipgloss.Color,
) string {
func applyHighlighting(content string, segments []Segment, segmentType LineType, highlightBg lipgloss.AdaptiveColor) string {
// Find all ANSI sequences in the content
ansiRegex := regexp.MustCompile(`\x1b(?:[@-Z\\-_]|\[[0-9?]*(?:;[0-9?]*)*[@-~])`)
ansiMatches := ansiRegex.FindAllStringIndex(content, -1)
@@ -663,6 +614,10 @@ func applyHighlighting(content string, segments []Segment, segmentType LineType,
inSelection := false
currentPos := 0
// Get the appropriate color based on terminal background
bgColor := lipgloss.Color(getColor(highlightBg))
fgColor := lipgloss.Color(getColor(theme.CurrentTheme().Background()))
for i := 0; i < len(content); {
// Check if we're at an ANSI sequence
isAnsi := false
@@ -697,12 +652,16 @@ func applyHighlighting(content string, segments []Segment, segmentType LineType,
// Get the current styling
currentStyle := ansiSequences[currentPos]
// Apply background highlight
// Apply foreground and background highlight
sb.WriteString("\x1b[38;2;")
r, g, b, _ := fgColor.RGBA()
sb.WriteString(fmt.Sprintf("%d;%d;%dm", r>>8, g>>8, b>>8))
sb.WriteString("\x1b[48;2;")
r, g, b, _ := highlightBg.RGBA()
r, g, b, _ = bgColor.RGBA()
sb.WriteString(fmt.Sprintf("%d;%d;%dm", r>>8, g>>8, b>>8))
sb.WriteString(char)
sb.WriteString("\x1b[49m") // Reset only background
// Reset foreground and background
sb.WriteString("\x1b[39m")
// Reapply the original ANSI sequence
sb.WriteString(currentStyle)
@@ -719,22 +678,24 @@ func applyHighlighting(content string, segments []Segment, segmentType LineType,
}
// renderLeftColumn formats the left side of a side-by-side diff
func renderLeftColumn(fileName string, dl *DiffLine, colWidth int, styles StyleConfig) string {
func renderLeftColumn(fileName string, dl *DiffLine, colWidth int) string {
t := theme.CurrentTheme()
if dl == nil {
contextLineStyle := lipgloss.NewStyle().Background(styles.ContextLineBg)
contextLineStyle := lipgloss.NewStyle().Background(t.DiffContextBg())
return contextLineStyle.Width(colWidth).Render("")
}
removedLineStyle, _, contextLineStyle, lineNumberStyle := createStyles(styles)
removedLineStyle, _, contextLineStyle, lineNumberStyle := createStyles(t)
// Determine line style based on line type
var marker string
var bgStyle lipgloss.Style
switch dl.Kind {
case LineRemoved:
marker = removedLineStyle.Foreground(styles.RemovedFg).Render("-")
marker = removedLineStyle.Foreground(t.DiffRemoved()).Render("-")
bgStyle = removedLineStyle
lineNumberStyle = lineNumberStyle.Foreground(styles.RemovedFg).Background(styles.RemovedLineNumberBg)
lineNumberStyle = lineNumberStyle.Foreground(t.DiffRemoved()).Background(t.DiffRemovedLineNumberBg())
case LineAdded:
marker = "?"
bgStyle = contextLineStyle
@@ -757,7 +718,7 @@ func renderLeftColumn(fileName string, dl *DiffLine, colWidth int, styles StyleC
// Apply intra-line highlighting for removed lines
if dl.Kind == LineRemoved && len(dl.Segments) > 0 {
content = applyHighlighting(content, dl.Segments, LineRemoved, styles.RemovedHighlightBg)
content = applyHighlighting(content, dl.Segments, LineRemoved, t.DiffHighlightRemoved())
}
// Add a padding space for removed lines
@@ -771,28 +732,30 @@ func renderLeftColumn(fileName string, dl *DiffLine, colWidth int, styles StyleC
ansi.Truncate(
lineText,
colWidth,
lipgloss.NewStyle().Background(styles.HunkLineBg).Foreground(styles.HunkLineFg).Render("..."),
lipgloss.NewStyle().Background(bgStyle.GetBackground()).Foreground(t.TextMuted()).Render("..."),
),
)
}
// renderRightColumn formats the right side of a side-by-side diff
func renderRightColumn(fileName string, dl *DiffLine, colWidth int, styles StyleConfig) string {
func renderRightColumn(fileName string, dl *DiffLine, colWidth int) string {
t := theme.CurrentTheme()
if dl == nil {
contextLineStyle := lipgloss.NewStyle().Background(styles.ContextLineBg)
contextLineStyle := lipgloss.NewStyle().Background(t.DiffContextBg())
return contextLineStyle.Width(colWidth).Render("")
}
_, addedLineStyle, contextLineStyle, lineNumberStyle := createStyles(styles)
_, addedLineStyle, contextLineStyle, lineNumberStyle := createStyles(t)
// Determine line style based on line type
var marker string
var bgStyle lipgloss.Style
switch dl.Kind {
case LineAdded:
marker = addedLineStyle.Foreground(styles.AddedFg).Render("+")
marker = addedLineStyle.Foreground(t.DiffAdded()).Render("+")
bgStyle = addedLineStyle
lineNumberStyle = lineNumberStyle.Foreground(styles.AddedFg).Background(styles.AddedLineNamerBg)
lineNumberStyle = lineNumberStyle.Foreground(t.DiffAdded()).Background(t.DiffAddedLineNumberBg())
case LineRemoved:
marker = "?"
bgStyle = contextLineStyle
@@ -815,7 +778,7 @@ func renderRightColumn(fileName string, dl *DiffLine, colWidth int, styles Style
// Apply intra-line highlighting for added lines
if dl.Kind == LineAdded && len(dl.Segments) > 0 {
content = applyHighlighting(content, dl.Segments, LineAdded, styles.AddedHighlightBg)
content = applyHighlighting(content, dl.Segments, LineAdded, t.DiffHighlightAdded())
}
// Add a padding space for added lines
@@ -829,7 +792,7 @@ func renderRightColumn(fileName string, dl *DiffLine, colWidth int, styles Style
ansi.Truncate(
lineText,
colWidth,
lipgloss.NewStyle().Background(styles.HunkLineBg).Foreground(styles.HunkLineFg).Render("..."),
lipgloss.NewStyle().Background(bgStyle.GetBackground()).Foreground(t.TextMuted()).Render("..."),
),
)
}
@@ -848,7 +811,7 @@ func RenderSideBySideHunk(fileName string, h Hunk, opts ...SideBySideOption) str
copy(hunkCopy.Lines, h.Lines)
// Highlight changes within lines
HighlightIntralineChanges(&hunkCopy, config.Style)
HighlightIntralineChanges(&hunkCopy)
// Pair lines for side-by-side display
pairs := pairLines(hunkCopy.Lines)
@@ -860,8 +823,8 @@ func RenderSideBySideHunk(fileName string, h Hunk, opts ...SideBySideOption) str
rightWidth := config.TotalWidth - colWidth
var sb strings.Builder
for _, p := range pairs {
leftStr := renderLeftColumn(fileName, p.left, leftWidth, config.Style)
rightStr := renderRightColumn(fileName, p.right, rightWidth, config.Style)
leftStr := renderLeftColumn(fileName, p.left, leftWidth)
rightStr := renderRightColumn(fileName, p.right, rightWidth)
sb.WriteString(leftStr + rightStr + "\n")
}
@@ -876,54 +839,7 @@ func FormatDiff(diffText string, opts ...SideBySideOption) (string, error) {
}
var sb strings.Builder
config := NewSideBySideConfig(opts...)
if config.Style.ShowHeader {
removeIcon := lipgloss.NewStyle().
Background(config.Style.RemovedLineBg).
Foreground(config.Style.RemovedFg).
Render("⏹")
addIcon := lipgloss.NewStyle().
Background(config.Style.AddedLineBg).
Foreground(config.Style.AddedFg).
Render("⏹")
fileName := lipgloss.NewStyle().
Background(config.Style.ContextLineBg).
Foreground(config.Style.FileNameFg).
Render(" " + diffResult.OldFile)
sb.WriteString(
lipgloss.NewStyle().
Background(config.Style.ContextLineBg).
Padding(0, 1, 0, 1).
Foreground(config.Style.FileNameFg).
BorderStyle(lipgloss.NormalBorder()).
BorderTop(true).
BorderBottom(true).
BorderForeground(config.Style.FileNameFg).
BorderBackground(config.Style.ContextLineBg).
Width(config.TotalWidth).
Render(
lipgloss.JoinHorizontal(lipgloss.Top,
removeIcon,
addIcon,
fileName,
),
) + "\n",
)
}
for _, h := range diffResult.Hunks {
// Render hunk header
if config.Style.ShowHunkHeader {
sb.WriteString(
lipgloss.NewStyle().
Background(config.Style.HunkLineBg).
Foreground(config.Style.HunkLineFg).
Width(config.TotalWidth).
Render(h.Header) + "\n",
)
}
sb.WriteString(RenderSideBySideHunk(diffResult.OldFile, h, opts...))
}
@@ -944,8 +860,8 @@ func GenerateDiff(beforeContent, afterContent, fileName string) (string, int, in
removals = 0
)
lines := strings.Split(unified, "\n")
for _, line := range lines {
lines := strings.SplitSeq(unified, "\n")
for line := range lines {
if strings.HasPrefix(line, "+") && !strings.HasPrefix(line, "+++") {
additions++
} else if strings.HasPrefix(line, "-") && !strings.HasPrefix(line, "---") {
+20
View File
@@ -42,6 +42,7 @@ type Service interface {
Cancel(sessionID string)
IsSessionBusy(sessionID string) bool
IsBusy() bool
Update(agentName config.AgentName, modelID models.ModelID) (models.Model, error)
}
type agent struct {
@@ -436,6 +437,25 @@ func (a *agent) TrackUsage(ctx context.Context, sessionID string, model models.M
return nil
}
func (a *agent) Update(agentName config.AgentName, modelID models.ModelID) (models.Model, error) {
if a.IsBusy() {
return models.Model{}, fmt.Errorf("cannot change model while processing requests")
}
if err := config.UpdateAgentModel(agentName, modelID); err != nil {
return models.Model{}, fmt.Errorf("failed to update config: %w", err)
}
provider, err := createAgentProvider(agentName)
if err != nil {
return models.Model{}, fmt.Errorf("failed to create provider for model %s: %w", modelID, err)
}
a.provider = provider
return a.provider.Model(), nil
}
func createAgentProvider(agentName config.AgentName) (provider.Provider, error) {
cfg := config.Get()
agentConfig, ok := cfg.Agents[agentName]
+3 -3
View File
@@ -11,8 +11,8 @@ const (
Claude3Opus ModelID = "claude-3-opus"
)
// https://docs.anthropic.com/en/docs/about-claude/models/all-models
var AnthropicModels = map[ModelID]Model{
// Anthropic
Claude35Sonnet: {
ID: Claude35Sonnet,
Name: "Claude 3.5 Sonnet",
@@ -29,13 +29,13 @@ var AnthropicModels = map[ModelID]Model{
ID: Claude3Haiku,
Name: "Claude 3 Haiku",
Provider: ProviderAnthropic,
APIModel: "claude-3-haiku-latest",
APIModel: "claude-3-haiku-20240307", // doesn't support "-latest"
CostPer1MIn: 0.25,
CostPer1MInCached: 0.30,
CostPer1MOutCached: 0.03,
CostPer1MOut: 1.25,
ContextWindow: 200000,
DefaultMaxTokens: 5000,
DefaultMaxTokens: 4096,
},
Claude37Sonnet: {
ID: Claude37Sonnet,
+157
View File
@@ -0,0 +1,157 @@
package models
const ProviderAzure ModelProvider = "azure"
const (
AzureGPT41 ModelID = "azure.gpt-4.1"
AzureGPT41Mini ModelID = "azure.gpt-4.1-mini"
AzureGPT41Nano ModelID = "azure.gpt-4.1-nano"
AzureGPT45Preview ModelID = "azure.gpt-4.5-preview"
AzureGPT4o ModelID = "azure.gpt-4o"
AzureGPT4oMini ModelID = "azure.gpt-4o-mini"
AzureO1 ModelID = "azure.o1"
AzureO1Mini ModelID = "azure.o1-mini"
AzureO3 ModelID = "azure.o3"
AzureO3Mini ModelID = "azure.o3-mini"
AzureO4Mini ModelID = "azure.o4-mini"
)
var AzureModels = map[ModelID]Model{
AzureGPT41: {
ID: AzureGPT41,
Name: "Azure OpenAI GPT 4.1",
Provider: ProviderAzure,
APIModel: "gpt-4.1",
CostPer1MIn: OpenAIModels[GPT41].CostPer1MIn,
CostPer1MInCached: OpenAIModels[GPT41].CostPer1MInCached,
CostPer1MOut: OpenAIModels[GPT41].CostPer1MOut,
CostPer1MOutCached: OpenAIModels[GPT41].CostPer1MOutCached,
ContextWindow: OpenAIModels[GPT41].ContextWindow,
DefaultMaxTokens: OpenAIModels[GPT41].DefaultMaxTokens,
},
AzureGPT41Mini: {
ID: AzureGPT41Mini,
Name: "Azure OpenAI GPT 4.1 mini",
Provider: ProviderAzure,
APIModel: "gpt-4.1-mini",
CostPer1MIn: OpenAIModels[GPT41Mini].CostPer1MIn,
CostPer1MInCached: OpenAIModels[GPT41Mini].CostPer1MInCached,
CostPer1MOut: OpenAIModels[GPT41Mini].CostPer1MOut,
CostPer1MOutCached: OpenAIModels[GPT41Mini].CostPer1MOutCached,
ContextWindow: OpenAIModels[GPT41Mini].ContextWindow,
DefaultMaxTokens: OpenAIModels[GPT41Mini].DefaultMaxTokens,
},
AzureGPT41Nano: {
ID: AzureGPT41Nano,
Name: "Azure OpenAI GPT 4.1 nano",
Provider: ProviderAzure,
APIModel: "gpt-4.1-nano",
CostPer1MIn: OpenAIModels[GPT41Nano].CostPer1MIn,
CostPer1MInCached: OpenAIModels[GPT41Nano].CostPer1MInCached,
CostPer1MOut: OpenAIModels[GPT41Nano].CostPer1MOut,
CostPer1MOutCached: OpenAIModels[GPT41Nano].CostPer1MOutCached,
ContextWindow: OpenAIModels[GPT41Nano].ContextWindow,
DefaultMaxTokens: OpenAIModels[GPT41Nano].DefaultMaxTokens,
},
AzureGPT45Preview: {
ID: AzureGPT45Preview,
Name: "Azure OpenAI GPT 4.5 preview",
Provider: ProviderAzure,
APIModel: "gpt-4.5-preview",
CostPer1MIn: OpenAIModels[GPT45Preview].CostPer1MIn,
CostPer1MInCached: OpenAIModels[GPT45Preview].CostPer1MInCached,
CostPer1MOut: OpenAIModels[GPT45Preview].CostPer1MOut,
CostPer1MOutCached: OpenAIModels[GPT45Preview].CostPer1MOutCached,
ContextWindow: OpenAIModels[GPT45Preview].ContextWindow,
DefaultMaxTokens: OpenAIModels[GPT45Preview].DefaultMaxTokens,
},
AzureGPT4o: {
ID: AzureGPT4o,
Name: "Azure OpenAI GPT-4o",
Provider: ProviderAzure,
APIModel: "gpt-4o",
CostPer1MIn: OpenAIModels[GPT4o].CostPer1MIn,
CostPer1MInCached: OpenAIModels[GPT4o].CostPer1MInCached,
CostPer1MOut: OpenAIModels[GPT4o].CostPer1MOut,
CostPer1MOutCached: OpenAIModels[GPT4o].CostPer1MOutCached,
ContextWindow: OpenAIModels[GPT4o].ContextWindow,
DefaultMaxTokens: OpenAIModels[GPT4o].DefaultMaxTokens,
},
AzureGPT4oMini: {
ID: AzureGPT4oMini,
Name: "Azure OpenAI GPT-4o mini",
Provider: ProviderAzure,
APIModel: "gpt-4o-mini",
CostPer1MIn: OpenAIModels[GPT4oMini].CostPer1MIn,
CostPer1MInCached: OpenAIModels[GPT4oMini].CostPer1MInCached,
CostPer1MOut: OpenAIModels[GPT4oMini].CostPer1MOut,
CostPer1MOutCached: OpenAIModels[GPT4oMini].CostPer1MOutCached,
ContextWindow: OpenAIModels[GPT4oMini].ContextWindow,
DefaultMaxTokens: OpenAIModels[GPT4oMini].DefaultMaxTokens,
},
AzureO1: {
ID: AzureO1,
Name: "Azure OpenAI O1",
Provider: ProviderAzure,
APIModel: "o1",
CostPer1MIn: OpenAIModels[O1].CostPer1MIn,
CostPer1MInCached: OpenAIModels[O1].CostPer1MInCached,
CostPer1MOut: OpenAIModels[O1].CostPer1MOut,
CostPer1MOutCached: OpenAIModels[O1].CostPer1MOutCached,
ContextWindow: OpenAIModels[O1].ContextWindow,
DefaultMaxTokens: OpenAIModels[O1].DefaultMaxTokens,
CanReason: OpenAIModels[O1].CanReason,
},
AzureO1Mini: {
ID: AzureO1Mini,
Name: "Azure OpenAI O1 mini",
Provider: ProviderAzure,
APIModel: "o1-mini",
CostPer1MIn: OpenAIModels[O1Mini].CostPer1MIn,
CostPer1MInCached: OpenAIModels[O1Mini].CostPer1MInCached,
CostPer1MOut: OpenAIModels[O1Mini].CostPer1MOut,
CostPer1MOutCached: OpenAIModels[O1Mini].CostPer1MOutCached,
ContextWindow: OpenAIModels[O1Mini].ContextWindow,
DefaultMaxTokens: OpenAIModels[O1Mini].DefaultMaxTokens,
CanReason: OpenAIModels[O1Mini].CanReason,
},
AzureO3: {
ID: AzureO3,
Name: "Azure OpenAI O3",
Provider: ProviderAzure,
APIModel: "o3",
CostPer1MIn: OpenAIModels[O3].CostPer1MIn,
CostPer1MInCached: OpenAIModels[O3].CostPer1MInCached,
CostPer1MOut: OpenAIModels[O3].CostPer1MOut,
CostPer1MOutCached: OpenAIModels[O3].CostPer1MOutCached,
ContextWindow: OpenAIModels[O3].ContextWindow,
DefaultMaxTokens: OpenAIModels[O3].DefaultMaxTokens,
CanReason: OpenAIModels[O3].CanReason,
},
AzureO3Mini: {
ID: AzureO3Mini,
Name: "Azure OpenAI O3 mini",
Provider: ProviderAzure,
APIModel: "o3-mini",
CostPer1MIn: OpenAIModels[O3Mini].CostPer1MIn,
CostPer1MInCached: OpenAIModels[O3Mini].CostPer1MInCached,
CostPer1MOut: OpenAIModels[O3Mini].CostPer1MOut,
CostPer1MOutCached: OpenAIModels[O3Mini].CostPer1MOutCached,
ContextWindow: OpenAIModels[O3Mini].ContextWindow,
DefaultMaxTokens: OpenAIModels[O3Mini].DefaultMaxTokens,
CanReason: OpenAIModels[O3Mini].CanReason,
},
AzureO4Mini: {
ID: AzureO4Mini,
Name: "Azure OpenAI O4 mini",
Provider: ProviderAzure,
APIModel: "o4-mini",
CostPer1MIn: OpenAIModels[O4Mini].CostPer1MIn,
CostPer1MInCached: OpenAIModels[O4Mini].CostPer1MInCached,
CostPer1MOut: OpenAIModels[O4Mini].CostPer1MOut,
CostPer1MOutCached: OpenAIModels[O4Mini].CostPer1MOutCached,
ContextWindow: OpenAIModels[O4Mini].ContextWindow,
DefaultMaxTokens: OpenAIModels[O4Mini].DefaultMaxTokens,
CanReason: OpenAIModels[O4Mini].CanReason,
},
}
+13
View File
@@ -33,6 +33,17 @@ const (
ProviderMock ModelProvider = "__mock"
)
// Providers in order of popularity
var ProviderPopularity = map[ModelProvider]int{
ProviderAnthropic: 1,
ProviderOpenAI: 2,
ProviderGemini: 3,
ProviderGROQ: 4,
ProviderOpenRouter: 5,
ProviderBedrock: 6,
ProviderAzure: 7,
}
var SupportedModels = map[ModelID]Model{
//
// // GEMINI
@@ -76,4 +87,6 @@ func init() {
maps.Copy(SupportedModels, OpenAIModels)
maps.Copy(SupportedModels, GeminiModels)
maps.Copy(SupportedModels, GroqModels)
maps.Copy(SupportedModels, AzureModels)
maps.Copy(SupportedModels, OpenRouterModels)
}
+262
View File
@@ -0,0 +1,262 @@
package models
const (
ProviderOpenRouter ModelProvider = "openrouter"
OpenRouterGPT41 ModelID = "openrouter.gpt-4.1"
OpenRouterGPT41Mini ModelID = "openrouter.gpt-4.1-mini"
OpenRouterGPT41Nano ModelID = "openrouter.gpt-4.1-nano"
OpenRouterGPT45Preview ModelID = "openrouter.gpt-4.5-preview"
OpenRouterGPT4o ModelID = "openrouter.gpt-4o"
OpenRouterGPT4oMini ModelID = "openrouter.gpt-4o-mini"
OpenRouterO1 ModelID = "openrouter.o1"
OpenRouterO1Pro ModelID = "openrouter.o1-pro"
OpenRouterO1Mini ModelID = "openrouter.o1-mini"
OpenRouterO3 ModelID = "openrouter.o3"
OpenRouterO3Mini ModelID = "openrouter.o3-mini"
OpenRouterO4Mini ModelID = "openrouter.o4-mini"
OpenRouterGemini25Flash ModelID = "openrouter.gemini-2.5-flash"
OpenRouterGemini25 ModelID = "openrouter.gemini-2.5"
OpenRouterClaude35Sonnet ModelID = "openrouter.claude-3.5-sonnet"
OpenRouterClaude3Haiku ModelID = "openrouter.claude-3-haiku"
OpenRouterClaude37Sonnet ModelID = "openrouter.claude-3.7-sonnet"
OpenRouterClaude35Haiku ModelID = "openrouter.claude-3.5-haiku"
OpenRouterClaude3Opus ModelID = "openrouter.claude-3-opus"
)
var OpenRouterModels = map[ModelID]Model{
OpenRouterGPT41: {
ID: OpenRouterGPT41,
Name: "OpenRouter GPT 4.1",
Provider: ProviderOpenRouter,
APIModel: "openai/gpt-4.1",
CostPer1MIn: OpenAIModels[GPT41].CostPer1MIn,
CostPer1MInCached: OpenAIModels[GPT41].CostPer1MInCached,
CostPer1MOut: OpenAIModels[GPT41].CostPer1MOut,
CostPer1MOutCached: OpenAIModels[GPT41].CostPer1MOutCached,
ContextWindow: OpenAIModels[GPT41].ContextWindow,
DefaultMaxTokens: OpenAIModels[GPT41].DefaultMaxTokens,
},
OpenRouterGPT41Mini: {
ID: OpenRouterGPT41Mini,
Name: "OpenRouter GPT 4.1 mini",
Provider: ProviderOpenRouter,
APIModel: "openai/gpt-4.1-mini",
CostPer1MIn: OpenAIModels[GPT41Mini].CostPer1MIn,
CostPer1MInCached: OpenAIModels[GPT41Mini].CostPer1MInCached,
CostPer1MOut: OpenAIModels[GPT41Mini].CostPer1MOut,
CostPer1MOutCached: OpenAIModels[GPT41Mini].CostPer1MOutCached,
ContextWindow: OpenAIModels[GPT41Mini].ContextWindow,
DefaultMaxTokens: OpenAIModels[GPT41Mini].DefaultMaxTokens,
},
OpenRouterGPT41Nano: {
ID: OpenRouterGPT41Nano,
Name: "OpenRouter GPT 4.1 nano",
Provider: ProviderOpenRouter,
APIModel: "openai/gpt-4.1-nano",
CostPer1MIn: OpenAIModels[GPT41Nano].CostPer1MIn,
CostPer1MInCached: OpenAIModels[GPT41Nano].CostPer1MInCached,
CostPer1MOut: OpenAIModels[GPT41Nano].CostPer1MOut,
CostPer1MOutCached: OpenAIModels[GPT41Nano].CostPer1MOutCached,
ContextWindow: OpenAIModels[GPT41Nano].ContextWindow,
DefaultMaxTokens: OpenAIModels[GPT41Nano].DefaultMaxTokens,
},
OpenRouterGPT45Preview: {
ID: OpenRouterGPT45Preview,
Name: "OpenRouter GPT 4.5 preview",
Provider: ProviderOpenRouter,
APIModel: "openai/gpt-4.5-preview",
CostPer1MIn: OpenAIModels[GPT45Preview].CostPer1MIn,
CostPer1MInCached: OpenAIModels[GPT45Preview].CostPer1MInCached,
CostPer1MOut: OpenAIModels[GPT45Preview].CostPer1MOut,
CostPer1MOutCached: OpenAIModels[GPT45Preview].CostPer1MOutCached,
ContextWindow: OpenAIModels[GPT45Preview].ContextWindow,
DefaultMaxTokens: OpenAIModels[GPT45Preview].DefaultMaxTokens,
},
OpenRouterGPT4o: {
ID: OpenRouterGPT4o,
Name: "OpenRouter GPT 4o",
Provider: ProviderOpenRouter,
APIModel: "openai/gpt-4o",
CostPer1MIn: OpenAIModels[GPT4o].CostPer1MIn,
CostPer1MInCached: OpenAIModels[GPT4o].CostPer1MInCached,
CostPer1MOut: OpenAIModels[GPT4o].CostPer1MOut,
CostPer1MOutCached: OpenAIModels[GPT4o].CostPer1MOutCached,
ContextWindow: OpenAIModels[GPT4o].ContextWindow,
DefaultMaxTokens: OpenAIModels[GPT4o].DefaultMaxTokens,
},
OpenRouterGPT4oMini: {
ID: OpenRouterGPT4oMini,
Name: "OpenRouter GPT 4o mini",
Provider: ProviderOpenRouter,
APIModel: "openai/gpt-4o-mini",
CostPer1MIn: OpenAIModels[GPT4oMini].CostPer1MIn,
CostPer1MInCached: OpenAIModels[GPT4oMini].CostPer1MInCached,
CostPer1MOut: OpenAIModels[GPT4oMini].CostPer1MOut,
CostPer1MOutCached: OpenAIModels[GPT4oMini].CostPer1MOutCached,
ContextWindow: OpenAIModels[GPT4oMini].ContextWindow,
},
OpenRouterO1: {
ID: OpenRouterO1,
Name: "OpenRouter O1",
Provider: ProviderOpenRouter,
APIModel: "openai/o1",
CostPer1MIn: OpenAIModels[O1].CostPer1MIn,
CostPer1MInCached: OpenAIModels[O1].CostPer1MInCached,
CostPer1MOut: OpenAIModels[O1].CostPer1MOut,
CostPer1MOutCached: OpenAIModels[O1].CostPer1MOutCached,
ContextWindow: OpenAIModels[O1].ContextWindow,
DefaultMaxTokens: OpenAIModels[O1].DefaultMaxTokens,
CanReason: OpenAIModels[O1].CanReason,
},
OpenRouterO1Pro: {
ID: OpenRouterO1Pro,
Name: "OpenRouter o1 pro",
Provider: ProviderOpenRouter,
APIModel: "openai/o1-pro",
CostPer1MIn: OpenAIModels[O1Pro].CostPer1MIn,
CostPer1MInCached: OpenAIModels[O1Pro].CostPer1MInCached,
CostPer1MOut: OpenAIModels[O1Pro].CostPer1MOut,
CostPer1MOutCached: OpenAIModels[O1Pro].CostPer1MOutCached,
ContextWindow: OpenAIModels[O1Pro].ContextWindow,
DefaultMaxTokens: OpenAIModels[O1Pro].DefaultMaxTokens,
CanReason: OpenAIModels[O1Pro].CanReason,
},
OpenRouterO1Mini: {
ID: OpenRouterO1Mini,
Name: "OpenRouter o1 mini",
Provider: ProviderOpenRouter,
APIModel: "openai/o1-mini",
CostPer1MIn: OpenAIModels[O1Mini].CostPer1MIn,
CostPer1MInCached: OpenAIModels[O1Mini].CostPer1MInCached,
CostPer1MOut: OpenAIModels[O1Mini].CostPer1MOut,
CostPer1MOutCached: OpenAIModels[O1Mini].CostPer1MOutCached,
ContextWindow: OpenAIModels[O1Mini].ContextWindow,
DefaultMaxTokens: OpenAIModels[O1Mini].DefaultMaxTokens,
CanReason: OpenAIModels[O1Mini].CanReason,
},
OpenRouterO3: {
ID: OpenRouterO3,
Name: "OpenRouter o3",
Provider: ProviderOpenRouter,
APIModel: "openai/o3",
CostPer1MIn: OpenAIModels[O3].CostPer1MIn,
CostPer1MInCached: OpenAIModels[O3].CostPer1MInCached,
CostPer1MOut: OpenAIModels[O3].CostPer1MOut,
CostPer1MOutCached: OpenAIModels[O3].CostPer1MOutCached,
ContextWindow: OpenAIModels[O3].ContextWindow,
DefaultMaxTokens: OpenAIModels[O3].DefaultMaxTokens,
CanReason: OpenAIModels[O3].CanReason,
},
OpenRouterO3Mini: {
ID: OpenRouterO3Mini,
Name: "OpenRouter o3 mini",
Provider: ProviderOpenRouter,
APIModel: "openai/o3-mini-high",
CostPer1MIn: OpenAIModels[O3Mini].CostPer1MIn,
CostPer1MInCached: OpenAIModels[O3Mini].CostPer1MInCached,
CostPer1MOut: OpenAIModels[O3Mini].CostPer1MOut,
CostPer1MOutCached: OpenAIModels[O3Mini].CostPer1MOutCached,
ContextWindow: OpenAIModels[O3Mini].ContextWindow,
DefaultMaxTokens: OpenAIModels[O3Mini].DefaultMaxTokens,
CanReason: OpenAIModels[O3Mini].CanReason,
},
OpenRouterO4Mini: {
ID: OpenRouterO4Mini,
Name: "OpenRouter o4 mini",
Provider: ProviderOpenRouter,
APIModel: "openai/o4-mini-high",
CostPer1MIn: OpenAIModels[O4Mini].CostPer1MIn,
CostPer1MInCached: OpenAIModels[O4Mini].CostPer1MInCached,
CostPer1MOut: OpenAIModels[O4Mini].CostPer1MOut,
CostPer1MOutCached: OpenAIModels[O4Mini].CostPer1MOutCached,
ContextWindow: OpenAIModels[O4Mini].ContextWindow,
DefaultMaxTokens: OpenAIModels[O4Mini].DefaultMaxTokens,
CanReason: OpenAIModels[O4Mini].CanReason,
},
OpenRouterGemini25Flash: {
ID: OpenRouterGemini25Flash,
Name: "OpenRouter Gemini 2.5 Flash",
Provider: ProviderOpenRouter,
APIModel: "google/gemini-2.5-flash-preview:thinking",
CostPer1MIn: GeminiModels[Gemini25Flash].CostPer1MIn,
CostPer1MInCached: GeminiModels[Gemini25Flash].CostPer1MInCached,
CostPer1MOut: GeminiModels[Gemini25Flash].CostPer1MOut,
CostPer1MOutCached: GeminiModels[Gemini25Flash].CostPer1MOutCached,
ContextWindow: GeminiModels[Gemini25Flash].ContextWindow,
DefaultMaxTokens: GeminiModels[Gemini25Flash].DefaultMaxTokens,
},
OpenRouterGemini25: {
ID: OpenRouterGemini25,
Name: "OpenRouter Gemini 2.5 Pro",
Provider: ProviderOpenRouter,
APIModel: "google/gemini-2.5-pro-preview-03-25",
CostPer1MIn: GeminiModels[Gemini25].CostPer1MIn,
CostPer1MInCached: GeminiModels[Gemini25].CostPer1MInCached,
CostPer1MOut: GeminiModels[Gemini25].CostPer1MOut,
CostPer1MOutCached: GeminiModels[Gemini25].CostPer1MOutCached,
ContextWindow: GeminiModels[Gemini25].ContextWindow,
DefaultMaxTokens: GeminiModels[Gemini25].DefaultMaxTokens,
},
OpenRouterClaude35Sonnet: {
ID: OpenRouterClaude35Sonnet,
Name: "OpenRouter Claude 3.5 Sonnet",
Provider: ProviderOpenRouter,
APIModel: "anthropic/claude-3.5-sonnet",
CostPer1MIn: AnthropicModels[Claude35Sonnet].CostPer1MIn,
CostPer1MInCached: AnthropicModels[Claude35Sonnet].CostPer1MInCached,
CostPer1MOut: AnthropicModels[Claude35Sonnet].CostPer1MOut,
CostPer1MOutCached: AnthropicModels[Claude35Sonnet].CostPer1MOutCached,
ContextWindow: AnthropicModels[Claude35Sonnet].ContextWindow,
DefaultMaxTokens: AnthropicModels[Claude35Sonnet].DefaultMaxTokens,
},
OpenRouterClaude3Haiku: {
ID: OpenRouterClaude3Haiku,
Name: "OpenRouter Claude 3 Haiku",
Provider: ProviderOpenRouter,
APIModel: "anthropic/claude-3-haiku",
CostPer1MIn: AnthropicModels[Claude3Haiku].CostPer1MIn,
CostPer1MInCached: AnthropicModels[Claude3Haiku].CostPer1MInCached,
CostPer1MOut: AnthropicModels[Claude3Haiku].CostPer1MOut,
CostPer1MOutCached: AnthropicModels[Claude3Haiku].CostPer1MOutCached,
ContextWindow: AnthropicModels[Claude3Haiku].ContextWindow,
DefaultMaxTokens: AnthropicModels[Claude3Haiku].DefaultMaxTokens,
},
OpenRouterClaude37Sonnet: {
ID: OpenRouterClaude37Sonnet,
Name: "OpenRouter Claude 3.7 Sonnet",
Provider: ProviderOpenRouter,
APIModel: "anthropic/claude-3.7-sonnet",
CostPer1MIn: AnthropicModels[Claude37Sonnet].CostPer1MIn,
CostPer1MInCached: AnthropicModels[Claude37Sonnet].CostPer1MInCached,
CostPer1MOut: AnthropicModels[Claude37Sonnet].CostPer1MOut,
CostPer1MOutCached: AnthropicModels[Claude37Sonnet].CostPer1MOutCached,
ContextWindow: AnthropicModels[Claude37Sonnet].ContextWindow,
DefaultMaxTokens: AnthropicModels[Claude37Sonnet].DefaultMaxTokens,
CanReason: AnthropicModels[Claude37Sonnet].CanReason,
},
OpenRouterClaude35Haiku: {
ID: OpenRouterClaude35Haiku,
Name: "OpenRouter Claude 3.5 Haiku",
Provider: ProviderOpenRouter,
APIModel: "anthropic/claude-3.5-haiku",
CostPer1MIn: AnthropicModels[Claude35Haiku].CostPer1MIn,
CostPer1MInCached: AnthropicModels[Claude35Haiku].CostPer1MInCached,
CostPer1MOut: AnthropicModels[Claude35Haiku].CostPer1MOut,
CostPer1MOutCached: AnthropicModels[Claude35Haiku].CostPer1MOutCached,
ContextWindow: AnthropicModels[Claude35Haiku].ContextWindow,
DefaultMaxTokens: AnthropicModels[Claude35Haiku].DefaultMaxTokens,
},
OpenRouterClaude3Opus: {
ID: OpenRouterClaude3Opus,
Name: "OpenRouter Claude 3 Opus",
Provider: ProviderOpenRouter,
APIModel: "anthropic/claude-3-opus",
CostPer1MIn: AnthropicModels[Claude3Opus].CostPer1MIn,
CostPer1MInCached: AnthropicModels[Claude3Opus].CostPer1MInCached,
CostPer1MOut: AnthropicModels[Claude3Opus].CostPer1MOut,
CostPer1MOutCached: AnthropicModels[Claude3Opus].CostPer1MOutCached,
ContextWindow: AnthropicModels[Claude3Opus].ContextWindow,
DefaultMaxTokens: AnthropicModels[Claude3Opus].DefaultMaxTokens,
},
}
+98 -47
View File
@@ -5,26 +5,13 @@ import (
"os"
"path/filepath"
"strings"
"sync"
"github.com/opencode-ai/opencode/internal/config"
"github.com/opencode-ai/opencode/internal/llm/models"
"github.com/opencode-ai/opencode/internal/logging"
)
// contextFiles is a list of potential context files to check for
var contextFiles = []string{
".github/copilot-instructions.md",
".cursorrules",
".cursor/rules/", // Directory containing multiple rule files
"CLAUDE.md",
"CLAUDE.local.md",
"opencode.md",
"opencode.local.md",
"OpenCode.md",
"OpenCode.local.md",
"OPENCODE.md",
"OPENCODE.local.md",
}
func GetAgentPrompt(agentName config.AgentName, provider models.ModelProvider) string {
basePrompt := ""
switch agentName {
@@ -40,45 +27,109 @@ func GetAgentPrompt(agentName config.AgentName, provider models.ModelProvider) s
if agentName == config.AgentCoder || agentName == config.AgentTask {
// Add context from project-specific instruction files if they exist
contextContent := getContextFromFiles()
contextContent := getContextFromPaths()
logging.Debug("Context content", "Context", contextContent)
if contextContent != "" {
return fmt.Sprintf("%s\n\n# Project-Specific Context\n%s", basePrompt, contextContent)
return fmt.Sprintf("%s\n\n# Project-Specific Context\n Make sure to follow the instructions in the context below\n%s", basePrompt, contextContent)
}
}
return basePrompt
}
// getContextFromFiles checks for the existence of context files and returns their content
func getContextFromFiles() string {
workDir := config.WorkingDirectory()
var contextContent string
var (
onceContext sync.Once
contextContent string
)
for _, path := range contextFiles {
// Check if path ends with a slash (indicating a directory)
if strings.HasSuffix(path, "/") {
// Handle directory - read all files within it
dirPath := filepath.Join(workDir, path)
files, err := os.ReadDir(dirPath)
if err == nil {
for _, file := range files {
if !file.IsDir() {
filePath := filepath.Join(dirPath, file.Name())
content, err := os.ReadFile(filePath)
if err == nil {
contextContent += fmt.Sprintf("\n# From %s\n%s\n", file.Name(), string(content))
}
}
}
}
} else {
// Handle individual file as before
filePath := filepath.Join(workDir, path)
content, err := os.ReadFile(filePath)
if err == nil {
contextContent += fmt.Sprintf("\n%s\n", string(content))
}
}
}
func getContextFromPaths() string {
onceContext.Do(func() {
var (
cfg = config.Get()
workDir = cfg.WorkingDir
contextPaths = cfg.ContextPaths
)
contextContent = processContextPaths(workDir, contextPaths)
})
return contextContent
}
func processContextPaths(workDir string, paths []string) string {
var (
wg sync.WaitGroup
resultCh = make(chan string)
)
// Track processed files to avoid duplicates
processedFiles := make(map[string]bool)
var processedMutex sync.Mutex
for _, path := range paths {
wg.Add(1)
go func(p string) {
defer wg.Done()
if strings.HasSuffix(p, "/") {
filepath.WalkDir(filepath.Join(workDir, p), func(path string, d os.DirEntry, err error) error {
if err != nil {
return err
}
if !d.IsDir() {
// Check if we've already processed this file (case-insensitive)
processedMutex.Lock()
lowerPath := strings.ToLower(path)
if !processedFiles[lowerPath] {
processedFiles[lowerPath] = true
processedMutex.Unlock()
if result := processFile(path); result != "" {
resultCh <- result
}
} else {
processedMutex.Unlock()
}
}
return nil
})
} else {
fullPath := filepath.Join(workDir, p)
// Check if we've already processed this file (case-insensitive)
processedMutex.Lock()
lowerPath := strings.ToLower(fullPath)
if !processedFiles[lowerPath] {
processedFiles[lowerPath] = true
processedMutex.Unlock()
result := processFile(fullPath)
if result != "" {
resultCh <- result
}
} else {
processedMutex.Unlock()
}
}
}(path)
}
go func() {
wg.Wait()
close(resultCh)
}()
results := make([]string, 0)
for result := range resultCh {
results = append(results, result)
}
return strings.Join(results, "\n")
}
func processFile(filePath string) string {
content, err := os.ReadFile(filePath)
if err != nil {
return ""
}
return "# From:" + filePath + "\n" + string(content)
}
+57
View File
@@ -0,0 +1,57 @@
package prompt
import (
"fmt"
"os"
"path/filepath"
"testing"
"github.com/opencode-ai/opencode/internal/config"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestGetContextFromPaths(t *testing.T) {
t.Parallel()
tmpDir := t.TempDir()
_, err := config.Load(tmpDir, false)
if err != nil {
t.Fatalf("Failed to load config: %v", err)
}
cfg := config.Get()
cfg.WorkingDir = tmpDir
cfg.ContextPaths = []string{
"file.txt",
"directory/",
}
testFiles := []string{
"file.txt",
"directory/file_a.txt",
"directory/file_b.txt",
"directory/file_c.txt",
}
createTestFiles(t, tmpDir, testFiles)
context := getContextFromPaths()
expectedContext := fmt.Sprintf("# From:%s/file.txt\nfile.txt: test content\n# From:%s/directory/file_a.txt\ndirectory/file_a.txt: test content\n# From:%s/directory/file_b.txt\ndirectory/file_b.txt: test content\n# From:%s/directory/file_c.txt\ndirectory/file_c.txt: test content", tmpDir, tmpDir, tmpDir, tmpDir)
assert.Equal(t, expectedContext, context)
}
func createTestFiles(t *testing.T, tmpDir string, testFiles []string) {
t.Helper()
for _, path := range testFiles {
fullPath := filepath.Join(tmpDir, path)
if path[len(path)-1] == '/' {
err := os.MkdirAll(fullPath, 0755)
require.NoError(t, err)
} else {
dir := filepath.Dir(fullPath)
err := os.MkdirAll(dir, 0755)
require.NoError(t, err)
err = os.WriteFile(fullPath, []byte(path+": test content"), 0644)
require.NoError(t, err)
}
}
}
+47
View File
@@ -0,0 +1,47 @@
package provider
import (
"os"
"github.com/Azure/azure-sdk-for-go/sdk/azidentity"
"github.com/openai/openai-go"
"github.com/openai/openai-go/azure"
"github.com/openai/openai-go/option"
)
type azureClient struct {
*openaiClient
}
type AzureClient ProviderClient
func newAzureClient(opts providerClientOptions) AzureClient {
endpoint := os.Getenv("AZURE_OPENAI_ENDPOINT") // ex: https://foo.openai.azure.com
apiVersion := os.Getenv("AZURE_OPENAI_API_VERSION") // ex: 2025-04-01-preview
if endpoint == "" || apiVersion == "" {
return &azureClient{openaiClient: newOpenAIClient(opts).(*openaiClient)}
}
reqOpts := []option.RequestOption{
azure.WithEndpoint(endpoint, apiVersion),
}
if opts.apiKey != "" || os.Getenv("AZURE_OPENAI_API_KEY") != "" {
key := opts.apiKey
if key == "" {
key = os.Getenv("AZURE_OPENAI_API_KEY")
}
reqOpts = append(reqOpts, azure.WithAPIKey(key))
} else if cred, err := azidentity.NewDefaultAzureCredential(nil); err == nil {
reqOpts = append(reqOpts, azure.WithTokenCredential(cred))
}
base := &openaiClient{
providerOptions: opts,
client: openai.NewClient(reqOpts...),
}
return &azureClient{openaiClient: base}
}
+9 -2
View File
@@ -221,7 +221,10 @@ func (g *geminiClient) send(ctx context.Context, messages []message.Message, too
}
}
}
finishReason := g.finishReason(resp.Candidates[0].FinishReason)
finishReason := message.FinishReasonEndTurn
if len(resp.Candidates) > 0 {
finishReason = g.finishReason(resp.Candidates[0].FinishReason)
}
if len(toolCalls) > 0 {
finishReason = message.FinishReasonToolUse
}
@@ -350,7 +353,11 @@ func (g *geminiClient) stream(ctx context.Context, messages []message.Message, t
eventChan <- ProviderEvent{Type: EventContentStop}
if finalResp != nil {
finishReason := g.finishReason(finalResp.Candidates[0].FinishReason)
finishReason := message.FinishReasonEndTurn
if len(finalResp.Candidates) > 0 {
finishReason = g.finishReason(finalResp.Candidates[0].FinishReason)
}
if len(toolCalls) > 0 {
finishReason = message.FinishReasonToolUse
}
+29 -3
View File
@@ -21,6 +21,7 @@ type openaiOptions struct {
baseURL string
disableCache bool
reasoningEffort string
extraHeaders map[string]string
}
type OpenAIOption func(*openaiOptions)
@@ -49,6 +50,12 @@ func newOpenAIClient(opts providerClientOptions) OpenAIClient {
openaiClientOptions = append(openaiClientOptions, option.WithBaseURL(openaiOpts.baseURL))
}
if openaiOpts.extraHeaders != nil {
for key, value := range openaiOpts.extraHeaders {
openaiClientOptions = append(openaiClientOptions, option.WithHeader(key, value))
}
}
client := openai.NewClient(openaiClientOptions...)
return &openaiClient{
providerOptions: opts,
@@ -204,11 +211,18 @@ func (o *openaiClient) send(ctx context.Context, messages []message.Message, too
content = openaiResponse.Choices[0].Message.Content
}
toolCalls := o.toolCalls(*openaiResponse)
finishReason := o.finishReason(string(openaiResponse.Choices[0].FinishReason))
if len(toolCalls) > 0 {
finishReason = message.FinishReasonToolUse
}
return &ProviderResponse{
Content: content,
ToolCalls: o.toolCalls(*openaiResponse),
ToolCalls: toolCalls,
Usage: o.usage(*openaiResponse),
FinishReason: o.finishReason(string(openaiResponse.Choices[0].FinishReason)),
FinishReason: finishReason,
}, nil
}
}
@@ -267,13 +281,19 @@ func (o *openaiClient) stream(ctx context.Context, messages []message.Message, t
err := openaiStream.Err()
if err == nil || errors.Is(err, io.EOF) {
// Stream completed successfully
finishReason := o.finishReason(string(acc.ChatCompletion.Choices[0].FinishReason))
if len(toolCalls) > 0 {
finishReason = message.FinishReasonToolUse
}
eventChan <- ProviderEvent{
Type: EventComplete,
Response: &ProviderResponse{
Content: currentContent,
ToolCalls: toolCalls,
Usage: o.usage(acc.ChatCompletion),
FinishReason: o.finishReason(string(acc.ChatCompletion.Choices[0].FinishReason)),
FinishReason: finishReason,
},
}
close(eventChan)
@@ -375,6 +395,12 @@ func WithOpenAIBaseURL(baseURL string) OpenAIOption {
}
}
func WithOpenAIExtraHeaders(headers map[string]string) OpenAIOption {
return func(options *openaiOptions) {
options.extraHeaders = headers
}
}
func WithOpenAIDisableCache() OpenAIOption {
return func(options *openaiOptions) {
options.disableCache = true
+17
View File
@@ -115,6 +115,23 @@ func NewProvider(providerName models.ModelProvider, opts ...ProviderClientOption
options: clientOptions,
client: newOpenAIClient(clientOptions),
}, nil
case models.ProviderAzure:
return &baseProvider[AzureClient]{
options: clientOptions,
client: newAzureClient(clientOptions),
}, nil
case models.ProviderOpenRouter:
clientOptions.openaiOptions = append(clientOptions.openaiOptions,
WithOpenAIBaseURL("https://openrouter.ai/api/v1"),
WithOpenAIExtraHeaders(map[string]string{
"HTTP-Referer": "opencode.ai",
"X-Title": "OpenCode",
}),
)
return &baseProvider[OpenAIClient]{
options: clientOptions,
client: newOpenAIClient(clientOptions),
}, nil
case models.ProviderMock:
// TODO: implement mock client for test
panic("not implemented")
+11 -4
View File
@@ -389,7 +389,7 @@ func (c *Client) openKeyConfigFiles(ctx context.Context) {
filepath.Join(workDir, "package.json"),
filepath.Join(workDir, "jsconfig.json"),
}
// Also find and open a few TypeScript files to help the server initialize
c.openTypeScriptFiles(ctx, workDir)
case ServerTypeGo:
@@ -547,12 +547,12 @@ func (c *Client) openTypeScriptFiles(ctx context.Context, workDir string) {
// shouldSkipDir returns true if the directory should be skipped during file search
func shouldSkipDir(path string) bool {
dirName := filepath.Base(path)
// Skip hidden directories
if strings.HasPrefix(dirName, ".") {
return true
}
// Skip common directories that won't contain relevant source files
skipDirs := map[string]bool{
"node_modules": true,
@@ -562,7 +562,7 @@ func shouldSkipDir(path string) bool {
"vendor": true,
"target": true,
}
return skipDirs[dirName]
}
@@ -776,3 +776,10 @@ func (c *Client) GetDiagnosticsForFile(ctx context.Context, filepath string) ([]
return diagnostics, nil
}
// ClearDiagnosticsForURI removes diagnostics for a specific URI from the cache
func (c *Client) ClearDiagnosticsForURI(uri protocol.DocumentUri) {
c.diagnosticsMu.Lock()
defer c.diagnosticsMu.Unlock()
delete(c.diagnostics, uri)
}
+3 -1
View File
@@ -643,7 +643,9 @@ func (w *WorkspaceWatcher) debounceHandleFileEvent(ctx context.Context, uri stri
func (w *WorkspaceWatcher) handleFileEvent(ctx context.Context, uri string, changeType protocol.FileChangeType) {
// If the file is open and it's a change event, use didChange notification
filePath := uri[7:] // Remove "file://" prefix
if changeType == protocol.FileChangeType(protocol.Changed) && w.client.IsFileOpen(filePath) {
if changeType == protocol.FileChangeType(protocol.Deleted) {
w.client.ClearDiagnosticsForURI(protocol.DocumentUri(uri))
} else if changeType == protocol.FileChangeType(protocol.Changed) && w.client.IsFileOpen(filePath) {
err := w.client.NotifyChange(ctx, filePath)
if err != nil {
logging.Error("Error notifying change", "error", err)
+46 -26
View File
@@ -9,6 +9,7 @@ import (
"github.com/opencode-ai/opencode/internal/config"
"github.com/opencode-ai/opencode/internal/session"
"github.com/opencode-ai/opencode/internal/tui/styles"
"github.com/opencode-ai/opencode/internal/tui/theme"
"github.com/opencode-ai/opencode/internal/version"
)
@@ -22,12 +23,29 @@ type SessionClearedMsg struct{}
type EditorFocusMsg bool
func header(width int) string {
return lipgloss.JoinVertical(
lipgloss.Top,
logo(width),
repo(width),
"",
cwd(width),
)
}
func lspsConfigured(width int) string {
cfg := config.Get()
title := "LSP Configuration"
title = ansi.Truncate(title, width, "…")
lsps := styles.BaseStyle.Width(width).Foreground(styles.PrimaryColor).Bold(true).Render(title)
t := theme.CurrentTheme()
baseStyle := styles.BaseStyle()
lsps := baseStyle.
Width(width).
Foreground(t.Primary()).
Bold(true).
Render(title)
// Get LSP names and sort them for consistent ordering
var lspNames []string
@@ -39,16 +57,19 @@ func lspsConfigured(width int) string {
var lspViews []string
for _, name := range lspNames {
lsp := cfg.LSP[name]
lspName := styles.BaseStyle.Foreground(styles.Forground).Render(
fmt.Sprintf("• %s", name),
)
lspName := baseStyle.
Foreground(t.Text()).
Render(fmt.Sprintf("• %s", name))
cmd := lsp.Command
cmd = ansi.Truncate(cmd, width-lipgloss.Width(lspName)-3, "…")
lspPath := styles.BaseStyle.Foreground(styles.ForgroundDim).Render(
fmt.Sprintf(" (%s)", cmd),
)
lspPath := baseStyle.
Foreground(t.TextMuted()).
Render(fmt.Sprintf(" (%s)", cmd))
lspViews = append(lspViews,
styles.BaseStyle.
baseStyle.
Width(width).
Render(
lipgloss.JoinHorizontal(
@@ -59,7 +80,8 @@ func lspsConfigured(width int) string {
),
)
}
return styles.BaseStyle.
return baseStyle.
Width(width).
Render(
lipgloss.JoinVertical(
@@ -75,10 +97,14 @@ func lspsConfigured(width int) string {
func logo(width int) string {
logo := fmt.Sprintf("%s %s", styles.OpenCodeIcon, "OpenCode")
t := theme.CurrentTheme()
baseStyle := styles.BaseStyle()
version := styles.BaseStyle.Foreground(styles.ForgroundDim).Render(version.Version)
versionText := baseStyle.
Foreground(t.TextMuted()).
Render(version.Version)
return styles.BaseStyle.
return baseStyle.
Bold(true).
Width(width).
Render(
@@ -86,34 +112,28 @@ func logo(width int) string {
lipgloss.Left,
logo,
" ",
version,
versionText,
),
)
}
func repo(width int) string {
repo := "https://github.com/opencode-ai/opencode"
return styles.BaseStyle.
Foreground(styles.ForgroundDim).
t := theme.CurrentTheme()
return styles.BaseStyle().
Foreground(t.TextMuted()).
Width(width).
Render(repo)
}
func cwd(width int) string {
cwd := fmt.Sprintf("cwd: %s", config.WorkingDirectory())
return styles.BaseStyle.
Foreground(styles.ForgroundDim).
t := theme.CurrentTheme()
return styles.BaseStyle().
Foreground(t.TextMuted()).
Width(width).
Render(cwd)
}
func header(width int) string {
header := lipgloss.JoinVertical(
lipgloss.Top,
logo(width),
repo(width),
"",
cwd(width),
)
return header
}
+45 -15
View File
@@ -10,8 +10,10 @@ import (
"github.com/charmbracelet/lipgloss"
"github.com/opencode-ai/opencode/internal/app"
"github.com/opencode-ai/opencode/internal/session"
"github.com/opencode-ai/opencode/internal/tui/components/dialog"
"github.com/opencode-ai/opencode/internal/tui/layout"
"github.com/opencode-ai/opencode/internal/tui/styles"
"github.com/opencode-ai/opencode/internal/tui/theme"
"github.com/opencode-ai/opencode/internal/tui/util"
)
@@ -100,6 +102,9 @@ func (m *editorCmp) send() tea.Cmd {
func (m *editorCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var cmd tea.Cmd
switch msg := msg.(type) {
case dialog.ThemeChangedMsg:
m.textarea = CreateTextArea(&m.textarea)
return m, nil
case SessionSelectedMsg:
if msg.ID != m.session.ID {
m.session = msg
@@ -134,7 +139,13 @@ func (m *editorCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
}
func (m *editorCmp) View() string {
style := lipgloss.NewStyle().Padding(0, 0, 0, 1).Bold(true)
t := theme.CurrentTheme()
// Style the prompt with theme colors
style := lipgloss.NewStyle().
Padding(0, 0, 0, 1).
Bold(true).
Foreground(t.Primary())
return lipgloss.JoinHorizontal(lipgloss.Top, style.Render(">"), m.textarea.View())
}
@@ -155,23 +166,42 @@ func (m *editorCmp) BindingKeys() []key.Binding {
return bindings
}
func CreateTextArea(existing *textarea.Model) textarea.Model {
t := theme.CurrentTheme()
bgColor := t.Background()
textColor := t.Text()
textMutedColor := t.TextMuted()
ta := textarea.New()
ta.BlurredStyle.Base = styles.BaseStyle().Background(bgColor).Foreground(textColor)
ta.BlurredStyle.CursorLine = styles.BaseStyle().Background(bgColor)
ta.BlurredStyle.Placeholder = styles.BaseStyle().Background(bgColor).Foreground(textMutedColor)
ta.BlurredStyle.Text = styles.BaseStyle().Background(bgColor).Foreground(textColor)
ta.FocusedStyle.Base = styles.BaseStyle().Background(bgColor).Foreground(textColor)
ta.FocusedStyle.CursorLine = styles.BaseStyle().Background(bgColor)
ta.FocusedStyle.Placeholder = styles.BaseStyle().Background(bgColor).Foreground(textMutedColor)
ta.FocusedStyle.Text = styles.BaseStyle().Background(bgColor).Foreground(textColor)
ta.Prompt = " "
ta.ShowLineNumbers = false
ta.CharLimit = -1
if existing != nil {
ta.SetValue(existing.Value())
ta.SetWidth(existing.Width())
ta.SetHeight(existing.Height())
}
ta.Focus()
return ta
}
func NewEditorCmp(app *app.App) tea.Model {
ti := textarea.New()
ti.Prompt = " "
ti.ShowLineNumbers = false
ti.BlurredStyle.Base = ti.BlurredStyle.Base.Background(styles.Background)
ti.BlurredStyle.CursorLine = ti.BlurredStyle.CursorLine.Background(styles.Background)
ti.BlurredStyle.Placeholder = ti.BlurredStyle.Placeholder.Background(styles.Background)
ti.BlurredStyle.Text = ti.BlurredStyle.Text.Background(styles.Background)
ta := CreateTextArea(nil)
ti.FocusedStyle.Base = ti.FocusedStyle.Base.Background(styles.Background)
ti.FocusedStyle.CursorLine = ti.FocusedStyle.CursorLine.Background(styles.Background)
ti.FocusedStyle.Placeholder = ti.FocusedStyle.Placeholder.Background(styles.Background)
ti.FocusedStyle.Text = ti.BlurredStyle.Text.Background(styles.Background)
ti.CharLimit = -1
ti.Focus()
return &editorCmp{
app: app,
textarea: ti,
textarea: ta,
}
}
+47 -29
View File
@@ -14,7 +14,9 @@ import (
"github.com/opencode-ai/opencode/internal/message"
"github.com/opencode-ai/opencode/internal/pubsub"
"github.com/opencode-ai/opencode/internal/session"
"github.com/opencode-ai/opencode/internal/tui/components/dialog"
"github.com/opencode-ai/opencode/internal/tui/styles"
"github.com/opencode-ai/opencode/internal/tui/theme"
"github.com/opencode-ai/opencode/internal/tui/util"
)
@@ -69,7 +71,9 @@ func (m *messagesCmp) Init() tea.Cmd {
func (m *messagesCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var cmds []tea.Cmd
switch msg := msg.(type) {
case dialog.ThemeChangedMsg:
m.rerender()
return m, nil
case SessionSelectedMsg:
if msg.ID != m.session.ID {
cmd := m.SetSession(msg)
@@ -174,6 +178,7 @@ func formatTimeDifference(unixTime1, unixTime2 int64) string {
func (m *messagesCmp) renderView() {
m.uiMessages = make([]uiMessage, 0)
pos := 0
baseStyle := styles.BaseStyle()
if m.width == 0 {
return
@@ -225,15 +230,13 @@ func (m *messagesCmp) renderView() {
messages := make([]string, 0)
for _, v := range m.uiMessages {
messages = append(messages, v.content,
styles.BaseStyle.
baseStyle.
Width(m.width).
Render(
"",
),
Render(""),
)
}
m.viewport.SetContent(
styles.BaseStyle.
baseStyle.
Width(m.width).
Render(
lipgloss.JoinVertical(
@@ -245,8 +248,10 @@ func (m *messagesCmp) renderView() {
}
func (m *messagesCmp) View() string {
baseStyle := styles.BaseStyle()
if m.rendering {
return styles.BaseStyle.
return baseStyle.
Width(m.width).
Render(
lipgloss.JoinVertical(
@@ -258,14 +263,14 @@ func (m *messagesCmp) View() string {
)
}
if len(m.messages) == 0 {
content := styles.BaseStyle.
content := baseStyle.
Width(m.width).
Height(m.height - 1).
Render(
m.initialScreen(),
)
return styles.BaseStyle.
return baseStyle.
Width(m.width).
Render(
lipgloss.JoinVertical(
@@ -277,7 +282,7 @@ func (m *messagesCmp) View() string {
)
}
return styles.BaseStyle.
return baseStyle.
Width(m.width).
Render(
lipgloss.JoinVertical(
@@ -328,6 +333,9 @@ func hasUnfinishedToolCalls(messages []message.Message) bool {
func (m *messagesCmp) working() string {
text := ""
if m.IsAgentWorking() && len(m.messages) > 0 {
t := theme.CurrentTheme()
baseStyle := styles.BaseStyle()
task := "Thinking..."
lastMessage := m.messages[len(m.messages)-1]
if hasToolsWithoutResponse(m.messages) {
@@ -338,42 +346,49 @@ func (m *messagesCmp) working() string {
task = "Generating..."
}
if task != "" {
text += styles.BaseStyle.Width(m.width).Foreground(styles.PrimaryColor).Bold(true).Render(
fmt.Sprintf("%s %s ", m.spinner.View(), task),
)
text += baseStyle.
Width(m.width).
Foreground(t.Primary()).
Bold(true).
Render(fmt.Sprintf("%s %s ", m.spinner.View(), task))
}
}
return text
}
func (m *messagesCmp) help() string {
t := theme.CurrentTheme()
baseStyle := styles.BaseStyle()
text := ""
if m.app.CoderAgent.IsBusy() {
text += lipgloss.JoinHorizontal(
lipgloss.Left,
styles.BaseStyle.Foreground(styles.ForgroundDim).Bold(true).Render("press "),
styles.BaseStyle.Foreground(styles.Forground).Bold(true).Render("esc"),
styles.BaseStyle.Foreground(styles.ForgroundDim).Bold(true).Render(" to exit cancel"),
baseStyle.Foreground(t.TextMuted()).Bold(true).Render("press "),
baseStyle.Foreground(t.Text()).Bold(true).Render("esc"),
baseStyle.Foreground(t.TextMuted()).Bold(true).Render(" to exit cancel"),
)
} else {
text += lipgloss.JoinHorizontal(
lipgloss.Left,
styles.BaseStyle.Foreground(styles.ForgroundDim).Bold(true).Render("press "),
styles.BaseStyle.Foreground(styles.Forground).Bold(true).Render("enter"),
styles.BaseStyle.Foreground(styles.ForgroundDim).Bold(true).Render(" to send the message,"),
styles.BaseStyle.Foreground(styles.ForgroundDim).Bold(true).Render(" write"),
styles.BaseStyle.Foreground(styles.Forground).Bold(true).Render(" \\"),
styles.BaseStyle.Foreground(styles.ForgroundDim).Bold(true).Render(" and enter to add a new line"),
baseStyle.Foreground(t.TextMuted()).Bold(true).Render("press "),
baseStyle.Foreground(t.Text()).Bold(true).Render("enter"),
baseStyle.Foreground(t.TextMuted()).Bold(true).Render(" to send the message,"),
baseStyle.Foreground(t.TextMuted()).Bold(true).Render(" write"),
baseStyle.Foreground(t.Text()).Bold(true).Render(" \\"),
baseStyle.Foreground(t.TextMuted()).Bold(true).Render(" and enter to add a new line"),
)
}
return styles.BaseStyle.
return baseStyle.
Width(m.width).
Render(text)
}
func (m *messagesCmp) initialScreen() string {
return styles.BaseStyle.Width(m.width).Render(
baseStyle := styles.BaseStyle()
return baseStyle.Width(m.width).Render(
lipgloss.JoinVertical(
lipgloss.Top,
header(m.width),
@@ -383,6 +398,13 @@ func (m *messagesCmp) initialScreen() string {
)
}
func (m *messagesCmp) rerender() {
for _, msg := range m.messages {
delete(m.cachedContent, msg.ID)
}
m.renderView()
}
func (m *messagesCmp) SetSize(width, height int) tea.Cmd {
if m.width == width && m.height == height {
return nil
@@ -391,11 +413,7 @@ func (m *messagesCmp) SetSize(width, height int) tea.Cmd {
m.height = height
m.viewport.Width = width
m.viewport.Height = height - 2
for _, msg := range m.messages {
delete(m.cachedContent, msg.ID)
}
m.uiMessages = make([]uiMessage, 0)
m.renderView()
m.rerender()
return nil
}
+96 -87
View File
@@ -6,10 +6,8 @@ import (
"fmt"
"path/filepath"
"strings"
"sync"
"time"
"github.com/charmbracelet/glamour"
"github.com/charmbracelet/lipgloss"
"github.com/charmbracelet/x/ansi"
"github.com/opencode-ai/opencode/internal/config"
@@ -19,6 +17,7 @@ import (
"github.com/opencode-ai/opencode/internal/llm/tools"
"github.com/opencode-ai/opencode/internal/message"
"github.com/opencode-ai/opencode/internal/tui/styles"
"github.com/opencode-ai/opencode/internal/tui/theme"
)
type uiMessageType int
@@ -31,8 +30,6 @@ const (
maxResultHeight = 10
)
var diffStyle = diff.NewStyleConfig(diff.WithShowHeader(false), diff.WithShowHunkHeader(false))
type uiMessage struct {
ID string
messageType uiMessageType
@@ -41,46 +38,37 @@ type uiMessage struct {
content string
}
type renderCache struct {
mutex sync.Mutex
cache map[string][]uiMessage
}
func toMarkdown(content string, focused bool, width int) string {
r, _ := glamour.NewTermRenderer(
glamour.WithStyles(styles.MarkdownTheme(false)),
glamour.WithWordWrap(width),
)
if focused {
r, _ = glamour.NewTermRenderer(
glamour.WithStyles(styles.MarkdownTheme(true)),
glamour.WithWordWrap(width),
)
}
r := styles.GetMarkdownRenderer(width)
rendered, _ := r.Render(content)
return rendered
}
func renderMessage(msg string, isUser bool, isFocused bool, width int, info ...string) string {
style := styles.BaseStyle.
t := theme.CurrentTheme()
style := styles.BaseStyle().
Width(width - 1).
BorderLeft(true).
Foreground(styles.ForgroundDim).
BorderForeground(styles.PrimaryColor).
Foreground(t.TextMuted()).
BorderForeground(t.Primary()).
BorderStyle(lipgloss.ThickBorder())
if isUser {
style = style.
BorderForeground(styles.Blue)
}
parts := []string{
styles.ForceReplaceBackgroundWithLipgloss(toMarkdown(msg, isFocused, width), styles.Background),
style = style.BorderForeground(t.Secondary())
}
// remove newline at the end
// Apply markdown formatting and handle background color
parts := []string{
styles.ForceReplaceBackgroundWithLipgloss(toMarkdown(msg, isFocused, width), t.Background()),
}
// Remove newline at the end
parts[0] = strings.TrimSuffix(parts[0], "\n")
if len(info) > 0 {
parts = append(parts, info...)
}
rendered := style.Render(
lipgloss.JoinVertical(
lipgloss.Left,
@@ -121,26 +109,37 @@ func renderAssistantMessage(
finishData := msg.FinishPart()
info := []string{}
t := theme.CurrentTheme()
baseStyle := styles.BaseStyle()
// Add finish info if available
if finished {
switch finishData.Reason {
case message.FinishReasonEndTurn:
took := formatTimeDifference(msg.CreatedAt, finishData.Time)
info = append(info, styles.BaseStyle.Width(width-1).Foreground(styles.ForgroundDim).Render(
fmt.Sprintf(" %s (%s)", models.SupportedModels[msg.Model].Name, took),
))
took := formatTimestampDiff(msg.CreatedAt, finishData.Time)
info = append(info, baseStyle.
Width(width-1).
Foreground(t.TextMuted()).
Render(fmt.Sprintf(" %s (%s)", models.SupportedModels[msg.Model].Name, took)),
)
case message.FinishReasonCanceled:
info = append(info, styles.BaseStyle.Width(width-1).Foreground(styles.ForgroundDim).Render(
fmt.Sprintf(" %s (%s)", models.SupportedModels[msg.Model].Name, "canceled"),
))
info = append(info, baseStyle.
Width(width-1).
Foreground(t.TextMuted()).
Render(fmt.Sprintf(" %s (%s)", models.SupportedModels[msg.Model].Name, "canceled")),
)
case message.FinishReasonError:
info = append(info, styles.BaseStyle.Width(width-1).Foreground(styles.ForgroundDim).Render(
fmt.Sprintf(" %s (%s)", models.SupportedModels[msg.Model].Name, "error"),
))
info = append(info, baseStyle.
Width(width-1).
Foreground(t.TextMuted()).
Render(fmt.Sprintf(" %s (%s)", models.SupportedModels[msg.Model].Name, "error")),
)
case message.FinishReasonPermissionDenied:
info = append(info, styles.BaseStyle.Width(width-1).Foreground(styles.ForgroundDim).Render(
fmt.Sprintf(" %s (%s)", models.SupportedModels[msg.Model].Name, "permission denied"),
))
info = append(info, baseStyle.
Width(width-1).
Foreground(t.TextMuted()).
Render(fmt.Sprintf(" %s (%s)", models.SupportedModels[msg.Model].Name, "permission denied")),
)
}
}
if content != "" || (finished && finishData.Reason == message.FinishReasonEndTurn) {
@@ -414,32 +413,36 @@ func truncateHeight(content string, height int) string {
}
func renderToolResponse(toolCall message.ToolCall, response message.ToolResult, width int) string {
t := theme.CurrentTheme()
baseStyle := styles.BaseStyle()
if response.IsError {
errContent := fmt.Sprintf("Error: %s", strings.ReplaceAll(response.Content, "\n", " "))
errContent = ansi.Truncate(errContent, width-1, "...")
return styles.BaseStyle.
return baseStyle.
Width(width).
Foreground(styles.Error).
Foreground(t.Error()).
Render(errContent)
}
resultContent := truncateHeight(response.Content, maxResultHeight)
switch toolCall.Name {
case agent.AgentToolName:
return styles.ForceReplaceBackgroundWithLipgloss(
toMarkdown(resultContent, false, width),
styles.Background,
t.Background(),
)
case tools.BashToolName:
resultContent = fmt.Sprintf("```bash\n%s\n```", resultContent)
return styles.ForceReplaceBackgroundWithLipgloss(
toMarkdown(resultContent, true, width),
styles.Background,
t.Background(),
)
case tools.EditToolName:
metadata := tools.EditResponseMetadata{}
json.Unmarshal([]byte(response.Metadata), &metadata)
truncDiff := truncateHeight(metadata.Diff, maxResultHeight)
formattedDiff, _ := diff.FormatDiff(truncDiff, diff.WithTotalWidth(width), diff.WithStyle(diffStyle))
formattedDiff, _ := diff.FormatDiff(truncDiff, diff.WithTotalWidth(width))
return formattedDiff
case tools.FetchToolName:
var params tools.FetchParams
@@ -454,16 +457,16 @@ func renderToolResponse(toolCall message.ToolCall, response message.ToolResult,
resultContent = fmt.Sprintf("```%s\n%s\n```", mdFormat, resultContent)
return styles.ForceReplaceBackgroundWithLipgloss(
toMarkdown(resultContent, true, width),
styles.Background,
t.Background(),
)
case tools.GlobToolName:
return styles.BaseStyle.Width(width).Foreground(styles.ForgroundMid).Render(resultContent)
return baseStyle.Width(width).Foreground(t.TextMuted()).Render(resultContent)
case tools.GrepToolName:
return styles.BaseStyle.Width(width).Foreground(styles.ForgroundMid).Render(resultContent)
return baseStyle.Width(width).Foreground(t.TextMuted()).Render(resultContent)
case tools.LSToolName:
return styles.BaseStyle.Width(width).Foreground(styles.ForgroundMid).Render(resultContent)
return baseStyle.Width(width).Foreground(t.TextMuted()).Render(resultContent)
case tools.SourcegraphToolName:
return styles.BaseStyle.Width(width).Foreground(styles.ForgroundMid).Render(resultContent)
return baseStyle.Width(width).Foreground(t.TextMuted()).Render(resultContent)
case tools.ViewToolName:
metadata := tools.ViewResponseMetadata{}
json.Unmarshal([]byte(response.Metadata), &metadata)
@@ -476,7 +479,7 @@ func renderToolResponse(toolCall message.ToolCall, response message.ToolResult,
resultContent = fmt.Sprintf("```%s\n%s\n```", ext, truncateHeight(metadata.Content, maxResultHeight))
return styles.ForceReplaceBackgroundWithLipgloss(
toMarkdown(resultContent, true, width),
styles.Background,
t.Background(),
)
case tools.WriteToolName:
params := tools.WriteParams{}
@@ -492,13 +495,13 @@ func renderToolResponse(toolCall message.ToolCall, response message.ToolResult,
resultContent = fmt.Sprintf("```%s\n%s\n```", ext, truncateHeight(params.Content, maxResultHeight))
return styles.ForceReplaceBackgroundWithLipgloss(
toMarkdown(resultContent, true, width),
styles.Background,
t.Background(),
)
default:
resultContent = fmt.Sprintf("```text\n%s\n```", resultContent)
return styles.ForceReplaceBackgroundWithLipgloss(
toMarkdown(resultContent, true, width),
styles.Background,
t.Background(),
)
}
}
@@ -515,39 +518,31 @@ func renderToolMessage(
if nested {
width = width - 3
}
style := styles.BaseStyle.
t := theme.CurrentTheme()
baseStyle := styles.BaseStyle()
style := baseStyle.
Width(width - 1).
BorderLeft(true).
BorderStyle(lipgloss.ThickBorder()).
PaddingLeft(1).
BorderForeground(styles.ForgroundDim)
BorderForeground(t.TextMuted())
response := findToolResponse(toolCall.ID, allMessages)
toolName := styles.BaseStyle.Foreground(styles.ForgroundDim).Render(fmt.Sprintf("%s: ", toolName(toolCall.Name)))
toolNameText := baseStyle.Foreground(t.TextMuted()).
Render(fmt.Sprintf("%s: ", toolName(toolCall.Name)))
if !toolCall.Finished {
// Get a brief description of what the tool is doing
toolAction := getToolAction(toolCall.Name)
// toolInput := strings.ReplaceAll(toolCall.Input, "\n", " ")
// truncatedInput := toolInput
// if len(truncatedInput) > 10 {
// truncatedInput = truncatedInput[len(truncatedInput)-10:]
// }
//
// truncatedInput = styles.BaseStyle.
// Italic(true).
// Width(width - 2 - lipgloss.Width(toolName)).
// Background(styles.BackgroundDim).
// Foreground(styles.ForgroundMid).
// Render(truncatedInput)
progressText := styles.BaseStyle.
Width(width - 2 - lipgloss.Width(toolName)).
Foreground(styles.ForgroundDim).
progressText := baseStyle.
Width(width - 2 - lipgloss.Width(toolNameText)).
Foreground(t.TextMuted()).
Render(fmt.Sprintf("%s", toolAction))
content := style.Render(lipgloss.JoinHorizontal(lipgloss.Left, toolName, progressText))
content := style.Render(lipgloss.JoinHorizontal(lipgloss.Left, toolNameText, progressText))
toolMsg := uiMessage{
messageType: toolMessageType,
position: position,
@@ -556,37 +551,39 @@ func renderToolMessage(
}
return toolMsg
}
params := renderToolParams(width-2-lipgloss.Width(toolName), toolCall)
params := renderToolParams(width-2-lipgloss.Width(toolNameText), toolCall)
responseContent := ""
if response != nil {
responseContent = renderToolResponse(toolCall, *response, width-2)
responseContent = strings.TrimSuffix(responseContent, "\n")
} else {
responseContent = styles.BaseStyle.
responseContent = baseStyle.
Italic(true).
Width(width - 2).
Foreground(styles.ForgroundDim).
Foreground(t.TextMuted()).
Render("Waiting for response...")
}
parts := []string{}
if !nested {
params := styles.BaseStyle.
Width(width - 2 - lipgloss.Width(toolName)).
Foreground(styles.ForgroundDim).
formattedParams := baseStyle.
Width(width - 2 - lipgloss.Width(toolNameText)).
Foreground(t.TextMuted()).
Render(params)
parts = append(parts, lipgloss.JoinHorizontal(lipgloss.Left, toolName, params))
parts = append(parts, lipgloss.JoinHorizontal(lipgloss.Left, toolNameText, formattedParams))
} else {
prefix := styles.BaseStyle.
Foreground(styles.ForgroundDim).
prefix := baseStyle.
Foreground(t.TextMuted()).
Render(" └ ")
params := styles.BaseStyle.
Width(width - 2 - lipgloss.Width(toolName)).
Foreground(styles.ForgroundMid).
formattedParams := baseStyle.
Width(width - 2 - lipgloss.Width(toolNameText)).
Foreground(t.TextMuted()).
Render(params)
parts = append(parts, lipgloss.JoinHorizontal(lipgloss.Left, prefix, toolName, params))
parts = append(parts, lipgloss.JoinHorizontal(lipgloss.Left, prefix, toolNameText, formattedParams))
}
if toolCall.Name == agent.AgentToolName {
taskMessages, _ := messagesService.List(context.Background(), toolCall.ID)
toolCalls := []message.ToolCall{}
@@ -622,3 +619,15 @@ func renderToolMessage(
}
return toolMsg
}
// Helper function to format the time difference between two Unix timestamps
func formatTimestampDiff(start, end int64) string {
diffSeconds := float64(end-start) / 1000.0 // Convert to seconds
if diffSeconds < 1 {
return fmt.Sprintf("%dms", int(diffSeconds*1000))
}
if diffSeconds < 60 {
return fmt.Sprintf("%.1fs", diffSeconds)
}
return fmt.Sprintf("%.1fm", diffSeconds/60)
}
+57 -21
View File
@@ -14,6 +14,7 @@ import (
"github.com/opencode-ai/opencode/internal/pubsub"
"github.com/opencode-ai/opencode/internal/session"
"github.com/opencode-ai/opencode/internal/tui/styles"
"github.com/opencode-ai/opencode/internal/tui/theme"
)
type sidebarCmp struct {
@@ -81,7 +82,9 @@ func (m *sidebarCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
}
func (m *sidebarCmp) View() string {
return styles.BaseStyle.
baseStyle := styles.BaseStyle()
return baseStyle.
Width(m.width).
PaddingLeft(4).
PaddingRight(2).
@@ -101,11 +104,19 @@ func (m *sidebarCmp) View() string {
}
func (m *sidebarCmp) sessionSection() string {
sessionKey := styles.BaseStyle.Foreground(styles.PrimaryColor).Bold(true).Render("Session")
sessionValue := styles.BaseStyle.
Foreground(styles.Forground).
t := theme.CurrentTheme()
baseStyle := styles.BaseStyle()
sessionKey := baseStyle.
Foreground(t.Primary()).
Bold(true).
Render("Session")
sessionValue := baseStyle.
Foreground(t.Text()).
Width(m.width - lipgloss.Width(sessionKey)).
Render(fmt.Sprintf(": %s", m.session.Title))
return lipgloss.JoinHorizontal(
lipgloss.Left,
sessionKey,
@@ -114,22 +125,40 @@ func (m *sidebarCmp) sessionSection() string {
}
func (m *sidebarCmp) modifiedFile(filePath string, additions, removals int) string {
t := theme.CurrentTheme()
baseStyle := styles.BaseStyle()
stats := ""
if additions > 0 && removals > 0 {
additions := styles.BaseStyle.Foreground(styles.Green).PaddingLeft(1).Render(fmt.Sprintf("+%d", additions))
removals := styles.BaseStyle.Foreground(styles.Red).PaddingLeft(1).Render(fmt.Sprintf("-%d", removals))
content := lipgloss.JoinHorizontal(lipgloss.Left, additions, removals)
stats = styles.BaseStyle.Width(lipgloss.Width(content)).Render(content)
} else if additions > 0 {
additions := fmt.Sprintf(" %s", styles.BaseStyle.PaddingLeft(1).Foreground(styles.Green).Render(fmt.Sprintf("+%d", additions)))
stats = styles.BaseStyle.Width(lipgloss.Width(additions)).Render(additions)
} else if removals > 0 {
removals := fmt.Sprintf(" %s", styles.BaseStyle.PaddingLeft(1).Foreground(styles.Red).Render(fmt.Sprintf("-%d", removals)))
stats = styles.BaseStyle.Width(lipgloss.Width(removals)).Render(removals)
}
filePathStr := styles.BaseStyle.Render(filePath)
additionsStr := baseStyle.
Foreground(t.Success()).
PaddingLeft(1).
Render(fmt.Sprintf("+%d", additions))
return styles.BaseStyle.
removalsStr := baseStyle.
Foreground(t.Error()).
PaddingLeft(1).
Render(fmt.Sprintf("-%d", removals))
content := lipgloss.JoinHorizontal(lipgloss.Left, additionsStr, removalsStr)
stats = baseStyle.Width(lipgloss.Width(content)).Render(content)
} else if additions > 0 {
additionsStr := fmt.Sprintf(" %s", baseStyle.
PaddingLeft(1).
Foreground(t.Success()).
Render(fmt.Sprintf("+%d", additions)))
stats = baseStyle.Width(lipgloss.Width(additionsStr)).Render(additionsStr)
} else if removals > 0 {
removalsStr := fmt.Sprintf(" %s", baseStyle.
PaddingLeft(1).
Foreground(t.Error()).
Render(fmt.Sprintf("-%d", removals)))
stats = baseStyle.Width(lipgloss.Width(removalsStr)).Render(removalsStr)
}
filePathStr := baseStyle.Render(filePath)
return baseStyle.
Width(m.width).
Render(
lipgloss.JoinHorizontal(
@@ -141,7 +170,14 @@ func (m *sidebarCmp) modifiedFile(filePath string, additions, removals int) stri
}
func (m *sidebarCmp) modifiedFiles() string {
modifiedFiles := styles.BaseStyle.Width(m.width).Foreground(styles.PrimaryColor).Bold(true).Render("Modified Files:")
t := theme.CurrentTheme()
baseStyle := styles.BaseStyle()
modifiedFiles := baseStyle.
Width(m.width).
Foreground(t.Primary()).
Bold(true).
Render("Modified Files:")
// If no modified files, show a placeholder message
if m.modFiles == nil || len(m.modFiles) == 0 {
@@ -150,13 +186,13 @@ func (m *sidebarCmp) modifiedFiles() string {
if remainingWidth > 0 {
message += strings.Repeat(" ", remainingWidth)
}
return styles.BaseStyle.
return baseStyle.
Width(m.width).
Render(
lipgloss.JoinVertical(
lipgloss.Top,
modifiedFiles,
styles.BaseStyle.Foreground(styles.ForgroundDim).Render(message),
baseStyle.Foreground(t.TextMuted()).Render(message),
),
)
}
@@ -175,7 +211,7 @@ func (m *sidebarCmp) modifiedFiles() string {
fileViews = append(fileViews, m.modifiedFile(path, stats.additions, stats.removals))
}
return styles.BaseStyle.
return baseStyle.
Width(m.width).
Render(
lipgloss.JoinVertical(
+64 -28
View File
@@ -15,12 +15,13 @@ import (
"github.com/opencode-ai/opencode/internal/session"
"github.com/opencode-ai/opencode/internal/tui/components/chat"
"github.com/opencode-ai/opencode/internal/tui/styles"
"github.com/opencode-ai/opencode/internal/tui/theme"
"github.com/opencode-ai/opencode/internal/tui/util"
)
type StatusCmp interface {
tea.Model
SetHelpMsg(string)
SetHelpWidgetMsg(string)
}
type statusCmp struct {
@@ -70,7 +71,21 @@ func (m statusCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return m, nil
}
var helpWidget = styles.Padded.Background(styles.ForgroundMid).Foreground(styles.BackgroundDarker).Bold(true).Render("ctrl+? help")
var helpWidget = ""
// getHelpWidget returns the help widget with current theme colors
func getHelpWidget(helpText string) string {
t := theme.CurrentTheme()
if helpText == "" {
helpText = "ctrl+? help"
}
return styles.Padded().
Background(t.TextMuted()).
Foreground(t.BackgroundDarker()).
Bold(true).
Render(helpText)
}
func formatTokensAndCost(tokens int64, cost float64) string {
// Format tokens in human-readable format (e.g., 110K, 1.2M)
@@ -99,29 +114,38 @@ func formatTokensAndCost(tokens int64, cost float64) string {
}
func (m statusCmp) View() string {
status := helpWidget
t := theme.CurrentTheme()
// Initialize the help widget
status := getHelpWidget("")
if m.session.ID != "" {
tokens := formatTokensAndCost(m.session.PromptTokens+m.session.CompletionTokens, m.session.Cost)
tokensStyle := styles.Padded.
Background(styles.Forground).
Foreground(styles.BackgroundDim).
tokensStyle := styles.Padded().
Background(t.Text()).
Foreground(t.BackgroundSecondary()).
Render(tokens)
status += tokensStyle
}
diagnostics := styles.Padded.Background(styles.BackgroundDarker).Render(m.projectDiagnostics())
diagnostics := styles.Padded().
Background(t.BackgroundDarker()).
Render(m.projectDiagnostics())
if m.info.Msg != "" {
infoStyle := styles.Padded.
Foreground(styles.Base).
infoStyle := styles.Padded().
Foreground(t.Background()).
Width(m.availableFooterMsgWidth(diagnostics))
switch m.info.Type {
case util.InfoTypeInfo:
infoStyle = infoStyle.Background(styles.BorderColor)
infoStyle = infoStyle.Background(t.Info())
case util.InfoTypeWarn:
infoStyle = infoStyle.Background(styles.Peach)
infoStyle = infoStyle.Background(t.Warning())
case util.InfoTypeError:
infoStyle = infoStyle.Background(styles.Red)
infoStyle = infoStyle.Background(t.Error())
}
// Truncate message if it's longer than available width
msg := m.info.Msg
availWidth := m.availableFooterMsgWidth(diagnostics) - 10
@@ -130,9 +154,9 @@ func (m statusCmp) View() string {
}
status += infoStyle.Render(msg)
} else {
status += styles.Padded.
Foreground(styles.Base).
Background(styles.BackgroundDim).
status += styles.Padded().
Foreground(t.Text()).
Background(t.BackgroundSecondary()).
Width(m.availableFooterMsgWidth(diagnostics)).
Render("")
}
@@ -143,6 +167,8 @@ func (m statusCmp) View() string {
}
func (m *statusCmp) projectDiagnostics() string {
t := theme.CurrentTheme()
// Check if any LSP server is still initializing
initializing := false
for _, client := range m.lspClients {
@@ -155,8 +181,8 @@ func (m *statusCmp) projectDiagnostics() string {
// If any server is initializing, show that status
if initializing {
return lipgloss.NewStyle().
Background(styles.BackgroundDarker).
Foreground(styles.Peach).
Background(t.BackgroundDarker()).
Foreground(t.Warning()).
Render(fmt.Sprintf("%s Initializing LSP...", styles.SpinnerIcon))
}
@@ -189,29 +215,29 @@ func (m *statusCmp) projectDiagnostics() string {
if len(errorDiagnostics) > 0 {
errStr := lipgloss.NewStyle().
Background(styles.BackgroundDarker).
Foreground(styles.Error).
Background(t.BackgroundDarker()).
Foreground(t.Error()).
Render(fmt.Sprintf("%s %d", styles.ErrorIcon, len(errorDiagnostics)))
diagnostics = append(diagnostics, errStr)
}
if len(warnDiagnostics) > 0 {
warnStr := lipgloss.NewStyle().
Background(styles.BackgroundDarker).
Foreground(styles.Warning).
Background(t.BackgroundDarker()).
Foreground(t.Warning()).
Render(fmt.Sprintf("%s %d", styles.WarningIcon, len(warnDiagnostics)))
diagnostics = append(diagnostics, warnStr)
}
if len(hintDiagnostics) > 0 {
hintStr := lipgloss.NewStyle().
Background(styles.BackgroundDarker).
Foreground(styles.Text).
Background(t.BackgroundDarker()).
Foreground(t.Text()).
Render(fmt.Sprintf("%s %d", styles.HintIcon, len(hintDiagnostics)))
diagnostics = append(diagnostics, hintStr)
}
if len(infoDiagnostics) > 0 {
infoStr := lipgloss.NewStyle().
Background(styles.BackgroundDarker).
Foreground(styles.Peach).
Background(t.BackgroundDarker()).
Foreground(t.Info()).
Render(fmt.Sprintf("%s %d", styles.InfoIcon, len(infoDiagnostics)))
diagnostics = append(diagnostics, infoStr)
}
@@ -230,6 +256,8 @@ func (m statusCmp) availableFooterMsgWidth(diagnostics string) int {
}
func (m statusCmp) model() string {
t := theme.CurrentTheme()
cfg := config.Get()
coder, ok := cfg.Agents[config.AgentCoder]
@@ -237,14 +265,22 @@ func (m statusCmp) model() string {
return "Unknown"
}
model := models.SupportedModels[coder.Model]
return styles.Padded.Background(styles.Grey).Foreground(styles.Text).Render(model.Name)
return styles.Padded().
Background(t.Secondary()).
Foreground(t.Background()).
Render(model.Name)
}
func (m statusCmp) SetHelpMsg(s string) {
helpWidget = styles.Padded.Background(styles.Forground).Foreground(styles.BackgroundDarker).Bold(true).Render(s)
func (m statusCmp) SetHelpWidgetMsg(s string) {
// Update the help widget text using the getHelpWidget function
helpWidget = getHelpWidget(s)
}
func NewStatusCmp(lspClients map[string]*lsp.Client) StatusCmp {
// Initialize the help widget with default text
helpWidget = getHelpWidget("")
return &statusCmp{
messageTTL: 10 * time.Second,
lspClients: lspClients,
+21 -17
View File
@@ -6,6 +6,7 @@ import (
"github.com/charmbracelet/lipgloss"
"github.com/opencode-ai/opencode/internal/tui/layout"
"github.com/opencode-ai/opencode/internal/tui/styles"
"github.com/opencode-ai/opencode/internal/tui/theme"
"github.com/opencode-ai/opencode/internal/tui/util"
)
@@ -112,11 +113,14 @@ func (c *commandDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
}
func (c *commandDialogCmp) View() string {
t := theme.CurrentTheme()
baseStyle := styles.BaseStyle()
if len(c.commands) == 0 {
return styles.BaseStyle.Padding(1, 2).
return baseStyle.Padding(1, 2).
Border(lipgloss.RoundedBorder()).
BorderBackground(styles.Background).
BorderForeground(styles.ForgroundDim).
BorderBackground(t.Background()).
BorderForeground(t.TextMuted()).
Width(40).
Render("No commands available")
}
@@ -154,17 +158,17 @@ func (c *commandDialogCmp) View() string {
for i := startIdx; i < endIdx; i++ {
cmd := c.commands[i]
itemStyle := styles.BaseStyle.Width(maxWidth)
descStyle := styles.BaseStyle.Width(maxWidth).Foreground(styles.ForgroundDim)
itemStyle := baseStyle.Width(maxWidth)
descStyle := baseStyle.Width(maxWidth).Foreground(t.TextMuted())
if i == c.selectedIdx {
itemStyle = itemStyle.
Background(styles.PrimaryColor).
Foreground(styles.Background).
Background(t.Primary()).
Foreground(t.Background()).
Bold(true)
descStyle = descStyle.
Background(styles.PrimaryColor).
Foreground(styles.Background)
Background(t.Primary()).
Foreground(t.Background())
}
title := itemStyle.Padding(0, 1).Render(cmd.Title)
@@ -177,8 +181,8 @@ func (c *commandDialogCmp) View() string {
}
}
title := styles.BaseStyle.
Foreground(styles.PrimaryColor).
title := baseStyle.
Foreground(t.Primary()).
Bold(true).
Width(maxWidth).
Padding(0, 1).
@@ -187,15 +191,15 @@ func (c *commandDialogCmp) View() string {
content := lipgloss.JoinVertical(
lipgloss.Left,
title,
styles.BaseStyle.Width(maxWidth).Render(""),
styles.BaseStyle.Width(maxWidth).Render(lipgloss.JoinVertical(lipgloss.Left, commandItems...)),
styles.BaseStyle.Width(maxWidth).Render(""),
baseStyle.Width(maxWidth).Render(""),
baseStyle.Width(maxWidth).Render(lipgloss.JoinVertical(lipgloss.Left, commandItems...)),
baseStyle.Width(maxWidth).Render(""),
)
return styles.BaseStyle.Padding(1, 2).
return baseStyle.Padding(1, 2).
Border(lipgloss.RoundedBorder()).
BorderBackground(styles.Background).
BorderForeground(styles.ForgroundDim).
BorderBackground(t.Background()).
BorderForeground(t.TextMuted()).
Width(lipgloss.Width(content) + 4).
Render(content)
}
+33 -15
View File
@@ -7,6 +7,7 @@ import (
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"github.com/opencode-ai/opencode/internal/tui/styles"
"github.com/opencode-ai/opencode/internal/tui/theme"
)
type helpCmp struct {
@@ -53,10 +54,21 @@ func removeDuplicateBindings(bindings []key.Binding) []key.Binding {
}
func (h *helpCmp) render() string {
helpKeyStyle := styles.Bold.Background(styles.Background).Foreground(styles.Forground).Padding(0, 1, 0, 0)
helpDescStyle := styles.Regular.Background(styles.Background).Foreground(styles.ForgroundMid)
t := theme.CurrentTheme()
baseStyle := styles.BaseStyle()
helpKeyStyle := styles.Bold().
Background(t.Background()).
Foreground(t.Text()).
Padding(0, 1, 0, 0)
helpDescStyle := styles.Regular().
Background(t.Background()).
Foreground(t.TextMuted())
// Compile list of bindings to render
bindings := removeDuplicateBindings(h.keys)
// Enumerate through each group of bindings, populating a series of
// pairs of columns, one for keys, one for descriptions
var (
@@ -64,6 +76,7 @@ func (h *helpCmp) render() string {
width int
rows = 10 - 2
)
for i := 0; i < len(bindings); i += rows {
var (
keys []string
@@ -73,11 +86,12 @@ func (h *helpCmp) render() string {
keys = append(keys, helpKeyStyle.Render(bindings[j].Help().Key))
descs = append(descs, helpDescStyle.Render(bindings[j].Help().Desc))
}
// Render pair of columns; beyond the first pair, render a three space
// left margin, in order to visually separate the pairs.
var cols []string
if len(pairs) > 0 {
cols = []string{styles.BaseStyle.Render(" ")}
cols = []string{baseStyle.Render(" ")}
}
maxDescWidth := 0
@@ -89,7 +103,7 @@ func (h *helpCmp) render() string {
for i := range descs {
remainingWidth := maxDescWidth - lipgloss.Width(descs[i])
if remainingWidth > 0 {
descs[i] = descs[i] + styles.BaseStyle.Render(strings.Repeat(" ", remainingWidth))
descs[i] = descs[i] + baseStyle.Render(strings.Repeat(" ", remainingWidth))
}
}
maxKeyWidth := 0
@@ -101,7 +115,7 @@ func (h *helpCmp) render() string {
for i := range keys {
remainingWidth := maxKeyWidth - lipgloss.Width(keys[i])
if remainingWidth > 0 {
keys[i] = keys[i] + styles.BaseStyle.Render(strings.Repeat(" ", remainingWidth))
keys[i] = keys[i] + baseStyle.Render(strings.Repeat(" ", remainingWidth))
}
}
@@ -110,7 +124,7 @@ func (h *helpCmp) render() string {
strings.Join(descs, "\n"),
)
pair := styles.BaseStyle.Render(lipgloss.JoinHorizontal(lipgloss.Top, cols...))
pair := baseStyle.Render(lipgloss.JoinHorizontal(lipgloss.Top, cols...))
// check whether it exceeds the maximum width avail (the width of the
// terminal, subtracting 2 for the borders).
width += lipgloss.Width(pair)
@@ -130,9 +144,9 @@ func (h *helpCmp) render() string {
lipgloss.Left, // x
lipgloss.Top, // y
lastPair, // content
lipgloss.WithWhitespaceBackground(styles.Background), // background
lipgloss.WithWhitespaceBackground(t.Background()),
))
content := styles.BaseStyle.Width(h.width).Render(
content := baseStyle.Width(h.width).Render(
lipgloss.JoinHorizontal(
lipgloss.Top,
prefix...,
@@ -140,8 +154,9 @@ func (h *helpCmp) render() string {
)
return content
}
// Join pairs of columns and enclose in a border
content := styles.BaseStyle.Width(h.width).Render(
content := baseStyle.Width(h.width).Render(
lipgloss.JoinHorizontal(
lipgloss.Top,
pairs...,
@@ -151,22 +166,25 @@ func (h *helpCmp) render() string {
}
func (h *helpCmp) View() string {
t := theme.CurrentTheme()
baseStyle := styles.BaseStyle()
content := h.render()
header := styles.BaseStyle.
header := baseStyle.
Bold(true).
Width(lipgloss.Width(content)).
Foreground(styles.PrimaryColor).
Foreground(t.Primary()).
Render("Keyboard Shortcuts")
return styles.BaseStyle.Padding(1).
return baseStyle.Padding(1).
Border(lipgloss.RoundedBorder()).
BorderForeground(styles.ForgroundDim).
BorderForeground(t.TextMuted()).
Width(h.width).
BorderBackground(styles.Background).
BorderBackground(t.Background()).
Render(
lipgloss.JoinVertical(lipgloss.Center,
header,
styles.BaseStyle.Render(strings.Repeat(" ", lipgloss.Width(header))),
baseStyle.Render(strings.Repeat(" ", lipgloss.Width(header))),
content,
),
)
+27 -23
View File
@@ -6,6 +6,7 @@ import (
"github.com/charmbracelet/lipgloss"
"github.com/opencode-ai/opencode/internal/tui/styles"
"github.com/opencode-ai/opencode/internal/tui/theme"
"github.com/opencode-ai/opencode/internal/tui/util"
)
@@ -92,55 +93,58 @@ func (m InitDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
// View implements tea.Model.
func (m InitDialogCmp) View() string {
t := theme.CurrentTheme()
baseStyle := styles.BaseStyle()
// Calculate width needed for content
maxWidth := 60 // Width for explanation text
title := styles.BaseStyle.
Foreground(styles.PrimaryColor).
title := baseStyle.
Foreground(t.Primary()).
Bold(true).
Width(maxWidth).
Padding(0, 1).
Render("Initialize Project")
explanation := styles.BaseStyle.
Foreground(styles.Forground).
explanation := baseStyle.
Foreground(t.Text()).
Width(maxWidth).
Padding(0, 1).
Render("Initialization generates a new OpenCode.md file that contains information about your codebase, this file serves as memory for each project, you can freely add to it to help the agents be better at their job.")
question := styles.BaseStyle.
Foreground(styles.Forground).
question := baseStyle.
Foreground(t.Text()).
Width(maxWidth).
Padding(1, 1).
Render("Would you like to initialize this project?")
maxWidth = min(maxWidth, m.width-10)
yesStyle := styles.BaseStyle
noStyle := styles.BaseStyle
yesStyle := baseStyle
noStyle := baseStyle
if m.selected == 0 {
yesStyle = yesStyle.
Background(styles.PrimaryColor).
Foreground(styles.Background).
Background(t.Primary()).
Foreground(t.Background()).
Bold(true)
noStyle = noStyle.
Background(styles.Background).
Foreground(styles.PrimaryColor)
Background(t.Background()).
Foreground(t.Primary())
} else {
noStyle = noStyle.
Background(styles.PrimaryColor).
Foreground(styles.Background).
Background(t.Primary()).
Foreground(t.Background()).
Bold(true)
yesStyle = yesStyle.
Background(styles.Background).
Foreground(styles.PrimaryColor)
Background(t.Background()).
Foreground(t.Primary())
}
yes := yesStyle.Padding(0, 3).Render("Yes")
no := noStyle.Padding(0, 3).Render("No")
buttons := lipgloss.JoinHorizontal(lipgloss.Center, yes, styles.BaseStyle.Render(" "), no)
buttons = styles.BaseStyle.
buttons := lipgloss.JoinHorizontal(lipgloss.Center, yes, baseStyle.Render(" "), no)
buttons = baseStyle.
Width(maxWidth).
Padding(1, 0).
Render(buttons)
@@ -148,17 +152,17 @@ func (m InitDialogCmp) View() string {
content := lipgloss.JoinVertical(
lipgloss.Left,
title,
styles.BaseStyle.Width(maxWidth).Render(""),
baseStyle.Width(maxWidth).Render(""),
explanation,
question,
buttons,
styles.BaseStyle.Width(maxWidth).Render(""),
baseStyle.Width(maxWidth).Render(""),
)
return styles.BaseStyle.Padding(1, 2).
return baseStyle.Padding(1, 2).
Border(lipgloss.RoundedBorder()).
BorderBackground(styles.Background).
BorderForeground(styles.ForgroundDim).
BorderBackground(t.Background()).
BorderForeground(t.TextMuted()).
Width(lipgloss.Width(content) + 4).
Render(content)
}
+370
View File
@@ -0,0 +1,370 @@
package dialog
import (
"fmt"
"slices"
"strings"
"github.com/charmbracelet/bubbles/key"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"github.com/opencode-ai/opencode/internal/config"
"github.com/opencode-ai/opencode/internal/llm/models"
"github.com/opencode-ai/opencode/internal/tui/layout"
"github.com/opencode-ai/opencode/internal/tui/styles"
"github.com/opencode-ai/opencode/internal/tui/theme"
"github.com/opencode-ai/opencode/internal/tui/util"
)
const (
numVisibleModels = 10
maxDialogWidth = 40
)
// ModelSelectedMsg is sent when a model is selected
type ModelSelectedMsg struct {
Model models.Model
}
// CloseModelDialogMsg is sent when a model is selected
type CloseModelDialogMsg struct{}
// ModelDialog interface for the model selection dialog
type ModelDialog interface {
tea.Model
layout.Bindings
}
type modelDialogCmp struct {
models []models.Model
provider models.ModelProvider
availableProviders []models.ModelProvider
selectedIdx int
width int
height int
scrollOffset int
hScrollOffset int
hScrollPossible bool
}
type modelKeyMap struct {
Up key.Binding
Down key.Binding
Left key.Binding
Right key.Binding
Enter key.Binding
Escape key.Binding
J key.Binding
K key.Binding
H key.Binding
L key.Binding
}
var modelKeys = modelKeyMap{
Up: key.NewBinding(
key.WithKeys("up"),
key.WithHelp("↑", "previous model"),
),
Down: key.NewBinding(
key.WithKeys("down"),
key.WithHelp("↓", "next model"),
),
Left: key.NewBinding(
key.WithKeys("left"),
key.WithHelp("←", "scroll left"),
),
Right: key.NewBinding(
key.WithKeys("right"),
key.WithHelp("→", "scroll right"),
),
Enter: key.NewBinding(
key.WithKeys("enter"),
key.WithHelp("enter", "select model"),
),
Escape: key.NewBinding(
key.WithKeys("esc"),
key.WithHelp("esc", "close"),
),
J: key.NewBinding(
key.WithKeys("j"),
key.WithHelp("j", "next model"),
),
K: key.NewBinding(
key.WithKeys("k"),
key.WithHelp("k", "previous model"),
),
H: key.NewBinding(
key.WithKeys("h"),
key.WithHelp("h", "scroll left"),
),
L: key.NewBinding(
key.WithKeys("l"),
key.WithHelp("l", "scroll right"),
),
}
func (m *modelDialogCmp) Init() tea.Cmd {
m.setupModels()
return nil
}
func (m *modelDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyMsg:
switch {
case key.Matches(msg, modelKeys.Up) || key.Matches(msg, modelKeys.K):
m.moveSelectionUp()
case key.Matches(msg, modelKeys.Down) || key.Matches(msg, modelKeys.J):
m.moveSelectionDown()
case key.Matches(msg, modelKeys.Left) || key.Matches(msg, modelKeys.H):
if m.hScrollPossible {
m.switchProvider(-1)
}
case key.Matches(msg, modelKeys.Right) || key.Matches(msg, modelKeys.L):
if m.hScrollPossible {
m.switchProvider(1)
}
case key.Matches(msg, modelKeys.Enter):
util.ReportInfo(fmt.Sprintf("selected model: %s", m.models[m.selectedIdx].Name))
return m, util.CmdHandler(ModelSelectedMsg{Model: m.models[m.selectedIdx]})
case key.Matches(msg, modelKeys.Escape):
return m, util.CmdHandler(CloseModelDialogMsg{})
}
case tea.WindowSizeMsg:
m.width = msg.Width
m.height = msg.Height
}
return m, nil
}
// moveSelectionUp moves the selection up or wraps to bottom
func (m *modelDialogCmp) moveSelectionUp() {
if m.selectedIdx > 0 {
m.selectedIdx--
} else {
m.selectedIdx = len(m.models) - 1
m.scrollOffset = max(0, len(m.models)-numVisibleModels)
}
// Keep selection visible
if m.selectedIdx < m.scrollOffset {
m.scrollOffset = m.selectedIdx
}
}
// moveSelectionDown moves the selection down or wraps to top
func (m *modelDialogCmp) moveSelectionDown() {
if m.selectedIdx < len(m.models)-1 {
m.selectedIdx++
} else {
m.selectedIdx = 0
m.scrollOffset = 0
}
// Keep selection visible
if m.selectedIdx >= m.scrollOffset+numVisibleModels {
m.scrollOffset = m.selectedIdx - (numVisibleModels - 1)
}
}
func (m *modelDialogCmp) switchProvider(offset int) {
newOffset := m.hScrollOffset + offset
// Ensure we stay within bounds
if newOffset < 0 {
newOffset = len(m.availableProviders) - 1
}
if newOffset >= len(m.availableProviders) {
newOffset = 0
}
m.hScrollOffset = newOffset
m.provider = m.availableProviders[m.hScrollOffset]
m.setupModelsForProvider(m.provider)
}
func (m *modelDialogCmp) View() string {
t := theme.CurrentTheme()
baseStyle := styles.BaseStyle()
// Capitalize first letter of provider name
providerName := strings.ToUpper(string(m.provider)[:1]) + string(m.provider[1:])
title := baseStyle.
Foreground(t.Primary()).
Bold(true).
Width(maxDialogWidth).
Padding(0, 0, 1).
Render(fmt.Sprintf("Select %s Model", providerName))
// Render visible models
endIdx := min(m.scrollOffset+numVisibleModels, len(m.models))
modelItems := make([]string, 0, endIdx-m.scrollOffset)
for i := m.scrollOffset; i < endIdx; i++ {
itemStyle := baseStyle.Width(maxDialogWidth)
if i == m.selectedIdx {
itemStyle = itemStyle.Background(t.Primary()).
Foreground(t.Background()).Bold(true)
}
modelItems = append(modelItems, itemStyle.Render(m.models[i].Name))
}
scrollIndicator := m.getScrollIndicators(maxDialogWidth)
content := lipgloss.JoinVertical(
lipgloss.Left,
title,
baseStyle.Width(maxDialogWidth).Render(lipgloss.JoinVertical(lipgloss.Left, modelItems...)),
scrollIndicator,
)
return baseStyle.Padding(1, 2).
Border(lipgloss.RoundedBorder()).
BorderBackground(t.Background()).
BorderForeground(t.TextMuted()).
Width(lipgloss.Width(content) + 4).
Render(content)
}
func (m *modelDialogCmp) getScrollIndicators(maxWidth int) string {
var indicator string
if len(m.models) > numVisibleModels {
if m.scrollOffset > 0 {
indicator += "↑ "
}
if m.scrollOffset+numVisibleModels < len(m.models) {
indicator += "↓ "
}
}
if m.hScrollPossible {
if m.hScrollOffset > 0 {
indicator = "← " + indicator
}
if m.hScrollOffset < len(m.availableProviders)-1 {
indicator += "→"
}
}
if indicator == "" {
return ""
}
t := theme.CurrentTheme()
baseStyle := styles.BaseStyle()
return baseStyle.
Foreground(t.Primary()).
Width(maxWidth).
Align(lipgloss.Right).
Bold(true).
Render(indicator)
}
func (m *modelDialogCmp) BindingKeys() []key.Binding {
return layout.KeyMapToSlice(modelKeys)
}
func (m *modelDialogCmp) setupModels() {
cfg := config.Get()
m.availableProviders = getEnabledProviders(cfg)
m.hScrollPossible = len(m.availableProviders) > 1
agentCfg := cfg.Agents[config.AgentCoder]
selectedModelId := agentCfg.Model
modelInfo := models.SupportedModels[selectedModelId]
m.provider = modelInfo.Provider
m.hScrollOffset = findProviderIndex(m.availableProviders, m.provider)
m.setupModelsForProvider(m.provider)
}
func getEnabledProviders(cfg *config.Config) []models.ModelProvider {
var providers []models.ModelProvider
for providerId, provider := range cfg.Providers {
if !provider.Disabled {
providers = append(providers, providerId)
}
}
// Sort by provider popularity
slices.SortFunc(providers, func(a, b models.ModelProvider) int {
rA := models.ProviderPopularity[a]
rB := models.ProviderPopularity[b]
// models not included in popularity ranking default to last
if rA == 0 {
rA = 999
}
if rB == 0 {
rB = 999
}
return rA - rB
})
return providers
}
// findProviderIndex returns the index of the provider in the list, or -1 if not found
func findProviderIndex(providers []models.ModelProvider, provider models.ModelProvider) int {
for i, p := range providers {
if p == provider {
return i
}
}
return -1
}
func (m *modelDialogCmp) setupModelsForProvider(provider models.ModelProvider) {
cfg := config.Get()
agentCfg := cfg.Agents[config.AgentCoder]
selectedModelId := agentCfg.Model
m.provider = provider
m.models = getModelsForProvider(provider)
m.selectedIdx = 0
m.scrollOffset = 0
// Try to select the current model if it belongs to this provider
if provider == models.SupportedModels[selectedModelId].Provider {
for i, model := range m.models {
if model.ID == selectedModelId {
m.selectedIdx = i
// Adjust scroll position to keep selected model visible
if m.selectedIdx >= numVisibleModels {
m.scrollOffset = m.selectedIdx - (numVisibleModels - 1)
}
break
}
}
}
}
func getModelsForProvider(provider models.ModelProvider) []models.Model {
var providerModels []models.Model
for _, model := range models.SupportedModels {
if model.Provider == provider {
providerModels = append(providerModels, model)
}
}
// reverse alphabetical order (if llm naming was consistent latest would appear first)
slices.SortFunc(providerModels, func(a, b models.Model) int {
if a.Name > b.Name {
return -1
} else if a.Name < b.Name {
return 1
}
return 0
})
return providerModels
}
func NewModelDialogCmp() ModelDialog {
return &modelDialogCmp{}
}
+69 -53
View File
@@ -7,13 +7,13 @@ import (
"github.com/charmbracelet/bubbles/key"
"github.com/charmbracelet/bubbles/viewport"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/glamour"
"github.com/charmbracelet/lipgloss"
"github.com/opencode-ai/opencode/internal/diff"
"github.com/opencode-ai/opencode/internal/llm/tools"
"github.com/opencode-ai/opencode/internal/permission"
"github.com/opencode-ai/opencode/internal/tui/layout"
"github.com/opencode-ai/opencode/internal/tui/styles"
"github.com/opencode-ai/opencode/internal/tui/theme"
"github.com/opencode-ai/opencode/internal/tui/util"
)
@@ -149,25 +149,28 @@ func (p *permissionDialogCmp) selectCurrentOption() tea.Cmd {
}
func (p *permissionDialogCmp) renderButtons() string {
allowStyle := styles.BaseStyle
allowSessionStyle := styles.BaseStyle
denyStyle := styles.BaseStyle
spacerStyle := styles.BaseStyle.Background(styles.Background)
t := theme.CurrentTheme()
baseStyle := styles.BaseStyle()
allowStyle := baseStyle
allowSessionStyle := baseStyle
denyStyle := baseStyle
spacerStyle := baseStyle.Background(t.Background())
// Style the selected button
switch p.selectedOption {
case 0:
allowStyle = allowStyle.Background(styles.PrimaryColor).Foreground(styles.Background)
allowSessionStyle = allowSessionStyle.Background(styles.Background).Foreground(styles.PrimaryColor)
denyStyle = denyStyle.Background(styles.Background).Foreground(styles.PrimaryColor)
allowStyle = allowStyle.Background(t.Primary()).Foreground(t.Background())
allowSessionStyle = allowSessionStyle.Background(t.Background()).Foreground(t.Primary())
denyStyle = denyStyle.Background(t.Background()).Foreground(t.Primary())
case 1:
allowStyle = allowStyle.Background(styles.Background).Foreground(styles.PrimaryColor)
allowSessionStyle = allowSessionStyle.Background(styles.PrimaryColor).Foreground(styles.Background)
denyStyle = denyStyle.Background(styles.Background).Foreground(styles.PrimaryColor)
allowStyle = allowStyle.Background(t.Background()).Foreground(t.Primary())
allowSessionStyle = allowSessionStyle.Background(t.Primary()).Foreground(t.Background())
denyStyle = denyStyle.Background(t.Background()).Foreground(t.Primary())
case 2:
allowStyle = allowStyle.Background(styles.Background).Foreground(styles.PrimaryColor)
allowSessionStyle = allowSessionStyle.Background(styles.Background).Foreground(styles.PrimaryColor)
denyStyle = denyStyle.Background(styles.PrimaryColor).Foreground(styles.Background)
allowStyle = allowStyle.Background(t.Background()).Foreground(t.Primary())
allowSessionStyle = allowSessionStyle.Background(t.Background()).Foreground(t.Primary())
denyStyle = denyStyle.Background(t.Primary()).Foreground(t.Background())
}
allowButton := allowStyle.Padding(0, 1).Render("Allow (a)")
@@ -192,15 +195,18 @@ func (p *permissionDialogCmp) renderButtons() string {
}
func (p *permissionDialogCmp) renderHeader() string {
toolKey := styles.BaseStyle.Foreground(styles.ForgroundDim).Bold(true).Render("Tool")
toolValue := styles.BaseStyle.
Foreground(styles.Forground).
t := theme.CurrentTheme()
baseStyle := styles.BaseStyle()
toolKey := baseStyle.Foreground(t.TextMuted()).Bold(true).Render("Tool")
toolValue := baseStyle.
Foreground(t.Text()).
Width(p.width - lipgloss.Width(toolKey)).
Render(fmt.Sprintf(": %s", p.permission.ToolName))
pathKey := styles.BaseStyle.Foreground(styles.ForgroundDim).Bold(true).Render("Path")
pathValue := styles.BaseStyle.
Foreground(styles.Forground).
pathKey := baseStyle.Foreground(t.TextMuted()).Bold(true).Render("Path")
pathValue := baseStyle.
Foreground(t.Text()).
Width(p.width - lipgloss.Width(pathKey)).
Render(fmt.Sprintf(": %s", p.permission.Path))
@@ -210,45 +216,45 @@ func (p *permissionDialogCmp) renderHeader() string {
toolKey,
toolValue,
),
styles.BaseStyle.Render(strings.Repeat(" ", p.width)),
baseStyle.Render(strings.Repeat(" ", p.width)),
lipgloss.JoinHorizontal(
lipgloss.Left,
pathKey,
pathValue,
),
styles.BaseStyle.Render(strings.Repeat(" ", p.width)),
baseStyle.Render(strings.Repeat(" ", p.width)),
}
// Add tool-specific header information
switch p.permission.ToolName {
case tools.BashToolName:
headerParts = append(headerParts, styles.BaseStyle.Foreground(styles.ForgroundDim).Width(p.width).Bold(true).Render("Command"))
headerParts = append(headerParts, baseStyle.Foreground(t.TextMuted()).Width(p.width).Bold(true).Render("Command"))
case tools.EditToolName:
headerParts = append(headerParts, styles.BaseStyle.Foreground(styles.ForgroundDim).Width(p.width).Bold(true).Render("Diff"))
headerParts = append(headerParts, baseStyle.Foreground(t.TextMuted()).Width(p.width).Bold(true).Render("Diff"))
case tools.WriteToolName:
headerParts = append(headerParts, styles.BaseStyle.Foreground(styles.ForgroundDim).Width(p.width).Bold(true).Render("Diff"))
headerParts = append(headerParts, baseStyle.Foreground(t.TextMuted()).Width(p.width).Bold(true).Render("Diff"))
case tools.FetchToolName:
headerParts = append(headerParts, styles.BaseStyle.Foreground(styles.ForgroundDim).Width(p.width).Bold(true).Render("URL"))
headerParts = append(headerParts, baseStyle.Foreground(t.TextMuted()).Width(p.width).Bold(true).Render("URL"))
}
return lipgloss.NewStyle().Render(lipgloss.JoinVertical(lipgloss.Left, headerParts...))
return lipgloss.NewStyle().Background(t.Background()).Render(lipgloss.JoinVertical(lipgloss.Left, headerParts...))
}
func (p *permissionDialogCmp) renderBashContent() string {
t := theme.CurrentTheme()
baseStyle := styles.BaseStyle()
if pr, ok := p.permission.Params.(tools.BashPermissionsParams); ok {
content := fmt.Sprintf("```bash\n%s\n```", pr.Command)
// Use the cache for markdown rendering
renderedContent := p.GetOrSetMarkdown(p.permission.ID, func() (string, error) {
r, _ := glamour.NewTermRenderer(
glamour.WithStyles(styles.MarkdownTheme(true)),
glamour.WithWordWrap(p.width-10),
)
r := styles.GetMarkdownRenderer(p.width-10)
s, err := r.Render(content)
return styles.ForceReplaceBackgroundWithLipgloss(s, styles.Background), err
return styles.ForceReplaceBackgroundWithLipgloss(s, t.Background()), err
})
finalContent := styles.BaseStyle.
finalContent := baseStyle.
Width(p.contentViewPort.Width).
Render(renderedContent)
p.contentViewPort.SetContent(finalContent)
@@ -295,39 +301,45 @@ func (p *permissionDialogCmp) renderWriteContent() string {
}
func (p *permissionDialogCmp) renderFetchContent() string {
t := theme.CurrentTheme()
baseStyle := styles.BaseStyle()
if pr, ok := p.permission.Params.(tools.FetchPermissionsParams); ok {
content := fmt.Sprintf("```bash\n%s\n```", pr.URL)
// Use the cache for markdown rendering
renderedContent := p.GetOrSetMarkdown(p.permission.ID, func() (string, error) {
r, _ := glamour.NewTermRenderer(
glamour.WithStyles(styles.MarkdownTheme(true)),
glamour.WithWordWrap(p.width-10),
)
r := styles.GetMarkdownRenderer(p.width-10)
s, err := r.Render(content)
return styles.ForceReplaceBackgroundWithLipgloss(s, styles.Background), err
return styles.ForceReplaceBackgroundWithLipgloss(s, t.Background()), err
})
p.contentViewPort.SetContent(renderedContent)
finalContent := baseStyle.
Width(p.contentViewPort.Width).
Render(renderedContent)
p.contentViewPort.SetContent(finalContent)
return p.styleViewport()
}
return ""
}
func (p *permissionDialogCmp) renderDefaultContent() string {
t := theme.CurrentTheme()
baseStyle := styles.BaseStyle()
content := p.permission.Description
// Use the cache for markdown rendering
renderedContent := p.GetOrSetMarkdown(p.permission.ID, func() (string, error) {
r, _ := glamour.NewTermRenderer(
glamour.WithStyles(styles.CatppuccinMarkdownStyle()),
glamour.WithWordWrap(p.width-10),
)
r := styles.GetMarkdownRenderer(p.width-10)
s, err := r.Render(content)
return styles.ForceReplaceBackgroundWithLipgloss(s, styles.Background), err
return styles.ForceReplaceBackgroundWithLipgloss(s, t.Background()), err
})
p.contentViewPort.SetContent(renderedContent)
finalContent := baseStyle.
Width(p.contentViewPort.Width).
Render(renderedContent)
p.contentViewPort.SetContent(finalContent)
if renderedContent == "" {
return ""
@@ -337,17 +349,21 @@ func (p *permissionDialogCmp) renderDefaultContent() string {
}
func (p *permissionDialogCmp) styleViewport() string {
t := theme.CurrentTheme()
contentStyle := lipgloss.NewStyle().
Background(styles.Background)
Background(t.Background())
return contentStyle.Render(p.contentViewPort.View())
}
func (p *permissionDialogCmp) render() string {
title := styles.BaseStyle.
t := theme.CurrentTheme()
baseStyle := styles.BaseStyle()
title := baseStyle.
Bold(true).
Width(p.width - 4).
Foreground(styles.PrimaryColor).
Foreground(t.Primary()).
Render("Permission Required")
// Render header
headerContent := p.renderHeader()
@@ -378,18 +394,18 @@ func (p *permissionDialogCmp) render() string {
content := lipgloss.JoinVertical(
lipgloss.Top,
title,
styles.BaseStyle.Render(strings.Repeat(" ", lipgloss.Width(title))),
baseStyle.Render(strings.Repeat(" ", lipgloss.Width(title))),
headerContent,
contentFinal,
buttons,
styles.BaseStyle.Render(strings.Repeat(" ", p.width-4)),
baseStyle.Render(strings.Repeat(" ", p.width-4)),
)
return styles.BaseStyle.
return baseStyle.
Padding(1, 0, 0, 1).
Border(lipgloss.RoundedBorder()).
BorderBackground(styles.Background).
BorderForeground(styles.ForgroundDim).
BorderBackground(t.Background()).
BorderForeground(t.TextMuted()).
Width(p.width).
Height(p.height).
Render(
+15 -11
View File
@@ -8,6 +8,7 @@ import (
"github.com/charmbracelet/lipgloss"
"github.com/opencode-ai/opencode/internal/tui/layout"
"github.com/opencode-ai/opencode/internal/tui/styles"
"github.com/opencode-ai/opencode/internal/tui/theme"
"github.com/opencode-ai/opencode/internal/tui/util"
)
@@ -81,16 +82,19 @@ func (q *quitDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
}
func (q *quitDialogCmp) View() string {
yesStyle := styles.BaseStyle
noStyle := styles.BaseStyle
spacerStyle := styles.BaseStyle.Background(styles.Background)
t := theme.CurrentTheme()
baseStyle := styles.BaseStyle()
yesStyle := baseStyle
noStyle := baseStyle
spacerStyle := baseStyle.Background(t.Background())
if q.selectedNo {
noStyle = noStyle.Background(styles.PrimaryColor).Foreground(styles.Background)
yesStyle = yesStyle.Background(styles.Background).Foreground(styles.PrimaryColor)
noStyle = noStyle.Background(t.Primary()).Foreground(t.Background())
yesStyle = yesStyle.Background(t.Background()).Foreground(t.Primary())
} else {
yesStyle = yesStyle.Background(styles.PrimaryColor).Foreground(styles.Background)
noStyle = noStyle.Background(styles.Background).Foreground(styles.PrimaryColor)
yesStyle = yesStyle.Background(t.Primary()).Foreground(t.Background())
noStyle = noStyle.Background(t.Background()).Foreground(t.Primary())
}
yesButton := yesStyle.Padding(0, 1).Render("Yes")
@@ -104,7 +108,7 @@ func (q *quitDialogCmp) View() string {
buttons = spacerStyle.Render(strings.Repeat(" ", remainingWidth)) + buttons
}
content := styles.BaseStyle.Render(
content := baseStyle.Render(
lipgloss.JoinVertical(
lipgloss.Center,
question,
@@ -113,10 +117,10 @@ func (q *quitDialogCmp) View() string {
),
)
return styles.BaseStyle.Padding(1, 2).
return baseStyle.Padding(1, 2).
Border(lipgloss.RoundedBorder()).
BorderBackground(styles.Background).
BorderForeground(styles.ForgroundDim).
BorderBackground(t.Background()).
BorderForeground(t.TextMuted()).
Width(lipgloss.Width(content) + 4).
Render(content)
}
+18 -14
View File
@@ -7,6 +7,7 @@ import (
"github.com/opencode-ai/opencode/internal/session"
"github.com/opencode-ai/opencode/internal/tui/layout"
"github.com/opencode-ai/opencode/internal/tui/styles"
"github.com/opencode-ai/opencode/internal/tui/theme"
"github.com/opencode-ai/opencode/internal/tui/util"
)
@@ -105,11 +106,14 @@ func (s *sessionDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
}
func (s *sessionDialogCmp) View() string {
t := theme.CurrentTheme()
baseStyle := styles.BaseStyle()
if len(s.sessions) == 0 {
return styles.BaseStyle.Padding(1, 2).
return baseStyle.Padding(1, 2).
Border(lipgloss.RoundedBorder()).
BorderBackground(styles.Background).
BorderForeground(styles.ForgroundDim).
BorderBackground(t.Background()).
BorderForeground(t.TextMuted()).
Width(40).
Render("No sessions available")
}
@@ -146,20 +150,20 @@ func (s *sessionDialogCmp) View() string {
for i := startIdx; i < endIdx; i++ {
sess := s.sessions[i]
itemStyle := styles.BaseStyle.Width(maxWidth)
itemStyle := baseStyle.Width(maxWidth)
if i == s.selectedIdx {
itemStyle = itemStyle.
Background(styles.PrimaryColor).
Foreground(styles.Background).
Background(t.Primary()).
Foreground(t.Background()).
Bold(true)
}
sessionItems = append(sessionItems, itemStyle.Padding(0, 1).Render(sess.Title))
}
title := styles.BaseStyle.
Foreground(styles.PrimaryColor).
title := baseStyle.
Foreground(t.Primary()).
Bold(true).
Width(maxWidth).
Padding(0, 1).
@@ -168,15 +172,15 @@ func (s *sessionDialogCmp) View() string {
content := lipgloss.JoinVertical(
lipgloss.Left,
title,
styles.BaseStyle.Width(maxWidth).Render(""),
styles.BaseStyle.Width(maxWidth).Render(lipgloss.JoinVertical(lipgloss.Left, sessionItems...)),
styles.BaseStyle.Width(maxWidth).Render(""),
baseStyle.Width(maxWidth).Render(""),
baseStyle.Width(maxWidth).Render(lipgloss.JoinVertical(lipgloss.Left, sessionItems...)),
baseStyle.Width(maxWidth).Render(""),
)
return styles.BaseStyle.Padding(1, 2).
return baseStyle.Padding(1, 2).
Border(lipgloss.RoundedBorder()).
BorderBackground(styles.Background).
BorderForeground(styles.ForgroundDim).
BorderBackground(t.Background()).
BorderForeground(t.TextMuted()).
Width(lipgloss.Width(content) + 4).
Render(content)
}
+198
View File
@@ -0,0 +1,198 @@
package dialog
import (
"github.com/charmbracelet/bubbles/key"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"github.com/opencode-ai/opencode/internal/tui/layout"
"github.com/opencode-ai/opencode/internal/tui/styles"
"github.com/opencode-ai/opencode/internal/tui/theme"
"github.com/opencode-ai/opencode/internal/tui/util"
)
// ThemeChangedMsg is sent when the theme is changed
type ThemeChangedMsg struct {
ThemeName string
}
// CloseThemeDialogMsg is sent when the theme dialog is closed
type CloseThemeDialogMsg struct{}
// ThemeDialog interface for the theme switching dialog
type ThemeDialog interface {
tea.Model
layout.Bindings
}
type themeDialogCmp struct {
themes []string
selectedIdx int
width int
height int
currentTheme string
}
type themeKeyMap struct {
Up key.Binding
Down key.Binding
Enter key.Binding
Escape key.Binding
J key.Binding
K key.Binding
}
var themeKeys = themeKeyMap{
Up: key.NewBinding(
key.WithKeys("up"),
key.WithHelp("↑", "previous theme"),
),
Down: key.NewBinding(
key.WithKeys("down"),
key.WithHelp("↓", "next theme"),
),
Enter: key.NewBinding(
key.WithKeys("enter"),
key.WithHelp("enter", "select theme"),
),
Escape: key.NewBinding(
key.WithKeys("esc"),
key.WithHelp("esc", "close"),
),
J: key.NewBinding(
key.WithKeys("j"),
key.WithHelp("j", "next theme"),
),
K: key.NewBinding(
key.WithKeys("k"),
key.WithHelp("k", "previous theme"),
),
}
func (t *themeDialogCmp) Init() tea.Cmd {
// Load available themes and update selectedIdx based on current theme
t.themes = theme.AvailableThemes()
t.currentTheme = theme.CurrentThemeName()
// Find the current theme in the list
for i, name := range t.themes {
if name == t.currentTheme {
t.selectedIdx = i
break
}
}
return nil
}
func (t *themeDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyMsg:
switch {
case key.Matches(msg, themeKeys.Up) || key.Matches(msg, themeKeys.K):
if t.selectedIdx > 0 {
t.selectedIdx--
}
return t, nil
case key.Matches(msg, themeKeys.Down) || key.Matches(msg, themeKeys.J):
if t.selectedIdx < len(t.themes)-1 {
t.selectedIdx++
}
return t, nil
case key.Matches(msg, themeKeys.Enter):
if len(t.themes) > 0 {
previousTheme := theme.CurrentThemeName()
selectedTheme := t.themes[t.selectedIdx]
if previousTheme == selectedTheme {
return t, util.CmdHandler(CloseThemeDialogMsg{})
}
if err := theme.SetTheme(selectedTheme); err != nil {
return t, util.ReportError(err)
}
return t, util.CmdHandler(ThemeChangedMsg{
ThemeName: selectedTheme,
})
}
case key.Matches(msg, themeKeys.Escape):
return t, util.CmdHandler(CloseThemeDialogMsg{})
}
case tea.WindowSizeMsg:
t.width = msg.Width
t.height = msg.Height
}
return t, nil
}
func (t *themeDialogCmp) View() string {
currentTheme := theme.CurrentTheme()
baseStyle := styles.BaseStyle()
if len(t.themes) == 0 {
return baseStyle.Padding(1, 2).
Border(lipgloss.RoundedBorder()).
BorderBackground(currentTheme.Background()).
BorderForeground(currentTheme.TextMuted()).
Width(40).
Render("No themes available")
}
// Calculate max width needed for theme names
maxWidth := 40 // Minimum width
for _, themeName := range t.themes {
if len(themeName) > maxWidth-4 { // Account for padding
maxWidth = len(themeName) + 4
}
}
maxWidth = max(30, min(maxWidth, t.width-15)) // Limit width to avoid overflow
// Build the theme list
themeItems := make([]string, 0, len(t.themes))
for i, themeName := range t.themes {
itemStyle := baseStyle.Width(maxWidth)
if i == t.selectedIdx {
itemStyle = itemStyle.
Background(currentTheme.Primary()).
Foreground(currentTheme.Background()).
Bold(true)
}
themeItems = append(themeItems, itemStyle.Padding(0, 1).Render(themeName))
}
title := baseStyle.
Foreground(currentTheme.Primary()).
Bold(true).
Width(maxWidth).
Padding(0, 1).
Render("Select Theme")
content := lipgloss.JoinVertical(
lipgloss.Left,
title,
baseStyle.Width(maxWidth).Render(""),
baseStyle.Width(maxWidth).Render(lipgloss.JoinVertical(lipgloss.Left, themeItems...)),
baseStyle.Width(maxWidth).Render(""),
)
return baseStyle.Padding(1, 2).
Border(lipgloss.RoundedBorder()).
BorderBackground(currentTheme.Background()).
BorderForeground(currentTheme.TextMuted()).
Width(lipgloss.Width(content) + 4).
Render(content)
}
func (t *themeDialogCmp) BindingKeys() []key.Binding {
return layout.KeyMapToSlice(themeKeys)
}
// NewThemeDialogCmp creates a new theme switching dialog
func NewThemeDialogCmp() ThemeDialog {
return &themeDialogCmp{
themes: []string{},
selectedIdx: 0,
currentTheme: "",
}
}
+16 -12
View File
@@ -12,6 +12,7 @@ import (
"github.com/opencode-ai/opencode/internal/logging"
"github.com/opencode-ai/opencode/internal/tui/layout"
"github.com/opencode-ai/opencode/internal/tui/styles"
"github.com/opencode-ai/opencode/internal/tui/theme"
)
type DetailComponent interface {
@@ -49,9 +50,10 @@ func (i *detailCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
func (i *detailCmp) updateContent() {
var content strings.Builder
t := theme.CurrentTheme()
// Format the header with timestamp and level
timeStyle := lipgloss.NewStyle().Foreground(styles.SubText0)
timeStyle := lipgloss.NewStyle().Foreground(t.TextMuted())
levelStyle := getLevelStyle(i.currentLog.Level)
header := lipgloss.JoinHorizontal(
@@ -65,7 +67,7 @@ func (i *detailCmp) updateContent() {
content.WriteString("\n\n")
// Message with styling
messageStyle := lipgloss.NewStyle().Bold(true).Foreground(styles.Text)
messageStyle := lipgloss.NewStyle().Bold(true).Foreground(t.Text())
content.WriteString(messageStyle.Render("Message:"))
content.WriteString("\n")
content.WriteString(lipgloss.NewStyle().Padding(0, 2).Render(i.currentLog.Message))
@@ -73,13 +75,13 @@ func (i *detailCmp) updateContent() {
// Attributes section
if len(i.currentLog.Attributes) > 0 {
attrHeaderStyle := lipgloss.NewStyle().Bold(true).Foreground(styles.Text)
attrHeaderStyle := lipgloss.NewStyle().Bold(true).Foreground(t.Text())
content.WriteString(attrHeaderStyle.Render("Attributes:"))
content.WriteString("\n")
// Create a table-like display for attributes
keyStyle := lipgloss.NewStyle().Foreground(styles.Primary).Bold(true)
valueStyle := lipgloss.NewStyle().Foreground(styles.Text)
keyStyle := lipgloss.NewStyle().Foreground(t.Primary()).Bold(true)
valueStyle := lipgloss.NewStyle().Foreground(t.Text())
for _, attr := range i.currentLog.Attributes {
attrLine := fmt.Sprintf("%s: %s",
@@ -96,23 +98,25 @@ func (i *detailCmp) updateContent() {
func getLevelStyle(level string) lipgloss.Style {
style := lipgloss.NewStyle().Bold(true)
t := theme.CurrentTheme()
switch strings.ToLower(level) {
case "info":
return style.Foreground(styles.Blue)
return style.Foreground(t.Info())
case "warn", "warning":
return style.Foreground(styles.Warning)
return style.Foreground(t.Warning())
case "error", "err":
return style.Foreground(styles.Error)
return style.Foreground(t.Error())
case "debug":
return style.Foreground(styles.Green)
return style.Foreground(t.Success())
default:
return style.Foreground(styles.Text)
return style.Foreground(t.Text())
}
}
func (i *detailCmp) View() string {
return styles.ForceReplaceBackgroundWithLipgloss(i.viewport.View(), styles.Background)
t := theme.CurrentTheme()
return styles.ForceReplaceBackgroundWithLipgloss(i.viewport.View(), t.Background())
}
func (i *detailCmp) GetSize() (int, int) {
+7 -4
View File
@@ -11,6 +11,7 @@ import (
"github.com/opencode-ai/opencode/internal/pubsub"
"github.com/opencode-ai/opencode/internal/tui/layout"
"github.com/opencode-ai/opencode/internal/tui/styles"
"github.com/opencode-ai/opencode/internal/tui/theme"
"github.com/opencode-ai/opencode/internal/tui/util"
)
@@ -61,7 +62,11 @@ func (i *tableCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
}
func (i *tableCmp) View() string {
return styles.ForceReplaceBackgroundWithLipgloss(i.table.View(), styles.Background)
t := theme.CurrentTheme()
defaultStyles := table.DefaultStyles()
defaultStyles.Selected = defaultStyles.Selected.Foreground(t.Primary())
i.table.SetStyles(defaultStyles)
return styles.ForceReplaceBackgroundWithLipgloss(i.table.View(), t.Background())
}
func (i *tableCmp) GetSize() (int, int) {
@@ -121,11 +126,9 @@ func NewLogsTable() TableComponent {
{Title: "Message", Width: 10},
{Title: "Attributes", Width: 10},
}
defaultStyles := table.DefaultStyles()
defaultStyles.Selected = defaultStyles.Selected.Foreground(styles.Primary)
tableModel := table.New(
table.WithColumns(columns),
table.WithStyles(defaultStyles),
)
tableModel.Focus()
return &tableCmp{
+8 -29
View File
@@ -4,7 +4,7 @@ import (
"github.com/charmbracelet/bubbles/key"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"github.com/opencode-ai/opencode/internal/tui/styles"
"github.com/opencode-ai/opencode/internal/tui/theme"
)
type Container interface {
@@ -29,9 +29,6 @@ type container struct {
borderBottom bool
borderLeft bool
borderStyle lipgloss.Border
borderColor lipgloss.TerminalColor
backgroundColor lipgloss.TerminalColor
}
func (c *container) Init() tea.Cmd {
@@ -45,13 +42,12 @@ func (c *container) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
}
func (c *container) View() string {
t := theme.CurrentTheme()
style := lipgloss.NewStyle()
width := c.width
height := c.height
// Apply background color if specified
if c.backgroundColor != nil {
style = style.Background(c.backgroundColor)
}
style = style.Background(t.Background())
// Apply border if any side is enabled
if c.borderTop || c.borderRight || c.borderBottom || c.borderLeft {
@@ -69,11 +65,7 @@ func (c *container) View() string {
width--
}
style = style.Border(c.borderStyle, c.borderTop, c.borderRight, c.borderBottom, c.borderLeft)
// Apply border color if specified
if c.borderColor != nil {
style = style.BorderBackground(c.backgroundColor).BorderForeground(c.borderColor)
}
style = style.BorderBackground(t.Background()).BorderForeground(t.BorderNormal())
}
style = style.
Width(width).
@@ -132,11 +124,10 @@ func (c *container) BindingKeys() []key.Binding {
type ContainerOption func(*container)
func NewContainer(content tea.Model, options ...ContainerOption) Container {
c := &container{
content: content,
borderColor: styles.BorderColor,
borderStyle: lipgloss.NormalBorder(),
backgroundColor: styles.Background,
content: content,
borderStyle: lipgloss.NormalBorder(),
}
for _, option := range options {
@@ -201,12 +192,6 @@ func WithBorderStyle(style lipgloss.Border) ContainerOption {
}
}
func WithBorderColor(color lipgloss.TerminalColor) ContainerOption {
return func(c *container) {
c.borderColor = color
}
}
func WithRoundedBorder() ContainerOption {
return WithBorderStyle(lipgloss.RoundedBorder())
}
@@ -218,9 +203,3 @@ func WithThickBorder() ContainerOption {
func WithDoubleBorder() ContainerOption {
return WithBorderStyle(lipgloss.DoubleBorder())
}
func WithBackgroundColor(color lipgloss.TerminalColor) ContainerOption {
return func(c *container) {
c.backgroundColor = color
}
}
+7 -3
View File
@@ -9,6 +9,7 @@ import (
"github.com/muesli/reflow/truncate"
"github.com/muesli/termenv"
"github.com/opencode-ai/opencode/internal/tui/styles"
"github.com/opencode-ai/opencode/internal/tui/theme"
"github.com/opencode-ai/opencode/internal/tui/util"
)
@@ -43,12 +44,15 @@ func PlaceOverlay(
fgHeight := len(fgLines)
if shadow {
t := theme.CurrentTheme()
baseStyle := styles.BaseStyle()
var shadowbg string = ""
shadowchar := lipgloss.NewStyle().
Background(styles.BackgroundDarker).
Foreground(styles.Background).
Background(t.BackgroundDarker()).
Foreground(t.Background()).
Render("░")
bgchar := styles.BaseStyle.Render(" ")
bgchar := baseStyle.Render(" ")
for i := 0; i <= fgHeight; i++ {
if i == 0 {
shadowbg += bgchar + strings.Repeat(bgchar, fgWidth) + "\n"
+8 -14
View File
@@ -4,7 +4,7 @@ import (
"github.com/charmbracelet/bubbles/key"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"github.com/opencode-ai/opencode/internal/tui/styles"
"github.com/opencode-ai/opencode/internal/tui/theme"
)
type SplitPaneLayout interface {
@@ -29,8 +29,6 @@ type splitPaneLayout struct {
rightPanel Container
leftPanel Container
bottomPanel Container
backgroundColor lipgloss.TerminalColor
}
type SplitPaneOption func(*splitPaneLayout)
@@ -113,11 +111,13 @@ func (s *splitPaneLayout) View() string {
finalView = topSection
}
if s.backgroundColor != nil && finalView != "" {
if finalView != "" {
t := theme.CurrentTheme()
style := lipgloss.NewStyle().
Width(s.width).
Height(s.height).
Background(s.backgroundColor)
Background(t.Background())
return style.Render(finalView)
}
@@ -241,10 +241,10 @@ func (s *splitPaneLayout) BindingKeys() []key.Binding {
}
func NewSplitPane(options ...SplitPaneOption) SplitPaneLayout {
layout := &splitPaneLayout{
ratio: 0.7,
verticalRatio: 0.9, // Default 80% for top section, 20% for bottom
backgroundColor: styles.Background,
ratio: 0.7,
verticalRatio: 0.9, // Default 90% for top section, 10% for bottom
}
for _, option := range options {
option(layout)
@@ -270,12 +270,6 @@ func WithRatio(ratio float64) SplitPaneOption {
}
}
func WithSplitBackgroundColor(color lipgloss.TerminalColor) SplitPaneOption {
return func(s *splitPaneLayout) {
s.backgroundColor = color
}
}
func WithBottomPanel(panel Container) SplitPaneOption {
return func(s *splitPaneLayout) {
s.bottomPanel = panel
-1
View File
@@ -142,7 +142,6 @@ func NewChatPage(app *app.App) tea.Model {
chat.NewMessagesCmp(app),
layout.WithPadding(1, 1, 0, 1),
)
editorContainer := layout.NewContainer(
chat.NewEditorCmp(app),
layout.WithBorder(true, false, false, false),
+3 -3
View File
@@ -42,7 +42,7 @@ func (p *logsPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
}
func (p *logsPage) View() string {
style := styles.BaseStyle.Width(p.width).Height(p.height)
style := styles.BaseStyle().Width(p.width).Height(p.height)
return style.Render(lipgloss.JoinVertical(lipgloss.Top,
p.table.View(),
p.details.View(),
@@ -77,7 +77,7 @@ func (p *logsPage) Init() tea.Cmd {
func NewLogsPage() LogPage {
return &logsPage{
table: layout.NewContainer(logs.NewLogsTable(), layout.WithBorderAll(), layout.WithBorderColor(styles.ForgroundDim)),
details: layout.NewContainer(logs.NewLogsDetails(), layout.WithBorderAll(), layout.WithBorderColor(styles.ForgroundDim)),
table: layout.NewContainer(logs.NewLogsTable(), layout.WithBorderAll()),
details: layout.NewContainer(logs.NewLogsDetails(), layout.WithBorderAll()),
}
}
-46
View File
@@ -1,46 +0,0 @@
package styles
import (
"github.com/charmbracelet/huh"
"github.com/charmbracelet/lipgloss"
)
func HuhTheme() *huh.Theme {
t := huh.ThemeBase()
t.Focused.Base = t.Focused.Base.BorderStyle(lipgloss.HiddenBorder())
t.Focused.Title = t.Focused.Title.Foreground(Text)
t.Focused.NoteTitle = t.Focused.NoteTitle.Foreground(Text)
t.Focused.Directory = t.Focused.Directory.Foreground(Text)
t.Focused.Description = t.Focused.Description.Foreground(SubText0)
t.Focused.ErrorIndicator = t.Focused.ErrorIndicator.Foreground(Red)
t.Focused.ErrorMessage = t.Focused.ErrorMessage.Foreground(Red)
t.Focused.SelectSelector = t.Focused.SelectSelector.Foreground(Blue)
t.Focused.NextIndicator = t.Focused.NextIndicator.Foreground(Blue)
t.Focused.PrevIndicator = t.Focused.PrevIndicator.Foreground(Blue)
t.Focused.Option = t.Focused.Option.Foreground(Text)
t.Focused.MultiSelectSelector = t.Focused.MultiSelectSelector.Foreground(Blue)
t.Focused.SelectedOption = t.Focused.SelectedOption.Foreground(Green)
t.Focused.SelectedPrefix = t.Focused.SelectedPrefix.Foreground(Green)
t.Focused.UnselectedPrefix = t.Focused.UnselectedPrefix.Foreground(Text)
t.Focused.UnselectedOption = t.Focused.UnselectedOption.Foreground(Text)
t.Focused.FocusedButton = t.Focused.FocusedButton.Foreground(Base).Background(Blue)
t.Focused.BlurredButton = t.Focused.BlurredButton.Foreground(Text).Background(Base)
t.Focused.TextInput.Cursor = t.Focused.TextInput.Cursor.Foreground(Teal)
t.Focused.TextInput.Placeholder = t.Focused.TextInput.Placeholder.Foreground(Overlay0)
t.Focused.TextInput.Prompt = t.Focused.TextInput.Prompt.Foreground(Blue)
t.Blurred = t.Focused
t.Blurred.Base = t.Blurred.Base.BorderStyle(lipgloss.HiddenBorder())
t.Help.Ellipsis = t.Help.Ellipsis.Foreground(SubText0)
t.Help.ShortKey = t.Help.ShortKey.Foreground(SubText0)
t.Help.ShortDesc = t.Help.ShortDesc.Foreground(Ovelay1)
t.Help.ShortSeparator = t.Help.ShortSeparator.Foreground(SubText0)
t.Help.FullKey = t.Help.FullKey.Foreground(SubText0)
t.Help.FullDesc = t.Help.FullDesc.Foreground(Ovelay1)
t.Help.FullSeparator = t.Help.FullSeparator.Foreground(SubText0)
return t
}
File diff suppressed because it is too large Load Diff
+120 -145
View File
@@ -1,177 +1,152 @@
package styles
import (
catppuccin "github.com/catppuccin/go"
"github.com/charmbracelet/lipgloss"
"github.com/opencode-ai/opencode/internal/tui/theme"
)
var (
light = catppuccin.Latte
dark = catppuccin.Mocha
)
// Style generation functions that use the current theme
// NEW STYLES
var (
Background = lipgloss.AdaptiveColor{
Dark: "#212121",
Light: "#212121",
}
BackgroundDim = lipgloss.AdaptiveColor{
Dark: "#2c2c2c",
Light: "#2c2c2c",
}
BackgroundDarker = lipgloss.AdaptiveColor{
Dark: "#181818",
Light: "#181818",
}
BorderColor = lipgloss.AdaptiveColor{
Dark: "#4b4c5c",
Light: "#4b4c5c",
}
// BaseStyle returns the base style with background and foreground colors
func BaseStyle() lipgloss.Style {
t := theme.CurrentTheme()
return lipgloss.NewStyle().
Background(t.Background()).
Foreground(t.Text())
}
Forground = lipgloss.AdaptiveColor{
Dark: "#d3d3d3",
Light: "#d3d3d3",
}
// Regular returns a basic unstyled lipgloss.Style
func Regular() lipgloss.Style {
return lipgloss.NewStyle()
}
ForgroundMid = lipgloss.AdaptiveColor{
Dark: "#a0a0a0",
Light: "#a0a0a0",
}
// Bold returns a bold style
func Bold() lipgloss.Style {
return Regular().Bold(true)
}
ForgroundDim = lipgloss.AdaptiveColor{
Dark: "#737373",
Light: "#737373",
}
// Padded returns a style with horizontal padding
func Padded() lipgloss.Style {
return Regular().Padding(0, 1)
}
BaseStyle = lipgloss.NewStyle().
Background(Background).
Foreground(Forground)
// Border returns a style with a normal border
func Border() lipgloss.Style {
t := theme.CurrentTheme()
return Regular().
Border(lipgloss.NormalBorder()).
BorderForeground(t.BorderNormal())
}
PrimaryColor = lipgloss.AdaptiveColor{
Dark: "#fab283",
Light: "#fab283",
}
)
// ThickBorder returns a style with a thick border
func ThickBorder() lipgloss.Style {
t := theme.CurrentTheme()
return Regular().
Border(lipgloss.ThickBorder()).
BorderForeground(t.BorderNormal())
}
var (
Regular = lipgloss.NewStyle()
Bold = Regular.Bold(true)
Padded = Regular.Padding(0, 1)
// DoubleBorder returns a style with a double border
func DoubleBorder() lipgloss.Style {
t := theme.CurrentTheme()
return Regular().
Border(lipgloss.DoubleBorder()).
BorderForeground(t.BorderNormal())
}
Border = Regular.Border(lipgloss.NormalBorder())
ThickBorder = Regular.Border(lipgloss.ThickBorder())
DoubleBorder = Regular.Border(lipgloss.DoubleBorder())
// FocusedBorder returns a style with a border using the focused border color
func FocusedBorder() lipgloss.Style {
t := theme.CurrentTheme()
return Regular().
Border(lipgloss.NormalBorder()).
BorderForeground(t.BorderFocused())
}
// Colors
White = lipgloss.Color("#ffffff")
Surface0 = lipgloss.AdaptiveColor{
Dark: dark.Surface0().Hex,
Light: light.Surface0().Hex,
}
// DimBorder returns a style with a border using the dim border color
func DimBorder() lipgloss.Style {
t := theme.CurrentTheme()
return Regular().
Border(lipgloss.NormalBorder()).
BorderForeground(t.BorderDim())
}
Overlay0 = lipgloss.AdaptiveColor{
Dark: dark.Overlay0().Hex,
Light: light.Overlay0().Hex,
}
// PrimaryColor returns the primary color from the current theme
func PrimaryColor() lipgloss.AdaptiveColor {
return theme.CurrentTheme().Primary()
}
Ovelay1 = lipgloss.AdaptiveColor{
Dark: dark.Overlay1().Hex,
Light: light.Overlay1().Hex,
}
// SecondaryColor returns the secondary color from the current theme
func SecondaryColor() lipgloss.AdaptiveColor {
return theme.CurrentTheme().Secondary()
}
Text = lipgloss.AdaptiveColor{
Dark: dark.Text().Hex,
Light: light.Text().Hex,
}
// AccentColor returns the accent color from the current theme
func AccentColor() lipgloss.AdaptiveColor {
return theme.CurrentTheme().Accent()
}
SubText0 = lipgloss.AdaptiveColor{
Dark: dark.Subtext0().Hex,
Light: light.Subtext0().Hex,
}
// ErrorColor returns the error color from the current theme
func ErrorColor() lipgloss.AdaptiveColor {
return theme.CurrentTheme().Error()
}
SubText1 = lipgloss.AdaptiveColor{
Dark: dark.Subtext1().Hex,
Light: light.Subtext1().Hex,
}
// WarningColor returns the warning color from the current theme
func WarningColor() lipgloss.AdaptiveColor {
return theme.CurrentTheme().Warning()
}
LightGrey = lipgloss.AdaptiveColor{
Dark: dark.Surface0().Hex,
Light: light.Surface0().Hex,
}
Grey = lipgloss.AdaptiveColor{
Dark: dark.Surface1().Hex,
Light: light.Surface1().Hex,
}
// SuccessColor returns the success color from the current theme
func SuccessColor() lipgloss.AdaptiveColor {
return theme.CurrentTheme().Success()
}
DarkGrey = lipgloss.AdaptiveColor{
Dark: dark.Surface2().Hex,
Light: light.Surface2().Hex,
}
// InfoColor returns the info color from the current theme
func InfoColor() lipgloss.AdaptiveColor {
return theme.CurrentTheme().Info()
}
Base = lipgloss.AdaptiveColor{
Dark: dark.Base().Hex,
Light: light.Base().Hex,
}
// TextColor returns the text color from the current theme
func TextColor() lipgloss.AdaptiveColor {
return theme.CurrentTheme().Text()
}
Crust = lipgloss.AdaptiveColor{
Dark: dark.Crust().Hex,
Light: light.Crust().Hex,
}
// TextMutedColor returns the muted text color from the current theme
func TextMutedColor() lipgloss.AdaptiveColor {
return theme.CurrentTheme().TextMuted()
}
Blue = lipgloss.AdaptiveColor{
Dark: dark.Blue().Hex,
Light: light.Blue().Hex,
}
// TextEmphasizedColor returns the emphasized text color from the current theme
func TextEmphasizedColor() lipgloss.AdaptiveColor {
return theme.CurrentTheme().TextEmphasized()
}
Red = lipgloss.AdaptiveColor{
Dark: dark.Red().Hex,
Light: light.Red().Hex,
}
// BackgroundColor returns the background color from the current theme
func BackgroundColor() lipgloss.AdaptiveColor {
return theme.CurrentTheme().Background()
}
Green = lipgloss.AdaptiveColor{
Dark: dark.Green().Hex,
Light: light.Green().Hex,
}
// BackgroundSecondaryColor returns the secondary background color from the current theme
func BackgroundSecondaryColor() lipgloss.AdaptiveColor {
return theme.CurrentTheme().BackgroundSecondary()
}
Mauve = lipgloss.AdaptiveColor{
Dark: dark.Mauve().Hex,
Light: light.Mauve().Hex,
}
// BackgroundDarkerColor returns the darker background color from the current theme
func BackgroundDarkerColor() lipgloss.AdaptiveColor {
return theme.CurrentTheme().BackgroundDarker()
}
Teal = lipgloss.AdaptiveColor{
Dark: dark.Teal().Hex,
Light: light.Teal().Hex,
}
// BorderNormalColor returns the normal border color from the current theme
func BorderNormalColor() lipgloss.AdaptiveColor {
return theme.CurrentTheme().BorderNormal()
}
Rosewater = lipgloss.AdaptiveColor{
Dark: dark.Rosewater().Hex,
Light: light.Rosewater().Hex,
}
// BorderFocusedColor returns the focused border color from the current theme
func BorderFocusedColor() lipgloss.AdaptiveColor {
return theme.CurrentTheme().BorderFocused()
}
Flamingo = lipgloss.AdaptiveColor{
Dark: dark.Flamingo().Hex,
Light: light.Flamingo().Hex,
}
// BorderDimColor returns the dim border color from the current theme
func BorderDimColor() lipgloss.AdaptiveColor {
return theme.CurrentTheme().BorderDim()
}
Lavender = lipgloss.AdaptiveColor{
Dark: dark.Lavender().Hex,
Light: light.Lavender().Hex,
}
Peach = lipgloss.AdaptiveColor{
Dark: dark.Peach().Hex,
Light: light.Peach().Hex,
}
Yellow = lipgloss.AdaptiveColor{
Dark: dark.Yellow().Hex,
Light: light.Yellow().Hex,
}
Primary = Blue
Secondary = Mauve
Warning = Peach
Error = Red
)
+248
View File
@@ -0,0 +1,248 @@
package theme
import (
catppuccin "github.com/catppuccin/go"
"github.com/charmbracelet/lipgloss"
)
// CatppuccinTheme implements the Theme interface with Catppuccin colors.
// It provides both dark (Mocha) and light (Latte) variants.
type CatppuccinTheme struct {
BaseTheme
}
// NewCatppuccinTheme creates a new instance of the Catppuccin theme.
func NewCatppuccinTheme() *CatppuccinTheme {
// Get the Catppuccin palettes
mocha := catppuccin.Mocha
latte := catppuccin.Latte
theme := &CatppuccinTheme{}
// Base colors
theme.PrimaryColor = lipgloss.AdaptiveColor{
Dark: mocha.Blue().Hex,
Light: latte.Blue().Hex,
}
theme.SecondaryColor = lipgloss.AdaptiveColor{
Dark: mocha.Mauve().Hex,
Light: latte.Mauve().Hex,
}
theme.AccentColor = lipgloss.AdaptiveColor{
Dark: mocha.Peach().Hex,
Light: latte.Peach().Hex,
}
// Status colors
theme.ErrorColor = lipgloss.AdaptiveColor{
Dark: mocha.Red().Hex,
Light: latte.Red().Hex,
}
theme.WarningColor = lipgloss.AdaptiveColor{
Dark: mocha.Peach().Hex,
Light: latte.Peach().Hex,
}
theme.SuccessColor = lipgloss.AdaptiveColor{
Dark: mocha.Green().Hex,
Light: latte.Green().Hex,
}
theme.InfoColor = lipgloss.AdaptiveColor{
Dark: mocha.Blue().Hex,
Light: latte.Blue().Hex,
}
// Text colors
theme.TextColor = lipgloss.AdaptiveColor{
Dark: mocha.Text().Hex,
Light: latte.Text().Hex,
}
theme.TextMutedColor = lipgloss.AdaptiveColor{
Dark: mocha.Subtext0().Hex,
Light: latte.Subtext0().Hex,
}
theme.TextEmphasizedColor = lipgloss.AdaptiveColor{
Dark: mocha.Lavender().Hex,
Light: latte.Lavender().Hex,
}
// Background colors
theme.BackgroundColor = lipgloss.AdaptiveColor{
Dark: "#212121", // From existing styles
Light: "#EEEEEE", // Light equivalent
}
theme.BackgroundSecondaryColor = lipgloss.AdaptiveColor{
Dark: "#2c2c2c", // From existing styles
Light: "#E0E0E0", // Light equivalent
}
theme.BackgroundDarkerColor = lipgloss.AdaptiveColor{
Dark: "#181818", // From existing styles
Light: "#F5F5F5", // Light equivalent
}
// Border colors
theme.BorderNormalColor = lipgloss.AdaptiveColor{
Dark: "#4b4c5c", // From existing styles
Light: "#BDBDBD", // Light equivalent
}
theme.BorderFocusedColor = lipgloss.AdaptiveColor{
Dark: mocha.Blue().Hex,
Light: latte.Blue().Hex,
}
theme.BorderDimColor = lipgloss.AdaptiveColor{
Dark: mocha.Surface0().Hex,
Light: latte.Surface0().Hex,
}
// Diff view colors
theme.DiffAddedColor = lipgloss.AdaptiveColor{
Dark: "#478247", // From existing diff.go
Light: "#2E7D32", // Light equivalent
}
theme.DiffRemovedColor = lipgloss.AdaptiveColor{
Dark: "#7C4444", // From existing diff.go
Light: "#C62828", // Light equivalent
}
theme.DiffContextColor = lipgloss.AdaptiveColor{
Dark: "#a0a0a0", // From existing diff.go
Light: "#757575", // Light equivalent
}
theme.DiffHunkHeaderColor = lipgloss.AdaptiveColor{
Dark: "#a0a0a0", // From existing diff.go
Light: "#757575", // Light equivalent
}
theme.DiffHighlightAddedColor = lipgloss.AdaptiveColor{
Dark: "#DAFADA", // From existing diff.go
Light: "#A5D6A7", // Light equivalent
}
theme.DiffHighlightRemovedColor = lipgloss.AdaptiveColor{
Dark: "#FADADD", // From existing diff.go
Light: "#EF9A9A", // Light equivalent
}
theme.DiffAddedBgColor = lipgloss.AdaptiveColor{
Dark: "#303A30", // From existing diff.go
Light: "#E8F5E9", // Light equivalent
}
theme.DiffRemovedBgColor = lipgloss.AdaptiveColor{
Dark: "#3A3030", // From existing diff.go
Light: "#FFEBEE", // Light equivalent
}
theme.DiffContextBgColor = lipgloss.AdaptiveColor{
Dark: "#212121", // From existing diff.go
Light: "#F5F5F5", // Light equivalent
}
theme.DiffLineNumberColor = lipgloss.AdaptiveColor{
Dark: "#888888", // From existing diff.go
Light: "#9E9E9E", // Light equivalent
}
theme.DiffAddedLineNumberBgColor = lipgloss.AdaptiveColor{
Dark: "#293229", // From existing diff.go
Light: "#C8E6C9", // Light equivalent
}
theme.DiffRemovedLineNumberBgColor = lipgloss.AdaptiveColor{
Dark: "#332929", // From existing diff.go
Light: "#FFCDD2", // Light equivalent
}
// Markdown colors
theme.MarkdownTextColor = lipgloss.AdaptiveColor{
Dark: mocha.Text().Hex,
Light: latte.Text().Hex,
}
theme.MarkdownHeadingColor = lipgloss.AdaptiveColor{
Dark: mocha.Mauve().Hex,
Light: latte.Mauve().Hex,
}
theme.MarkdownLinkColor = lipgloss.AdaptiveColor{
Dark: mocha.Sky().Hex,
Light: latte.Sky().Hex,
}
theme.MarkdownLinkTextColor = lipgloss.AdaptiveColor{
Dark: mocha.Pink().Hex,
Light: latte.Pink().Hex,
}
theme.MarkdownCodeColor = lipgloss.AdaptiveColor{
Dark: mocha.Green().Hex,
Light: latte.Green().Hex,
}
theme.MarkdownBlockQuoteColor = lipgloss.AdaptiveColor{
Dark: mocha.Yellow().Hex,
Light: latte.Yellow().Hex,
}
theme.MarkdownEmphColor = lipgloss.AdaptiveColor{
Dark: mocha.Yellow().Hex,
Light: latte.Yellow().Hex,
}
theme.MarkdownStrongColor = lipgloss.AdaptiveColor{
Dark: mocha.Peach().Hex,
Light: latte.Peach().Hex,
}
theme.MarkdownHorizontalRuleColor = lipgloss.AdaptiveColor{
Dark: mocha.Overlay0().Hex,
Light: latte.Overlay0().Hex,
}
theme.MarkdownListItemColor = lipgloss.AdaptiveColor{
Dark: mocha.Blue().Hex,
Light: latte.Blue().Hex,
}
theme.MarkdownListEnumerationColor = lipgloss.AdaptiveColor{
Dark: mocha.Sky().Hex,
Light: latte.Sky().Hex,
}
theme.MarkdownImageColor = lipgloss.AdaptiveColor{
Dark: mocha.Sapphire().Hex,
Light: latte.Sapphire().Hex,
}
theme.MarkdownImageTextColor = lipgloss.AdaptiveColor{
Dark: mocha.Pink().Hex,
Light: latte.Pink().Hex,
}
theme.MarkdownCodeBlockColor = lipgloss.AdaptiveColor{
Dark: mocha.Text().Hex,
Light: latte.Text().Hex,
}
// Syntax highlighting colors
theme.SyntaxCommentColor = lipgloss.AdaptiveColor{
Dark: mocha.Overlay1().Hex,
Light: latte.Overlay1().Hex,
}
theme.SyntaxKeywordColor = lipgloss.AdaptiveColor{
Dark: mocha.Pink().Hex,
Light: latte.Pink().Hex,
}
theme.SyntaxFunctionColor = lipgloss.AdaptiveColor{
Dark: mocha.Green().Hex,
Light: latte.Green().Hex,
}
theme.SyntaxVariableColor = lipgloss.AdaptiveColor{
Dark: mocha.Sky().Hex,
Light: latte.Sky().Hex,
}
theme.SyntaxStringColor = lipgloss.AdaptiveColor{
Dark: mocha.Yellow().Hex,
Light: latte.Yellow().Hex,
}
theme.SyntaxNumberColor = lipgloss.AdaptiveColor{
Dark: mocha.Teal().Hex,
Light: latte.Teal().Hex,
}
theme.SyntaxTypeColor = lipgloss.AdaptiveColor{
Dark: mocha.Sky().Hex,
Light: latte.Sky().Hex,
}
theme.SyntaxOperatorColor = lipgloss.AdaptiveColor{
Dark: mocha.Pink().Hex,
Light: latte.Pink().Hex,
}
theme.SyntaxPunctuationColor = lipgloss.AdaptiveColor{
Dark: mocha.Text().Hex,
Light: latte.Text().Hex,
}
return theme
}
func init() {
// Register the Catppuccin theme with the theme manager
RegisterTheme("catppuccin", NewCatppuccinTheme())
}
+274
View File
@@ -0,0 +1,274 @@
package theme
import (
"github.com/charmbracelet/lipgloss"
)
// DraculaTheme implements the Theme interface with Dracula colors.
// It provides both dark and light variants, though Dracula is primarily a dark theme.
type DraculaTheme struct {
BaseTheme
}
// NewDraculaTheme creates a new instance of the Dracula theme.
func NewDraculaTheme() *DraculaTheme {
// Dracula color palette
// Official colors from https://draculatheme.com/
darkBackground := "#282a36"
darkCurrentLine := "#44475a"
darkSelection := "#44475a"
darkForeground := "#f8f8f2"
darkComment := "#6272a4"
darkCyan := "#8be9fd"
darkGreen := "#50fa7b"
darkOrange := "#ffb86c"
darkPink := "#ff79c6"
darkPurple := "#bd93f9"
darkRed := "#ff5555"
darkYellow := "#f1fa8c"
darkBorder := "#44475a"
// Light mode approximation (Dracula is primarily a dark theme)
lightBackground := "#f8f8f2"
lightCurrentLine := "#e6e6e6"
lightSelection := "#d8d8d8"
lightForeground := "#282a36"
lightComment := "#6272a4"
lightCyan := "#0097a7"
lightGreen := "#388e3c"
lightOrange := "#f57c00"
lightPink := "#d81b60"
lightPurple := "#7e57c2"
lightRed := "#e53935"
lightYellow := "#fbc02d"
lightBorder := "#d8d8d8"
theme := &DraculaTheme{}
// Base colors
theme.PrimaryColor = lipgloss.AdaptiveColor{
Dark: darkPurple,
Light: lightPurple,
}
theme.SecondaryColor = lipgloss.AdaptiveColor{
Dark: darkPink,
Light: lightPink,
}
theme.AccentColor = lipgloss.AdaptiveColor{
Dark: darkCyan,
Light: lightCyan,
}
// Status colors
theme.ErrorColor = lipgloss.AdaptiveColor{
Dark: darkRed,
Light: lightRed,
}
theme.WarningColor = lipgloss.AdaptiveColor{
Dark: darkOrange,
Light: lightOrange,
}
theme.SuccessColor = lipgloss.AdaptiveColor{
Dark: darkGreen,
Light: lightGreen,
}
theme.InfoColor = lipgloss.AdaptiveColor{
Dark: darkCyan,
Light: lightCyan,
}
// Text colors
theme.TextColor = lipgloss.AdaptiveColor{
Dark: darkForeground,
Light: lightForeground,
}
theme.TextMutedColor = lipgloss.AdaptiveColor{
Dark: darkComment,
Light: lightComment,
}
theme.TextEmphasizedColor = lipgloss.AdaptiveColor{
Dark: darkYellow,
Light: lightYellow,
}
// Background colors
theme.BackgroundColor = lipgloss.AdaptiveColor{
Dark: darkBackground,
Light: lightBackground,
}
theme.BackgroundSecondaryColor = lipgloss.AdaptiveColor{
Dark: darkCurrentLine,
Light: lightCurrentLine,
}
theme.BackgroundDarkerColor = lipgloss.AdaptiveColor{
Dark: "#21222c", // Slightly darker than background
Light: "#ffffff", // Slightly lighter than background
}
// Border colors
theme.BorderNormalColor = lipgloss.AdaptiveColor{
Dark: darkBorder,
Light: lightBorder,
}
theme.BorderFocusedColor = lipgloss.AdaptiveColor{
Dark: darkPurple,
Light: lightPurple,
}
theme.BorderDimColor = lipgloss.AdaptiveColor{
Dark: darkSelection,
Light: lightSelection,
}
// Diff view colors
theme.DiffAddedColor = lipgloss.AdaptiveColor{
Dark: darkGreen,
Light: lightGreen,
}
theme.DiffRemovedColor = lipgloss.AdaptiveColor{
Dark: darkRed,
Light: lightRed,
}
theme.DiffContextColor = lipgloss.AdaptiveColor{
Dark: darkComment,
Light: lightComment,
}
theme.DiffHunkHeaderColor = lipgloss.AdaptiveColor{
Dark: darkPurple,
Light: lightPurple,
}
theme.DiffHighlightAddedColor = lipgloss.AdaptiveColor{
Dark: "#50fa7b",
Light: "#a5d6a7",
}
theme.DiffHighlightRemovedColor = lipgloss.AdaptiveColor{
Dark: "#ff5555",
Light: "#ef9a9a",
}
theme.DiffAddedBgColor = lipgloss.AdaptiveColor{
Dark: "#2c3b2c",
Light: "#e8f5e9",
}
theme.DiffRemovedBgColor = lipgloss.AdaptiveColor{
Dark: "#3b2c2c",
Light: "#ffebee",
}
theme.DiffContextBgColor = lipgloss.AdaptiveColor{
Dark: darkBackground,
Light: lightBackground,
}
theme.DiffLineNumberColor = lipgloss.AdaptiveColor{
Dark: darkComment,
Light: lightComment,
}
theme.DiffAddedLineNumberBgColor = lipgloss.AdaptiveColor{
Dark: "#253025",
Light: "#c8e6c9",
}
theme.DiffRemovedLineNumberBgColor = lipgloss.AdaptiveColor{
Dark: "#302525",
Light: "#ffcdd2",
}
// Markdown colors
theme.MarkdownTextColor = lipgloss.AdaptiveColor{
Dark: darkForeground,
Light: lightForeground,
}
theme.MarkdownHeadingColor = lipgloss.AdaptiveColor{
Dark: darkPink,
Light: lightPink,
}
theme.MarkdownLinkColor = lipgloss.AdaptiveColor{
Dark: darkPurple,
Light: lightPurple,
}
theme.MarkdownLinkTextColor = lipgloss.AdaptiveColor{
Dark: darkCyan,
Light: lightCyan,
}
theme.MarkdownCodeColor = lipgloss.AdaptiveColor{
Dark: darkGreen,
Light: lightGreen,
}
theme.MarkdownBlockQuoteColor = lipgloss.AdaptiveColor{
Dark: darkYellow,
Light: lightYellow,
}
theme.MarkdownEmphColor = lipgloss.AdaptiveColor{
Dark: darkYellow,
Light: lightYellow,
}
theme.MarkdownStrongColor = lipgloss.AdaptiveColor{
Dark: darkOrange,
Light: lightOrange,
}
theme.MarkdownHorizontalRuleColor = lipgloss.AdaptiveColor{
Dark: darkComment,
Light: lightComment,
}
theme.MarkdownListItemColor = lipgloss.AdaptiveColor{
Dark: darkPurple,
Light: lightPurple,
}
theme.MarkdownListEnumerationColor = lipgloss.AdaptiveColor{
Dark: darkCyan,
Light: lightCyan,
}
theme.MarkdownImageColor = lipgloss.AdaptiveColor{
Dark: darkPurple,
Light: lightPurple,
}
theme.MarkdownImageTextColor = lipgloss.AdaptiveColor{
Dark: darkCyan,
Light: lightCyan,
}
theme.MarkdownCodeBlockColor = lipgloss.AdaptiveColor{
Dark: darkForeground,
Light: lightForeground,
}
// Syntax highlighting colors
theme.SyntaxCommentColor = lipgloss.AdaptiveColor{
Dark: darkComment,
Light: lightComment,
}
theme.SyntaxKeywordColor = lipgloss.AdaptiveColor{
Dark: darkPink,
Light: lightPink,
}
theme.SyntaxFunctionColor = lipgloss.AdaptiveColor{
Dark: darkGreen,
Light: lightGreen,
}
theme.SyntaxVariableColor = lipgloss.AdaptiveColor{
Dark: darkOrange,
Light: lightOrange,
}
theme.SyntaxStringColor = lipgloss.AdaptiveColor{
Dark: darkYellow,
Light: lightYellow,
}
theme.SyntaxNumberColor = lipgloss.AdaptiveColor{
Dark: darkPurple,
Light: lightPurple,
}
theme.SyntaxTypeColor = lipgloss.AdaptiveColor{
Dark: darkCyan,
Light: lightCyan,
}
theme.SyntaxOperatorColor = lipgloss.AdaptiveColor{
Dark: darkPink,
Light: lightPink,
}
theme.SyntaxPunctuationColor = lipgloss.AdaptiveColor{
Dark: darkForeground,
Light: lightForeground,
}
return theme
}
func init() {
// Register the Dracula theme with the theme manager
RegisterTheme("dracula", NewDraculaTheme())
}
+282
View File
@@ -0,0 +1,282 @@
package theme
import (
"github.com/charmbracelet/lipgloss"
)
// Flexoki color palette constants
const (
// Base colors
flexokiPaper = "#FFFCF0" // Paper (lightest)
flexokiBase50 = "#F2F0E5" // bg-2 (light)
flexokiBase100 = "#E6E4D9" // ui (light)
flexokiBase150 = "#DAD8CE" // ui-2 (light)
flexokiBase200 = "#CECDC3" // ui-3 (light)
flexokiBase300 = "#B7B5AC" // tx-3 (light)
flexokiBase500 = "#878580" // tx-2 (light)
flexokiBase600 = "#6F6E69" // tx (light)
flexokiBase700 = "#575653" // tx-3 (dark)
flexokiBase800 = "#403E3C" // ui-3 (dark)
flexokiBase850 = "#343331" // ui-2 (dark)
flexokiBase900 = "#282726" // ui (dark)
flexokiBase950 = "#1C1B1A" // bg-2 (dark)
flexokiBlack = "#100F0F" // bg (darkest)
// Accent colors - Light theme (600)
flexokiRed600 = "#AF3029"
flexokiOrange600 = "#BC5215"
flexokiYellow600 = "#AD8301"
flexokiGreen600 = "#66800B"
flexokiCyan600 = "#24837B"
flexokiBlue600 = "#205EA6"
flexokiPurple600 = "#5E409D"
flexokiMagenta600 = "#A02F6F"
// Accent colors - Dark theme (400)
flexokiRed400 = "#D14D41"
flexokiOrange400 = "#DA702C"
flexokiYellow400 = "#D0A215"
flexokiGreen400 = "#879A39"
flexokiCyan400 = "#3AA99F"
flexokiBlue400 = "#4385BE"
flexokiPurple400 = "#8B7EC8"
flexokiMagenta400 = "#CE5D97"
)
// FlexokiTheme implements the Theme interface with Flexoki colors.
// It provides both dark and light variants.
type FlexokiTheme struct {
BaseTheme
}
// NewFlexokiTheme creates a new instance of the Flexoki theme.
func NewFlexokiTheme() *FlexokiTheme {
theme := &FlexokiTheme{}
// Base colors
theme.PrimaryColor = lipgloss.AdaptiveColor{
Dark: flexokiBlue400,
Light: flexokiBlue600,
}
theme.SecondaryColor = lipgloss.AdaptiveColor{
Dark: flexokiPurple400,
Light: flexokiPurple600,
}
theme.AccentColor = lipgloss.AdaptiveColor{
Dark: flexokiOrange400,
Light: flexokiOrange600,
}
// Status colors
theme.ErrorColor = lipgloss.AdaptiveColor{
Dark: flexokiRed400,
Light: flexokiRed600,
}
theme.WarningColor = lipgloss.AdaptiveColor{
Dark: flexokiYellow400,
Light: flexokiYellow600,
}
theme.SuccessColor = lipgloss.AdaptiveColor{
Dark: flexokiGreen400,
Light: flexokiGreen600,
}
theme.InfoColor = lipgloss.AdaptiveColor{
Dark: flexokiCyan400,
Light: flexokiCyan600,
}
// Text colors
theme.TextColor = lipgloss.AdaptiveColor{
Dark: flexokiBase300,
Light: flexokiBase600,
}
theme.TextMutedColor = lipgloss.AdaptiveColor{
Dark: flexokiBase700,
Light: flexokiBase500,
}
theme.TextEmphasizedColor = lipgloss.AdaptiveColor{
Dark: flexokiYellow400,
Light: flexokiYellow600,
}
// Background colors
theme.BackgroundColor = lipgloss.AdaptiveColor{
Dark: flexokiBlack,
Light: flexokiPaper,
}
theme.BackgroundSecondaryColor = lipgloss.AdaptiveColor{
Dark: flexokiBase950,
Light: flexokiBase50,
}
theme.BackgroundDarkerColor = lipgloss.AdaptiveColor{
Dark: flexokiBase900,
Light: flexokiBase100,
}
// Border colors
theme.BorderNormalColor = lipgloss.AdaptiveColor{
Dark: flexokiBase900,
Light: flexokiBase100,
}
theme.BorderFocusedColor = lipgloss.AdaptiveColor{
Dark: flexokiBlue400,
Light: flexokiBlue600,
}
theme.BorderDimColor = lipgloss.AdaptiveColor{
Dark: flexokiBase850,
Light: flexokiBase150,
}
// Diff view colors
theme.DiffAddedColor = lipgloss.AdaptiveColor{
Dark: flexokiGreen400,
Light: flexokiGreen600,
}
theme.DiffRemovedColor = lipgloss.AdaptiveColor{
Dark: flexokiRed400,
Light: flexokiRed600,
}
theme.DiffContextColor = lipgloss.AdaptiveColor{
Dark: flexokiBase700,
Light: flexokiBase500,
}
theme.DiffHunkHeaderColor = lipgloss.AdaptiveColor{
Dark: flexokiBase700,
Light: flexokiBase500,
}
theme.DiffHighlightAddedColor = lipgloss.AdaptiveColor{
Dark: flexokiGreen400,
Light: flexokiGreen600,
}
theme.DiffHighlightRemovedColor = lipgloss.AdaptiveColor{
Dark: flexokiRed400,
Light: flexokiRed600,
}
theme.DiffAddedBgColor = lipgloss.AdaptiveColor{
Dark: "#1D2419", // Darker green background
Light: "#EFF2E2", // Light green background
}
theme.DiffRemovedBgColor = lipgloss.AdaptiveColor{
Dark: "#241919", // Darker red background
Light: "#F2E2E2", // Light red background
}
theme.DiffContextBgColor = lipgloss.AdaptiveColor{
Dark: flexokiBlack,
Light: flexokiPaper,
}
theme.DiffLineNumberColor = lipgloss.AdaptiveColor{
Dark: flexokiBase700,
Light: flexokiBase500,
}
theme.DiffAddedLineNumberBgColor = lipgloss.AdaptiveColor{
Dark: "#1A2017", // Slightly darker green
Light: "#E5EBD9", // Light green
}
theme.DiffRemovedLineNumberBgColor = lipgloss.AdaptiveColor{
Dark: "#201717", // Slightly darker red
Light: "#EBD9D9", // Light red
}
// Markdown colors
theme.MarkdownTextColor = lipgloss.AdaptiveColor{
Dark: flexokiBase300,
Light: flexokiBase600,
}
theme.MarkdownHeadingColor = lipgloss.AdaptiveColor{
Dark: flexokiYellow400,
Light: flexokiYellow600,
}
theme.MarkdownLinkColor = lipgloss.AdaptiveColor{
Dark: flexokiCyan400,
Light: flexokiCyan600,
}
theme.MarkdownLinkTextColor = lipgloss.AdaptiveColor{
Dark: flexokiMagenta400,
Light: flexokiMagenta600,
}
theme.MarkdownCodeColor = lipgloss.AdaptiveColor{
Dark: flexokiGreen400,
Light: flexokiGreen600,
}
theme.MarkdownBlockQuoteColor = lipgloss.AdaptiveColor{
Dark: flexokiCyan400,
Light: flexokiCyan600,
}
theme.MarkdownEmphColor = lipgloss.AdaptiveColor{
Dark: flexokiYellow400,
Light: flexokiYellow600,
}
theme.MarkdownStrongColor = lipgloss.AdaptiveColor{
Dark: flexokiOrange400,
Light: flexokiOrange600,
}
theme.MarkdownHorizontalRuleColor = lipgloss.AdaptiveColor{
Dark: flexokiBase800,
Light: flexokiBase200,
}
theme.MarkdownListItemColor = lipgloss.AdaptiveColor{
Dark: flexokiBlue400,
Light: flexokiBlue600,
}
theme.MarkdownListEnumerationColor = lipgloss.AdaptiveColor{
Dark: flexokiBlue400,
Light: flexokiBlue600,
}
theme.MarkdownImageColor = lipgloss.AdaptiveColor{
Dark: flexokiPurple400,
Light: flexokiPurple600,
}
theme.MarkdownImageTextColor = lipgloss.AdaptiveColor{
Dark: flexokiMagenta400,
Light: flexokiMagenta600,
}
theme.MarkdownCodeBlockColor = lipgloss.AdaptiveColor{
Dark: flexokiBase300,
Light: flexokiBase600,
}
// Syntax highlighting colors (based on Flexoki's mappings)
theme.SyntaxCommentColor = lipgloss.AdaptiveColor{
Dark: flexokiBase700, // tx-3
Light: flexokiBase300, // tx-3
}
theme.SyntaxKeywordColor = lipgloss.AdaptiveColor{
Dark: flexokiGreen400, // gr
Light: flexokiGreen600, // gr
}
theme.SyntaxFunctionColor = lipgloss.AdaptiveColor{
Dark: flexokiOrange400, // or
Light: flexokiOrange600, // or
}
theme.SyntaxVariableColor = lipgloss.AdaptiveColor{
Dark: flexokiBlue400, // bl
Light: flexokiBlue600, // bl
}
theme.SyntaxStringColor = lipgloss.AdaptiveColor{
Dark: flexokiCyan400, // cy
Light: flexokiCyan600, // cy
}
theme.SyntaxNumberColor = lipgloss.AdaptiveColor{
Dark: flexokiPurple400, // pu
Light: flexokiPurple600, // pu
}
theme.SyntaxTypeColor = lipgloss.AdaptiveColor{
Dark: flexokiYellow400, // ye
Light: flexokiYellow600, // ye
}
theme.SyntaxOperatorColor = lipgloss.AdaptiveColor{
Dark: flexokiBase500, // tx-2
Light: flexokiBase500, // tx-2
}
theme.SyntaxPunctuationColor = lipgloss.AdaptiveColor{
Dark: flexokiBase500, // tx-2
Light: flexokiBase500, // tx-2
}
return theme
}
func init() {
// Register the Flexoki theme with the theme manager
RegisterTheme("flexoki", NewFlexokiTheme())
}
+302
View File
@@ -0,0 +1,302 @@
package theme
import (
"github.com/charmbracelet/lipgloss"
)
// Gruvbox color palette constants
const (
// Dark theme colors
gruvboxDarkBg0 = "#282828"
gruvboxDarkBg0Soft = "#32302f"
gruvboxDarkBg1 = "#3c3836"
gruvboxDarkBg2 = "#504945"
gruvboxDarkBg3 = "#665c54"
gruvboxDarkBg4 = "#7c6f64"
gruvboxDarkFg0 = "#fbf1c7"
gruvboxDarkFg1 = "#ebdbb2"
gruvboxDarkFg2 = "#d5c4a1"
gruvboxDarkFg3 = "#bdae93"
gruvboxDarkFg4 = "#a89984"
gruvboxDarkGray = "#928374"
gruvboxDarkRed = "#cc241d"
gruvboxDarkRedBright = "#fb4934"
gruvboxDarkGreen = "#98971a"
gruvboxDarkGreenBright = "#b8bb26"
gruvboxDarkYellow = "#d79921"
gruvboxDarkYellowBright = "#fabd2f"
gruvboxDarkBlue = "#458588"
gruvboxDarkBlueBright = "#83a598"
gruvboxDarkPurple = "#b16286"
gruvboxDarkPurpleBright = "#d3869b"
gruvboxDarkAqua = "#689d6a"
gruvboxDarkAquaBright = "#8ec07c"
gruvboxDarkOrange = "#d65d0e"
gruvboxDarkOrangeBright = "#fe8019"
// Light theme colors
gruvboxLightBg0 = "#fbf1c7"
gruvboxLightBg0Soft = "#f2e5bc"
gruvboxLightBg1 = "#ebdbb2"
gruvboxLightBg2 = "#d5c4a1"
gruvboxLightBg3 = "#bdae93"
gruvboxLightBg4 = "#a89984"
gruvboxLightFg0 = "#282828"
gruvboxLightFg1 = "#3c3836"
gruvboxLightFg2 = "#504945"
gruvboxLightFg3 = "#665c54"
gruvboxLightFg4 = "#7c6f64"
gruvboxLightGray = "#928374"
gruvboxLightRed = "#9d0006"
gruvboxLightRedBright = "#cc241d"
gruvboxLightGreen = "#79740e"
gruvboxLightGreenBright = "#98971a"
gruvboxLightYellow = "#b57614"
gruvboxLightYellowBright = "#d79921"
gruvboxLightBlue = "#076678"
gruvboxLightBlueBright = "#458588"
gruvboxLightPurple = "#8f3f71"
gruvboxLightPurpleBright = "#b16286"
gruvboxLightAqua = "#427b58"
gruvboxLightAquaBright = "#689d6a"
gruvboxLightOrange = "#af3a03"
gruvboxLightOrangeBright = "#d65d0e"
)
// GruvboxTheme implements the Theme interface with Gruvbox colors.
// It provides both dark and light variants.
type GruvboxTheme struct {
BaseTheme
}
// NewGruvboxTheme creates a new instance of the Gruvbox theme.
func NewGruvboxTheme() *GruvboxTheme {
theme := &GruvboxTheme{}
// Base colors
theme.PrimaryColor = lipgloss.AdaptiveColor{
Dark: gruvboxDarkBlueBright,
Light: gruvboxLightBlueBright,
}
theme.SecondaryColor = lipgloss.AdaptiveColor{
Dark: gruvboxDarkPurpleBright,
Light: gruvboxLightPurpleBright,
}
theme.AccentColor = lipgloss.AdaptiveColor{
Dark: gruvboxDarkOrangeBright,
Light: gruvboxLightOrangeBright,
}
// Status colors
theme.ErrorColor = lipgloss.AdaptiveColor{
Dark: gruvboxDarkRedBright,
Light: gruvboxLightRedBright,
}
theme.WarningColor = lipgloss.AdaptiveColor{
Dark: gruvboxDarkYellowBright,
Light: gruvboxLightYellowBright,
}
theme.SuccessColor = lipgloss.AdaptiveColor{
Dark: gruvboxDarkGreenBright,
Light: gruvboxLightGreenBright,
}
theme.InfoColor = lipgloss.AdaptiveColor{
Dark: gruvboxDarkBlueBright,
Light: gruvboxLightBlueBright,
}
// Text colors
theme.TextColor = lipgloss.AdaptiveColor{
Dark: gruvboxDarkFg1,
Light: gruvboxLightFg1,
}
theme.TextMutedColor = lipgloss.AdaptiveColor{
Dark: gruvboxDarkFg4,
Light: gruvboxLightFg4,
}
theme.TextEmphasizedColor = lipgloss.AdaptiveColor{
Dark: gruvboxDarkYellowBright,
Light: gruvboxLightYellowBright,
}
// Background colors
theme.BackgroundColor = lipgloss.AdaptiveColor{
Dark: gruvboxDarkBg0,
Light: gruvboxLightBg0,
}
theme.BackgroundSecondaryColor = lipgloss.AdaptiveColor{
Dark: gruvboxDarkBg1,
Light: gruvboxLightBg1,
}
theme.BackgroundDarkerColor = lipgloss.AdaptiveColor{
Dark: gruvboxDarkBg0Soft,
Light: gruvboxLightBg0Soft,
}
// Border colors
theme.BorderNormalColor = lipgloss.AdaptiveColor{
Dark: gruvboxDarkBg2,
Light: gruvboxLightBg2,
}
theme.BorderFocusedColor = lipgloss.AdaptiveColor{
Dark: gruvboxDarkBlueBright,
Light: gruvboxLightBlueBright,
}
theme.BorderDimColor = lipgloss.AdaptiveColor{
Dark: gruvboxDarkBg1,
Light: gruvboxLightBg1,
}
// Diff view colors
theme.DiffAddedColor = lipgloss.AdaptiveColor{
Dark: gruvboxDarkGreenBright,
Light: gruvboxLightGreenBright,
}
theme.DiffRemovedColor = lipgloss.AdaptiveColor{
Dark: gruvboxDarkRedBright,
Light: gruvboxLightRedBright,
}
theme.DiffContextColor = lipgloss.AdaptiveColor{
Dark: gruvboxDarkFg4,
Light: gruvboxLightFg4,
}
theme.DiffHunkHeaderColor = lipgloss.AdaptiveColor{
Dark: gruvboxDarkFg3,
Light: gruvboxLightFg3,
}
theme.DiffHighlightAddedColor = lipgloss.AdaptiveColor{
Dark: gruvboxDarkGreenBright,
Light: gruvboxLightGreenBright,
}
theme.DiffHighlightRemovedColor = lipgloss.AdaptiveColor{
Dark: gruvboxDarkRedBright,
Light: gruvboxLightRedBright,
}
theme.DiffAddedBgColor = lipgloss.AdaptiveColor{
Dark: "#3C4C3C", // Darker green background
Light: "#E8F5E9", // Light green background
}
theme.DiffRemovedBgColor = lipgloss.AdaptiveColor{
Dark: "#4C3C3C", // Darker red background
Light: "#FFEBEE", // Light red background
}
theme.DiffContextBgColor = lipgloss.AdaptiveColor{
Dark: gruvboxDarkBg0,
Light: gruvboxLightBg0,
}
theme.DiffLineNumberColor = lipgloss.AdaptiveColor{
Dark: gruvboxDarkFg4,
Light: gruvboxLightFg4,
}
theme.DiffAddedLineNumberBgColor = lipgloss.AdaptiveColor{
Dark: "#32432F", // Slightly darker green
Light: "#C8E6C9", // Light green
}
theme.DiffRemovedLineNumberBgColor = lipgloss.AdaptiveColor{
Dark: "#43322F", // Slightly darker red
Light: "#FFCDD2", // Light red
}
// Markdown colors
theme.MarkdownTextColor = lipgloss.AdaptiveColor{
Dark: gruvboxDarkFg1,
Light: gruvboxLightFg1,
}
theme.MarkdownHeadingColor = lipgloss.AdaptiveColor{
Dark: gruvboxDarkYellowBright,
Light: gruvboxLightYellowBright,
}
theme.MarkdownLinkColor = lipgloss.AdaptiveColor{
Dark: gruvboxDarkBlueBright,
Light: gruvboxLightBlueBright,
}
theme.MarkdownLinkTextColor = lipgloss.AdaptiveColor{
Dark: gruvboxDarkAquaBright,
Light: gruvboxLightAquaBright,
}
theme.MarkdownCodeColor = lipgloss.AdaptiveColor{
Dark: gruvboxDarkGreenBright,
Light: gruvboxLightGreenBright,
}
theme.MarkdownBlockQuoteColor = lipgloss.AdaptiveColor{
Dark: gruvboxDarkAquaBright,
Light: gruvboxLightAquaBright,
}
theme.MarkdownEmphColor = lipgloss.AdaptiveColor{
Dark: gruvboxDarkYellowBright,
Light: gruvboxLightYellowBright,
}
theme.MarkdownStrongColor = lipgloss.AdaptiveColor{
Dark: gruvboxDarkOrangeBright,
Light: gruvboxLightOrangeBright,
}
theme.MarkdownHorizontalRuleColor = lipgloss.AdaptiveColor{
Dark: gruvboxDarkBg3,
Light: gruvboxLightBg3,
}
theme.MarkdownListItemColor = lipgloss.AdaptiveColor{
Dark: gruvboxDarkBlueBright,
Light: gruvboxLightBlueBright,
}
theme.MarkdownListEnumerationColor = lipgloss.AdaptiveColor{
Dark: gruvboxDarkBlueBright,
Light: gruvboxLightBlueBright,
}
theme.MarkdownImageColor = lipgloss.AdaptiveColor{
Dark: gruvboxDarkPurpleBright,
Light: gruvboxLightPurpleBright,
}
theme.MarkdownImageTextColor = lipgloss.AdaptiveColor{
Dark: gruvboxDarkAquaBright,
Light: gruvboxLightAquaBright,
}
theme.MarkdownCodeBlockColor = lipgloss.AdaptiveColor{
Dark: gruvboxDarkFg1,
Light: gruvboxLightFg1,
}
// Syntax highlighting colors
theme.SyntaxCommentColor = lipgloss.AdaptiveColor{
Dark: gruvboxDarkGray,
Light: gruvboxLightGray,
}
theme.SyntaxKeywordColor = lipgloss.AdaptiveColor{
Dark: gruvboxDarkRedBright,
Light: gruvboxLightRedBright,
}
theme.SyntaxFunctionColor = lipgloss.AdaptiveColor{
Dark: gruvboxDarkGreenBright,
Light: gruvboxLightGreenBright,
}
theme.SyntaxVariableColor = lipgloss.AdaptiveColor{
Dark: gruvboxDarkBlueBright,
Light: gruvboxLightBlueBright,
}
theme.SyntaxStringColor = lipgloss.AdaptiveColor{
Dark: gruvboxDarkYellowBright,
Light: gruvboxLightYellowBright,
}
theme.SyntaxNumberColor = lipgloss.AdaptiveColor{
Dark: gruvboxDarkPurpleBright,
Light: gruvboxLightPurpleBright,
}
theme.SyntaxTypeColor = lipgloss.AdaptiveColor{
Dark: gruvboxDarkYellow,
Light: gruvboxLightYellow,
}
theme.SyntaxOperatorColor = lipgloss.AdaptiveColor{
Dark: gruvboxDarkAquaBright,
Light: gruvboxLightAquaBright,
}
theme.SyntaxPunctuationColor = lipgloss.AdaptiveColor{
Dark: gruvboxDarkFg1,
Light: gruvboxLightFg1,
}
return theme
}
func init() {
// Register the Gruvbox theme with the theme manager
RegisterTheme("gruvbox", NewGruvboxTheme())
}
+118
View File
@@ -0,0 +1,118 @@
package theme
import (
"fmt"
"slices"
"strings"
"sync"
"github.com/alecthomas/chroma/v2/styles"
"github.com/opencode-ai/opencode/internal/config"
"github.com/opencode-ai/opencode/internal/logging"
)
// Manager handles theme registration, selection, and retrieval.
// It maintains a registry of available themes and tracks the currently active theme.
type Manager struct {
themes map[string]Theme
currentName string
mu sync.RWMutex
}
// Global instance of the theme manager
var globalManager = &Manager{
themes: make(map[string]Theme),
currentName: "",
}
// RegisterTheme adds a new theme to the registry.
// If this is the first theme registered, it becomes the default.
func RegisterTheme(name string, theme Theme) {
globalManager.mu.Lock()
defer globalManager.mu.Unlock()
globalManager.themes[name] = theme
// If this is the first theme, make it the default
if globalManager.currentName == "" {
globalManager.currentName = name
}
}
// SetTheme changes the active theme to the one with the specified name.
// Returns an error if the theme doesn't exist.
func SetTheme(name string) error {
globalManager.mu.Lock()
defer globalManager.mu.Unlock()
delete(styles.Registry, "charm")
if _, exists := globalManager.themes[name]; !exists {
return fmt.Errorf("theme '%s' not found", name)
}
globalManager.currentName = name
// Update the config file using viper
if err := updateConfigTheme(name); err != nil {
// Log the error but don't fail the theme change
logging.Warn("Warning: Failed to update config file with new theme", "err", err)
}
return nil
}
// CurrentTheme returns the currently active theme.
// If no theme is set, it returns nil.
func CurrentTheme() Theme {
globalManager.mu.RLock()
defer globalManager.mu.RUnlock()
if globalManager.currentName == "" {
return nil
}
return globalManager.themes[globalManager.currentName]
}
// CurrentThemeName returns the name of the currently active theme.
func CurrentThemeName() string {
globalManager.mu.RLock()
defer globalManager.mu.RUnlock()
return globalManager.currentName
}
// AvailableThemes returns a list of all registered theme names.
func AvailableThemes() []string {
globalManager.mu.RLock()
defer globalManager.mu.RUnlock()
names := make([]string, 0, len(globalManager.themes))
for name := range globalManager.themes {
names = append(names, name)
}
slices.SortFunc(names, func(a, b string) int {
if a == "opencode" {
return -1
} else if b == "opencode" {
return 1
}
return strings.Compare(a, b)
})
return names
}
// GetTheme returns a specific theme by name.
// Returns nil if the theme doesn't exist.
func GetTheme(name string) Theme {
globalManager.mu.RLock()
defer globalManager.mu.RUnlock()
return globalManager.themes[name]
}
// updateConfigTheme updates the theme setting in the configuration file
func updateConfigTheme(themeName string) error {
// Use the config package to update the theme
return config.UpdateTheme(themeName)
}
+273
View File
@@ -0,0 +1,273 @@
package theme
import (
"github.com/charmbracelet/lipgloss"
)
// MonokaiProTheme implements the Theme interface with Monokai Pro colors.
// It provides both dark and light variants.
type MonokaiProTheme struct {
BaseTheme
}
// NewMonokaiProTheme creates a new instance of the Monokai Pro theme.
func NewMonokaiProTheme() *MonokaiProTheme {
// Monokai Pro color palette (dark mode)
darkBackground := "#2d2a2e"
darkCurrentLine := "#403e41"
darkSelection := "#5b595c"
darkForeground := "#fcfcfa"
darkComment := "#727072"
darkRed := "#ff6188"
darkOrange := "#fc9867"
darkYellow := "#ffd866"
darkGreen := "#a9dc76"
darkCyan := "#78dce8"
darkBlue := "#ab9df2"
darkPurple := "#ab9df2"
darkBorder := "#403e41"
// Light mode colors (adapted from dark)
lightBackground := "#fafafa"
lightCurrentLine := "#f0f0f0"
lightSelection := "#e5e5e6"
lightForeground := "#2d2a2e"
lightComment := "#939293"
lightRed := "#f92672"
lightOrange := "#fd971f"
lightYellow := "#e6db74"
lightGreen := "#9bca65"
lightCyan := "#66d9ef"
lightBlue := "#7e75db"
lightPurple := "#ae81ff"
lightBorder := "#d3d3d3"
theme := &MonokaiProTheme{}
// Base colors
theme.PrimaryColor = lipgloss.AdaptiveColor{
Dark: darkCyan,
Light: lightCyan,
}
theme.SecondaryColor = lipgloss.AdaptiveColor{
Dark: darkPurple,
Light: lightPurple,
}
theme.AccentColor = lipgloss.AdaptiveColor{
Dark: darkOrange,
Light: lightOrange,
}
// Status colors
theme.ErrorColor = lipgloss.AdaptiveColor{
Dark: darkRed,
Light: lightRed,
}
theme.WarningColor = lipgloss.AdaptiveColor{
Dark: darkOrange,
Light: lightOrange,
}
theme.SuccessColor = lipgloss.AdaptiveColor{
Dark: darkGreen,
Light: lightGreen,
}
theme.InfoColor = lipgloss.AdaptiveColor{
Dark: darkBlue,
Light: lightBlue,
}
// Text colors
theme.TextColor = lipgloss.AdaptiveColor{
Dark: darkForeground,
Light: lightForeground,
}
theme.TextMutedColor = lipgloss.AdaptiveColor{
Dark: darkComment,
Light: lightComment,
}
theme.TextEmphasizedColor = lipgloss.AdaptiveColor{
Dark: darkYellow,
Light: lightYellow,
}
// Background colors
theme.BackgroundColor = lipgloss.AdaptiveColor{
Dark: darkBackground,
Light: lightBackground,
}
theme.BackgroundSecondaryColor = lipgloss.AdaptiveColor{
Dark: darkCurrentLine,
Light: lightCurrentLine,
}
theme.BackgroundDarkerColor = lipgloss.AdaptiveColor{
Dark: "#221f22", // Slightly darker than background
Light: "#ffffff", // Slightly lighter than background
}
// Border colors
theme.BorderNormalColor = lipgloss.AdaptiveColor{
Dark: darkBorder,
Light: lightBorder,
}
theme.BorderFocusedColor = lipgloss.AdaptiveColor{
Dark: darkCyan,
Light: lightCyan,
}
theme.BorderDimColor = lipgloss.AdaptiveColor{
Dark: darkSelection,
Light: lightSelection,
}
// Diff view colors
theme.DiffAddedColor = lipgloss.AdaptiveColor{
Dark: "#a9dc76",
Light: "#9bca65",
}
theme.DiffRemovedColor = lipgloss.AdaptiveColor{
Dark: "#ff6188",
Light: "#f92672",
}
theme.DiffContextColor = lipgloss.AdaptiveColor{
Dark: "#a0a0a0",
Light: "#757575",
}
theme.DiffHunkHeaderColor = lipgloss.AdaptiveColor{
Dark: "#a0a0a0",
Light: "#757575",
}
theme.DiffHighlightAddedColor = lipgloss.AdaptiveColor{
Dark: "#c2e7a9",
Light: "#c5e0b4",
}
theme.DiffHighlightRemovedColor = lipgloss.AdaptiveColor{
Dark: "#ff8ca6",
Light: "#ffb3c8",
}
theme.DiffAddedBgColor = lipgloss.AdaptiveColor{
Dark: "#3a4a35",
Light: "#e8f5e9",
}
theme.DiffRemovedBgColor = lipgloss.AdaptiveColor{
Dark: "#4a3439",
Light: "#ffebee",
}
theme.DiffContextBgColor = lipgloss.AdaptiveColor{
Dark: darkBackground,
Light: lightBackground,
}
theme.DiffLineNumberColor = lipgloss.AdaptiveColor{
Dark: "#888888",
Light: "#9e9e9e",
}
theme.DiffAddedLineNumberBgColor = lipgloss.AdaptiveColor{
Dark: "#2d3a28",
Light: "#c8e6c9",
}
theme.DiffRemovedLineNumberBgColor = lipgloss.AdaptiveColor{
Dark: "#3d2a2e",
Light: "#ffcdd2",
}
// Markdown colors
theme.MarkdownTextColor = lipgloss.AdaptiveColor{
Dark: darkForeground,
Light: lightForeground,
}
theme.MarkdownHeadingColor = lipgloss.AdaptiveColor{
Dark: darkPurple,
Light: lightPurple,
}
theme.MarkdownLinkColor = lipgloss.AdaptiveColor{
Dark: darkCyan,
Light: lightCyan,
}
theme.MarkdownLinkTextColor = lipgloss.AdaptiveColor{
Dark: darkBlue,
Light: lightBlue,
}
theme.MarkdownCodeColor = lipgloss.AdaptiveColor{
Dark: darkGreen,
Light: lightGreen,
}
theme.MarkdownBlockQuoteColor = lipgloss.AdaptiveColor{
Dark: darkYellow,
Light: lightYellow,
}
theme.MarkdownEmphColor = lipgloss.AdaptiveColor{
Dark: darkYellow,
Light: lightYellow,
}
theme.MarkdownStrongColor = lipgloss.AdaptiveColor{
Dark: darkOrange,
Light: lightOrange,
}
theme.MarkdownHorizontalRuleColor = lipgloss.AdaptiveColor{
Dark: darkComment,
Light: lightComment,
}
theme.MarkdownListItemColor = lipgloss.AdaptiveColor{
Dark: darkCyan,
Light: lightCyan,
}
theme.MarkdownListEnumerationColor = lipgloss.AdaptiveColor{
Dark: darkBlue,
Light: lightBlue,
}
theme.MarkdownImageColor = lipgloss.AdaptiveColor{
Dark: darkCyan,
Light: lightCyan,
}
theme.MarkdownImageTextColor = lipgloss.AdaptiveColor{
Dark: darkBlue,
Light: lightBlue,
}
theme.MarkdownCodeBlockColor = lipgloss.AdaptiveColor{
Dark: darkForeground,
Light: lightForeground,
}
// Syntax highlighting colors
theme.SyntaxCommentColor = lipgloss.AdaptiveColor{
Dark: darkComment,
Light: lightComment,
}
theme.SyntaxKeywordColor = lipgloss.AdaptiveColor{
Dark: darkRed,
Light: lightRed,
}
theme.SyntaxFunctionColor = lipgloss.AdaptiveColor{
Dark: darkGreen,
Light: lightGreen,
}
theme.SyntaxVariableColor = lipgloss.AdaptiveColor{
Dark: darkForeground,
Light: lightForeground,
}
theme.SyntaxStringColor = lipgloss.AdaptiveColor{
Dark: darkYellow,
Light: lightYellow,
}
theme.SyntaxNumberColor = lipgloss.AdaptiveColor{
Dark: darkPurple,
Light: lightPurple,
}
theme.SyntaxTypeColor = lipgloss.AdaptiveColor{
Dark: darkBlue,
Light: lightBlue,
}
theme.SyntaxOperatorColor = lipgloss.AdaptiveColor{
Dark: darkCyan,
Light: lightCyan,
}
theme.SyntaxPunctuationColor = lipgloss.AdaptiveColor{
Dark: darkForeground,
Light: lightForeground,
}
return theme
}
func init() {
// Register the Monokai Pro theme with the theme manager
RegisterTheme("monokai", NewMonokaiProTheme())
}
+274
View File
@@ -0,0 +1,274 @@
package theme
import (
"github.com/charmbracelet/lipgloss"
)
// OneDarkTheme implements the Theme interface with Atom's One Dark colors.
// It provides both dark and light variants.
type OneDarkTheme struct {
BaseTheme
}
// NewOneDarkTheme creates a new instance of the One Dark theme.
func NewOneDarkTheme() *OneDarkTheme {
// One Dark color palette
// Dark mode colors from Atom One Dark
darkBackground := "#282c34"
darkCurrentLine := "#2c313c"
darkSelection := "#3e4451"
darkForeground := "#abb2bf"
darkComment := "#5c6370"
darkRed := "#e06c75"
darkOrange := "#d19a66"
darkYellow := "#e5c07b"
darkGreen := "#98c379"
darkCyan := "#56b6c2"
darkBlue := "#61afef"
darkPurple := "#c678dd"
darkBorder := "#3b4048"
// Light mode colors from Atom One Light
lightBackground := "#fafafa"
lightCurrentLine := "#f0f0f0"
lightSelection := "#e5e5e6"
lightForeground := "#383a42"
lightComment := "#a0a1a7"
lightRed := "#e45649"
lightOrange := "#da8548"
lightYellow := "#c18401"
lightGreen := "#50a14f"
lightCyan := "#0184bc"
lightBlue := "#4078f2"
lightPurple := "#a626a4"
lightBorder := "#d3d3d3"
theme := &OneDarkTheme{}
// Base colors
theme.PrimaryColor = lipgloss.AdaptiveColor{
Dark: darkBlue,
Light: lightBlue,
}
theme.SecondaryColor = lipgloss.AdaptiveColor{
Dark: darkPurple,
Light: lightPurple,
}
theme.AccentColor = lipgloss.AdaptiveColor{
Dark: darkOrange,
Light: lightOrange,
}
// Status colors
theme.ErrorColor = lipgloss.AdaptiveColor{
Dark: darkRed,
Light: lightRed,
}
theme.WarningColor = lipgloss.AdaptiveColor{
Dark: darkOrange,
Light: lightOrange,
}
theme.SuccessColor = lipgloss.AdaptiveColor{
Dark: darkGreen,
Light: lightGreen,
}
theme.InfoColor = lipgloss.AdaptiveColor{
Dark: darkBlue,
Light: lightBlue,
}
// Text colors
theme.TextColor = lipgloss.AdaptiveColor{
Dark: darkForeground,
Light: lightForeground,
}
theme.TextMutedColor = lipgloss.AdaptiveColor{
Dark: darkComment,
Light: lightComment,
}
theme.TextEmphasizedColor = lipgloss.AdaptiveColor{
Dark: darkYellow,
Light: lightYellow,
}
// Background colors
theme.BackgroundColor = lipgloss.AdaptiveColor{
Dark: darkBackground,
Light: lightBackground,
}
theme.BackgroundSecondaryColor = lipgloss.AdaptiveColor{
Dark: darkCurrentLine,
Light: lightCurrentLine,
}
theme.BackgroundDarkerColor = lipgloss.AdaptiveColor{
Dark: "#21252b", // Slightly darker than background
Light: "#ffffff", // Slightly lighter than background
}
// Border colors
theme.BorderNormalColor = lipgloss.AdaptiveColor{
Dark: darkBorder,
Light: lightBorder,
}
theme.BorderFocusedColor = lipgloss.AdaptiveColor{
Dark: darkBlue,
Light: lightBlue,
}
theme.BorderDimColor = lipgloss.AdaptiveColor{
Dark: darkSelection,
Light: lightSelection,
}
// Diff view colors
theme.DiffAddedColor = lipgloss.AdaptiveColor{
Dark: "#478247",
Light: "#2E7D32",
}
theme.DiffRemovedColor = lipgloss.AdaptiveColor{
Dark: "#7C4444",
Light: "#C62828",
}
theme.DiffContextColor = lipgloss.AdaptiveColor{
Dark: "#a0a0a0",
Light: "#757575",
}
theme.DiffHunkHeaderColor = lipgloss.AdaptiveColor{
Dark: "#a0a0a0",
Light: "#757575",
}
theme.DiffHighlightAddedColor = lipgloss.AdaptiveColor{
Dark: "#DAFADA",
Light: "#A5D6A7",
}
theme.DiffHighlightRemovedColor = lipgloss.AdaptiveColor{
Dark: "#FADADD",
Light: "#EF9A9A",
}
theme.DiffAddedBgColor = lipgloss.AdaptiveColor{
Dark: "#303A30",
Light: "#E8F5E9",
}
theme.DiffRemovedBgColor = lipgloss.AdaptiveColor{
Dark: "#3A3030",
Light: "#FFEBEE",
}
theme.DiffContextBgColor = lipgloss.AdaptiveColor{
Dark: darkBackground,
Light: lightBackground,
}
theme.DiffLineNumberColor = lipgloss.AdaptiveColor{
Dark: "#888888",
Light: "#9E9E9E",
}
theme.DiffAddedLineNumberBgColor = lipgloss.AdaptiveColor{
Dark: "#293229",
Light: "#C8E6C9",
}
theme.DiffRemovedLineNumberBgColor = lipgloss.AdaptiveColor{
Dark: "#332929",
Light: "#FFCDD2",
}
// Markdown colors
theme.MarkdownTextColor = lipgloss.AdaptiveColor{
Dark: darkForeground,
Light: lightForeground,
}
theme.MarkdownHeadingColor = lipgloss.AdaptiveColor{
Dark: darkPurple,
Light: lightPurple,
}
theme.MarkdownLinkColor = lipgloss.AdaptiveColor{
Dark: darkBlue,
Light: lightBlue,
}
theme.MarkdownLinkTextColor = lipgloss.AdaptiveColor{
Dark: darkCyan,
Light: lightCyan,
}
theme.MarkdownCodeColor = lipgloss.AdaptiveColor{
Dark: darkGreen,
Light: lightGreen,
}
theme.MarkdownBlockQuoteColor = lipgloss.AdaptiveColor{
Dark: darkYellow,
Light: lightYellow,
}
theme.MarkdownEmphColor = lipgloss.AdaptiveColor{
Dark: darkYellow,
Light: lightYellow,
}
theme.MarkdownStrongColor = lipgloss.AdaptiveColor{
Dark: darkOrange,
Light: lightOrange,
}
theme.MarkdownHorizontalRuleColor = lipgloss.AdaptiveColor{
Dark: darkComment,
Light: lightComment,
}
theme.MarkdownListItemColor = lipgloss.AdaptiveColor{
Dark: darkBlue,
Light: lightBlue,
}
theme.MarkdownListEnumerationColor = lipgloss.AdaptiveColor{
Dark: darkCyan,
Light: lightCyan,
}
theme.MarkdownImageColor = lipgloss.AdaptiveColor{
Dark: darkBlue,
Light: lightBlue,
}
theme.MarkdownImageTextColor = lipgloss.AdaptiveColor{
Dark: darkCyan,
Light: lightCyan,
}
theme.MarkdownCodeBlockColor = lipgloss.AdaptiveColor{
Dark: darkForeground,
Light: lightForeground,
}
// Syntax highlighting colors
theme.SyntaxCommentColor = lipgloss.AdaptiveColor{
Dark: darkComment,
Light: lightComment,
}
theme.SyntaxKeywordColor = lipgloss.AdaptiveColor{
Dark: darkPurple,
Light: lightPurple,
}
theme.SyntaxFunctionColor = lipgloss.AdaptiveColor{
Dark: darkBlue,
Light: lightBlue,
}
theme.SyntaxVariableColor = lipgloss.AdaptiveColor{
Dark: darkRed,
Light: lightRed,
}
theme.SyntaxStringColor = lipgloss.AdaptiveColor{
Dark: darkGreen,
Light: lightGreen,
}
theme.SyntaxNumberColor = lipgloss.AdaptiveColor{
Dark: darkOrange,
Light: lightOrange,
}
theme.SyntaxTypeColor = lipgloss.AdaptiveColor{
Dark: darkYellow,
Light: lightYellow,
}
theme.SyntaxOperatorColor = lipgloss.AdaptiveColor{
Dark: darkCyan,
Light: lightCyan,
}
theme.SyntaxPunctuationColor = lipgloss.AdaptiveColor{
Dark: darkForeground,
Light: lightForeground,
}
return theme
}
func init() {
// Register the One Dark theme with the theme manager
RegisterTheme("onedark", NewOneDarkTheme())
}
+277
View File
@@ -0,0 +1,277 @@
package theme
import (
"github.com/charmbracelet/lipgloss"
)
// OpenCodeTheme implements the Theme interface with OpenCode brand colors.
// It provides both dark and light variants.
type OpenCodeTheme struct {
BaseTheme
}
// NewOpenCodeTheme creates a new instance of the OpenCode theme.
func NewOpenCodeTheme() *OpenCodeTheme {
// OpenCode color palette
// Dark mode colors
darkBackground := "#212121"
darkCurrentLine := "#252525"
darkSelection := "#303030"
darkForeground := "#e0e0e0"
darkComment := "#6a6a6a"
darkPrimary := "#fab283" // Primary orange/gold
darkSecondary := "#5c9cf5" // Secondary blue
darkAccent := "#9d7cd8" // Accent purple
darkRed := "#e06c75" // Error red
darkOrange := "#f5a742" // Warning orange
darkGreen := "#7fd88f" // Success green
darkCyan := "#56b6c2" // Info cyan
darkYellow := "#e5c07b" // Emphasized text
darkBorder := "#4b4c5c" // Border color
// Light mode colors
lightBackground := "#f8f8f8"
lightCurrentLine := "#f0f0f0"
lightSelection := "#e5e5e6"
lightForeground := "#2a2a2a"
lightComment := "#8a8a8a"
lightPrimary := "#3b7dd8" // Primary blue
lightSecondary := "#7b5bb6" // Secondary purple
lightAccent := "#d68c27" // Accent orange/gold
lightRed := "#d1383d" // Error red
lightOrange := "#d68c27" // Warning orange
lightGreen := "#3d9a57" // Success green
lightCyan := "#318795" // Info cyan
lightYellow := "#b0851f" // Emphasized text
lightBorder := "#d3d3d3" // Border color
theme := &OpenCodeTheme{}
// Base colors
theme.PrimaryColor = lipgloss.AdaptiveColor{
Dark: darkPrimary,
Light: lightPrimary,
}
theme.SecondaryColor = lipgloss.AdaptiveColor{
Dark: darkSecondary,
Light: lightSecondary,
}
theme.AccentColor = lipgloss.AdaptiveColor{
Dark: darkAccent,
Light: lightAccent,
}
// Status colors
theme.ErrorColor = lipgloss.AdaptiveColor{
Dark: darkRed,
Light: lightRed,
}
theme.WarningColor = lipgloss.AdaptiveColor{
Dark: darkOrange,
Light: lightOrange,
}
theme.SuccessColor = lipgloss.AdaptiveColor{
Dark: darkGreen,
Light: lightGreen,
}
theme.InfoColor = lipgloss.AdaptiveColor{
Dark: darkCyan,
Light: lightCyan,
}
// Text colors
theme.TextColor = lipgloss.AdaptiveColor{
Dark: darkForeground,
Light: lightForeground,
}
theme.TextMutedColor = lipgloss.AdaptiveColor{
Dark: darkComment,
Light: lightComment,
}
theme.TextEmphasizedColor = lipgloss.AdaptiveColor{
Dark: darkYellow,
Light: lightYellow,
}
// Background colors
theme.BackgroundColor = lipgloss.AdaptiveColor{
Dark: darkBackground,
Light: lightBackground,
}
theme.BackgroundSecondaryColor = lipgloss.AdaptiveColor{
Dark: darkCurrentLine,
Light: lightCurrentLine,
}
theme.BackgroundDarkerColor = lipgloss.AdaptiveColor{
Dark: "#121212", // Slightly darker than background
Light: "#ffffff", // Slightly lighter than background
}
// Border colors
theme.BorderNormalColor = lipgloss.AdaptiveColor{
Dark: darkBorder,
Light: lightBorder,
}
theme.BorderFocusedColor = lipgloss.AdaptiveColor{
Dark: darkPrimary,
Light: lightPrimary,
}
theme.BorderDimColor = lipgloss.AdaptiveColor{
Dark: darkSelection,
Light: lightSelection,
}
// Diff view colors
theme.DiffAddedColor = lipgloss.AdaptiveColor{
Dark: "#478247",
Light: "#2E7D32",
}
theme.DiffRemovedColor = lipgloss.AdaptiveColor{
Dark: "#7C4444",
Light: "#C62828",
}
theme.DiffContextColor = lipgloss.AdaptiveColor{
Dark: "#a0a0a0",
Light: "#757575",
}
theme.DiffHunkHeaderColor = lipgloss.AdaptiveColor{
Dark: "#a0a0a0",
Light: "#757575",
}
theme.DiffHighlightAddedColor = lipgloss.AdaptiveColor{
Dark: "#DAFADA",
Light: "#A5D6A7",
}
theme.DiffHighlightRemovedColor = lipgloss.AdaptiveColor{
Dark: "#FADADD",
Light: "#EF9A9A",
}
theme.DiffAddedBgColor = lipgloss.AdaptiveColor{
Dark: "#303A30",
Light: "#E8F5E9",
}
theme.DiffRemovedBgColor = lipgloss.AdaptiveColor{
Dark: "#3A3030",
Light: "#FFEBEE",
}
theme.DiffContextBgColor = lipgloss.AdaptiveColor{
Dark: darkBackground,
Light: lightBackground,
}
theme.DiffLineNumberColor = lipgloss.AdaptiveColor{
Dark: "#888888",
Light: "#9E9E9E",
}
theme.DiffAddedLineNumberBgColor = lipgloss.AdaptiveColor{
Dark: "#293229",
Light: "#C8E6C9",
}
theme.DiffRemovedLineNumberBgColor = lipgloss.AdaptiveColor{
Dark: "#332929",
Light: "#FFCDD2",
}
// Markdown colors
theme.MarkdownTextColor = lipgloss.AdaptiveColor{
Dark: darkForeground,
Light: lightForeground,
}
theme.MarkdownHeadingColor = lipgloss.AdaptiveColor{
Dark: darkSecondary,
Light: lightSecondary,
}
theme.MarkdownLinkColor = lipgloss.AdaptiveColor{
Dark: darkPrimary,
Light: lightPrimary,
}
theme.MarkdownLinkTextColor = lipgloss.AdaptiveColor{
Dark: darkCyan,
Light: lightCyan,
}
theme.MarkdownCodeColor = lipgloss.AdaptiveColor{
Dark: darkGreen,
Light: lightGreen,
}
theme.MarkdownBlockQuoteColor = lipgloss.AdaptiveColor{
Dark: darkYellow,
Light: lightYellow,
}
theme.MarkdownEmphColor = lipgloss.AdaptiveColor{
Dark: darkYellow,
Light: lightYellow,
}
theme.MarkdownStrongColor = lipgloss.AdaptiveColor{
Dark: darkAccent,
Light: lightAccent,
}
theme.MarkdownHorizontalRuleColor = lipgloss.AdaptiveColor{
Dark: darkComment,
Light: lightComment,
}
theme.MarkdownListItemColor = lipgloss.AdaptiveColor{
Dark: darkPrimary,
Light: lightPrimary,
}
theme.MarkdownListEnumerationColor = lipgloss.AdaptiveColor{
Dark: darkCyan,
Light: lightCyan,
}
theme.MarkdownImageColor = lipgloss.AdaptiveColor{
Dark: darkPrimary,
Light: lightPrimary,
}
theme.MarkdownImageTextColor = lipgloss.AdaptiveColor{
Dark: darkCyan,
Light: lightCyan,
}
theme.MarkdownCodeBlockColor = lipgloss.AdaptiveColor{
Dark: darkForeground,
Light: lightForeground,
}
// Syntax highlighting colors
theme.SyntaxCommentColor = lipgloss.AdaptiveColor{
Dark: darkComment,
Light: lightComment,
}
theme.SyntaxKeywordColor = lipgloss.AdaptiveColor{
Dark: darkSecondary,
Light: lightSecondary,
}
theme.SyntaxFunctionColor = lipgloss.AdaptiveColor{
Dark: darkPrimary,
Light: lightPrimary,
}
theme.SyntaxVariableColor = lipgloss.AdaptiveColor{
Dark: darkRed,
Light: lightRed,
}
theme.SyntaxStringColor = lipgloss.AdaptiveColor{
Dark: darkGreen,
Light: lightGreen,
}
theme.SyntaxNumberColor = lipgloss.AdaptiveColor{
Dark: darkAccent,
Light: lightAccent,
}
theme.SyntaxTypeColor = lipgloss.AdaptiveColor{
Dark: darkYellow,
Light: lightYellow,
}
theme.SyntaxOperatorColor = lipgloss.AdaptiveColor{
Dark: darkCyan,
Light: lightCyan,
}
theme.SyntaxPunctuationColor = lipgloss.AdaptiveColor{
Dark: darkForeground,
Light: lightForeground,
}
return theme
}
func init() {
// Register the OpenCode theme with the theme manager
RegisterTheme("opencode", NewOpenCodeTheme())
}
+208
View File
@@ -0,0 +1,208 @@
package theme
import (
"github.com/charmbracelet/lipgloss"
)
// Theme defines the interface for all UI themes in the application.
// All colors must be defined as lipgloss.AdaptiveColor to support
// both light and dark terminal backgrounds.
type Theme interface {
// Base colors
Primary() lipgloss.AdaptiveColor
Secondary() lipgloss.AdaptiveColor
Accent() lipgloss.AdaptiveColor
// Status colors
Error() lipgloss.AdaptiveColor
Warning() lipgloss.AdaptiveColor
Success() lipgloss.AdaptiveColor
Info() lipgloss.AdaptiveColor
// Text colors
Text() lipgloss.AdaptiveColor
TextMuted() lipgloss.AdaptiveColor
TextEmphasized() lipgloss.AdaptiveColor
// Background colors
Background() lipgloss.AdaptiveColor
BackgroundSecondary() lipgloss.AdaptiveColor
BackgroundDarker() lipgloss.AdaptiveColor
// Border colors
BorderNormal() lipgloss.AdaptiveColor
BorderFocused() lipgloss.AdaptiveColor
BorderDim() lipgloss.AdaptiveColor
// Diff view colors
DiffAdded() lipgloss.AdaptiveColor
DiffRemoved() lipgloss.AdaptiveColor
DiffContext() lipgloss.AdaptiveColor
DiffHunkHeader() lipgloss.AdaptiveColor
DiffHighlightAdded() lipgloss.AdaptiveColor
DiffHighlightRemoved() lipgloss.AdaptiveColor
DiffAddedBg() lipgloss.AdaptiveColor
DiffRemovedBg() lipgloss.AdaptiveColor
DiffContextBg() lipgloss.AdaptiveColor
DiffLineNumber() lipgloss.AdaptiveColor
DiffAddedLineNumberBg() lipgloss.AdaptiveColor
DiffRemovedLineNumberBg() lipgloss.AdaptiveColor
// Markdown colors
MarkdownText() lipgloss.AdaptiveColor
MarkdownHeading() lipgloss.AdaptiveColor
MarkdownLink() lipgloss.AdaptiveColor
MarkdownLinkText() lipgloss.AdaptiveColor
MarkdownCode() lipgloss.AdaptiveColor
MarkdownBlockQuote() lipgloss.AdaptiveColor
MarkdownEmph() lipgloss.AdaptiveColor
MarkdownStrong() lipgloss.AdaptiveColor
MarkdownHorizontalRule() lipgloss.AdaptiveColor
MarkdownListItem() lipgloss.AdaptiveColor
MarkdownListEnumeration() lipgloss.AdaptiveColor
MarkdownImage() lipgloss.AdaptiveColor
MarkdownImageText() lipgloss.AdaptiveColor
MarkdownCodeBlock() lipgloss.AdaptiveColor
// Syntax highlighting colors
SyntaxComment() lipgloss.AdaptiveColor
SyntaxKeyword() lipgloss.AdaptiveColor
SyntaxFunction() lipgloss.AdaptiveColor
SyntaxVariable() lipgloss.AdaptiveColor
SyntaxString() lipgloss.AdaptiveColor
SyntaxNumber() lipgloss.AdaptiveColor
SyntaxType() lipgloss.AdaptiveColor
SyntaxOperator() lipgloss.AdaptiveColor
SyntaxPunctuation() lipgloss.AdaptiveColor
}
// BaseTheme provides a default implementation of the Theme interface
// that can be embedded in concrete theme implementations.
type BaseTheme struct {
// Base colors
PrimaryColor lipgloss.AdaptiveColor
SecondaryColor lipgloss.AdaptiveColor
AccentColor lipgloss.AdaptiveColor
// Status colors
ErrorColor lipgloss.AdaptiveColor
WarningColor lipgloss.AdaptiveColor
SuccessColor lipgloss.AdaptiveColor
InfoColor lipgloss.AdaptiveColor
// Text colors
TextColor lipgloss.AdaptiveColor
TextMutedColor lipgloss.AdaptiveColor
TextEmphasizedColor lipgloss.AdaptiveColor
// Background colors
BackgroundColor lipgloss.AdaptiveColor
BackgroundSecondaryColor lipgloss.AdaptiveColor
BackgroundDarkerColor lipgloss.AdaptiveColor
// Border colors
BorderNormalColor lipgloss.AdaptiveColor
BorderFocusedColor lipgloss.AdaptiveColor
BorderDimColor lipgloss.AdaptiveColor
// Diff view colors
DiffAddedColor lipgloss.AdaptiveColor
DiffRemovedColor lipgloss.AdaptiveColor
DiffContextColor lipgloss.AdaptiveColor
DiffHunkHeaderColor lipgloss.AdaptiveColor
DiffHighlightAddedColor lipgloss.AdaptiveColor
DiffHighlightRemovedColor lipgloss.AdaptiveColor
DiffAddedBgColor lipgloss.AdaptiveColor
DiffRemovedBgColor lipgloss.AdaptiveColor
DiffContextBgColor lipgloss.AdaptiveColor
DiffLineNumberColor lipgloss.AdaptiveColor
DiffAddedLineNumberBgColor lipgloss.AdaptiveColor
DiffRemovedLineNumberBgColor lipgloss.AdaptiveColor
// Markdown colors
MarkdownTextColor lipgloss.AdaptiveColor
MarkdownHeadingColor lipgloss.AdaptiveColor
MarkdownLinkColor lipgloss.AdaptiveColor
MarkdownLinkTextColor lipgloss.AdaptiveColor
MarkdownCodeColor lipgloss.AdaptiveColor
MarkdownBlockQuoteColor lipgloss.AdaptiveColor
MarkdownEmphColor lipgloss.AdaptiveColor
MarkdownStrongColor lipgloss.AdaptiveColor
MarkdownHorizontalRuleColor lipgloss.AdaptiveColor
MarkdownListItemColor lipgloss.AdaptiveColor
MarkdownListEnumerationColor lipgloss.AdaptiveColor
MarkdownImageColor lipgloss.AdaptiveColor
MarkdownImageTextColor lipgloss.AdaptiveColor
MarkdownCodeBlockColor lipgloss.AdaptiveColor
// Syntax highlighting colors
SyntaxCommentColor lipgloss.AdaptiveColor
SyntaxKeywordColor lipgloss.AdaptiveColor
SyntaxFunctionColor lipgloss.AdaptiveColor
SyntaxVariableColor lipgloss.AdaptiveColor
SyntaxStringColor lipgloss.AdaptiveColor
SyntaxNumberColor lipgloss.AdaptiveColor
SyntaxTypeColor lipgloss.AdaptiveColor
SyntaxOperatorColor lipgloss.AdaptiveColor
SyntaxPunctuationColor lipgloss.AdaptiveColor
}
// Implement the Theme interface for BaseTheme
func (t *BaseTheme) Primary() lipgloss.AdaptiveColor { return t.PrimaryColor }
func (t *BaseTheme) Secondary() lipgloss.AdaptiveColor { return t.SecondaryColor }
func (t *BaseTheme) Accent() lipgloss.AdaptiveColor { return t.AccentColor }
func (t *BaseTheme) Error() lipgloss.AdaptiveColor { return t.ErrorColor }
func (t *BaseTheme) Warning() lipgloss.AdaptiveColor { return t.WarningColor }
func (t *BaseTheme) Success() lipgloss.AdaptiveColor { return t.SuccessColor }
func (t *BaseTheme) Info() lipgloss.AdaptiveColor { return t.InfoColor }
func (t *BaseTheme) Text() lipgloss.AdaptiveColor { return t.TextColor }
func (t *BaseTheme) TextMuted() lipgloss.AdaptiveColor { return t.TextMutedColor }
func (t *BaseTheme) TextEmphasized() lipgloss.AdaptiveColor { return t.TextEmphasizedColor }
func (t *BaseTheme) Background() lipgloss.AdaptiveColor { return t.BackgroundColor }
func (t *BaseTheme) BackgroundSecondary() lipgloss.AdaptiveColor { return t.BackgroundSecondaryColor }
func (t *BaseTheme) BackgroundDarker() lipgloss.AdaptiveColor { return t.BackgroundDarkerColor }
func (t *BaseTheme) BorderNormal() lipgloss.AdaptiveColor { return t.BorderNormalColor }
func (t *BaseTheme) BorderFocused() lipgloss.AdaptiveColor { return t.BorderFocusedColor }
func (t *BaseTheme) BorderDim() lipgloss.AdaptiveColor { return t.BorderDimColor }
func (t *BaseTheme) DiffAdded() lipgloss.AdaptiveColor { return t.DiffAddedColor }
func (t *BaseTheme) DiffRemoved() lipgloss.AdaptiveColor { return t.DiffRemovedColor }
func (t *BaseTheme) DiffContext() lipgloss.AdaptiveColor { return t.DiffContextColor }
func (t *BaseTheme) DiffHunkHeader() lipgloss.AdaptiveColor { return t.DiffHunkHeaderColor }
func (t *BaseTheme) DiffHighlightAdded() lipgloss.AdaptiveColor { return t.DiffHighlightAddedColor }
func (t *BaseTheme) DiffHighlightRemoved() lipgloss.AdaptiveColor { return t.DiffHighlightRemovedColor }
func (t *BaseTheme) DiffAddedBg() lipgloss.AdaptiveColor { return t.DiffAddedBgColor }
func (t *BaseTheme) DiffRemovedBg() lipgloss.AdaptiveColor { return t.DiffRemovedBgColor }
func (t *BaseTheme) DiffContextBg() lipgloss.AdaptiveColor { return t.DiffContextBgColor }
func (t *BaseTheme) DiffLineNumber() lipgloss.AdaptiveColor { return t.DiffLineNumberColor }
func (t *BaseTheme) DiffAddedLineNumberBg() lipgloss.AdaptiveColor { return t.DiffAddedLineNumberBgColor }
func (t *BaseTheme) DiffRemovedLineNumberBg() lipgloss.AdaptiveColor { return t.DiffRemovedLineNumberBgColor }
func (t *BaseTheme) MarkdownText() lipgloss.AdaptiveColor { return t.MarkdownTextColor }
func (t *BaseTheme) MarkdownHeading() lipgloss.AdaptiveColor { return t.MarkdownHeadingColor }
func (t *BaseTheme) MarkdownLink() lipgloss.AdaptiveColor { return t.MarkdownLinkColor }
func (t *BaseTheme) MarkdownLinkText() lipgloss.AdaptiveColor { return t.MarkdownLinkTextColor }
func (t *BaseTheme) MarkdownCode() lipgloss.AdaptiveColor { return t.MarkdownCodeColor }
func (t *BaseTheme) MarkdownBlockQuote() lipgloss.AdaptiveColor { return t.MarkdownBlockQuoteColor }
func (t *BaseTheme) MarkdownEmph() lipgloss.AdaptiveColor { return t.MarkdownEmphColor }
func (t *BaseTheme) MarkdownStrong() lipgloss.AdaptiveColor { return t.MarkdownStrongColor }
func (t *BaseTheme) MarkdownHorizontalRule() lipgloss.AdaptiveColor { return t.MarkdownHorizontalRuleColor }
func (t *BaseTheme) MarkdownListItem() lipgloss.AdaptiveColor { return t.MarkdownListItemColor }
func (t *BaseTheme) MarkdownListEnumeration() lipgloss.AdaptiveColor { return t.MarkdownListEnumerationColor }
func (t *BaseTheme) MarkdownImage() lipgloss.AdaptiveColor { return t.MarkdownImageColor }
func (t *BaseTheme) MarkdownImageText() lipgloss.AdaptiveColor { return t.MarkdownImageTextColor }
func (t *BaseTheme) MarkdownCodeBlock() lipgloss.AdaptiveColor { return t.MarkdownCodeBlockColor }
func (t *BaseTheme) SyntaxComment() lipgloss.AdaptiveColor { return t.SyntaxCommentColor }
func (t *BaseTheme) SyntaxKeyword() lipgloss.AdaptiveColor { return t.SyntaxKeywordColor }
func (t *BaseTheme) SyntaxFunction() lipgloss.AdaptiveColor { return t.SyntaxFunctionColor }
func (t *BaseTheme) SyntaxVariable() lipgloss.AdaptiveColor { return t.SyntaxVariableColor }
func (t *BaseTheme) SyntaxString() lipgloss.AdaptiveColor { return t.SyntaxStringColor }
func (t *BaseTheme) SyntaxNumber() lipgloss.AdaptiveColor { return t.SyntaxNumberColor }
func (t *BaseTheme) SyntaxType() lipgloss.AdaptiveColor { return t.SyntaxTypeColor }
func (t *BaseTheme) SyntaxOperator() lipgloss.AdaptiveColor { return t.SyntaxOperatorColor }
func (t *BaseTheme) SyntaxPunctuation() lipgloss.AdaptiveColor { return t.SyntaxPunctuationColor }
+89
View File
@@ -0,0 +1,89 @@
package theme
import (
"testing"
)
func TestThemeRegistration(t *testing.T) {
// Get list of available themes
availableThemes := AvailableThemes()
// Check if "catppuccin" theme is registered
catppuccinFound := false
for _, themeName := range availableThemes {
if themeName == "catppuccin" {
catppuccinFound = true
break
}
}
if !catppuccinFound {
t.Errorf("Catppuccin theme is not registered")
}
// Check if "gruvbox" theme is registered
gruvboxFound := false
for _, themeName := range availableThemes {
if themeName == "gruvbox" {
gruvboxFound = true
break
}
}
if !gruvboxFound {
t.Errorf("Gruvbox theme is not registered")
}
// Check if "monokai" theme is registered
monokaiFound := false
for _, themeName := range availableThemes {
if themeName == "monokai" {
monokaiFound = true
break
}
}
if !monokaiFound {
t.Errorf("Monokai theme is not registered")
}
// Try to get the themes and make sure they're not nil
catppuccin := GetTheme("catppuccin")
if catppuccin == nil {
t.Errorf("Catppuccin theme is nil")
}
gruvbox := GetTheme("gruvbox")
if gruvbox == nil {
t.Errorf("Gruvbox theme is nil")
}
monokai := GetTheme("monokai")
if monokai == nil {
t.Errorf("Monokai theme is nil")
}
// Test switching theme
originalTheme := CurrentThemeName()
err := SetTheme("gruvbox")
if err != nil {
t.Errorf("Failed to set theme to gruvbox: %v", err)
}
if CurrentThemeName() != "gruvbox" {
t.Errorf("Theme not properly switched to gruvbox")
}
err = SetTheme("monokai")
if err != nil {
t.Errorf("Failed to set theme to monokai: %v", err)
}
if CurrentThemeName() != "monokai" {
t.Errorf("Theme not properly switched to monokai")
}
// Switch back to original theme
_ = SetTheme(originalTheme)
}
+274
View File
@@ -0,0 +1,274 @@
package theme
import (
"github.com/charmbracelet/lipgloss"
)
// TokyoNightTheme implements the Theme interface with Tokyo Night colors.
// It provides both dark and light variants.
type TokyoNightTheme struct {
BaseTheme
}
// NewTokyoNightTheme creates a new instance of the Tokyo Night theme.
func NewTokyoNightTheme() *TokyoNightTheme {
// Tokyo Night color palette
// Dark mode colors
darkBackground := "#222436"
darkCurrentLine := "#1e2030"
darkSelection := "#2f334d"
darkForeground := "#c8d3f5"
darkComment := "#636da6"
darkRed := "#ff757f"
darkOrange := "#ff966c"
darkYellow := "#ffc777"
darkGreen := "#c3e88d"
darkCyan := "#86e1fc"
darkBlue := "#82aaff"
darkPurple := "#c099ff"
darkBorder := "#3b4261"
// Light mode colors (Tokyo Night Day)
lightBackground := "#e1e2e7"
lightCurrentLine := "#d5d6db"
lightSelection := "#c8c9ce"
lightForeground := "#3760bf"
lightComment := "#848cb5"
lightRed := "#f52a65"
lightOrange := "#b15c00"
lightYellow := "#8c6c3e"
lightGreen := "#587539"
lightCyan := "#007197"
lightBlue := "#2e7de9"
lightPurple := "#9854f1"
lightBorder := "#a8aecb"
theme := &TokyoNightTheme{}
// Base colors
theme.PrimaryColor = lipgloss.AdaptiveColor{
Dark: darkBlue,
Light: lightBlue,
}
theme.SecondaryColor = lipgloss.AdaptiveColor{
Dark: darkPurple,
Light: lightPurple,
}
theme.AccentColor = lipgloss.AdaptiveColor{
Dark: darkOrange,
Light: lightOrange,
}
// Status colors
theme.ErrorColor = lipgloss.AdaptiveColor{
Dark: darkRed,
Light: lightRed,
}
theme.WarningColor = lipgloss.AdaptiveColor{
Dark: darkOrange,
Light: lightOrange,
}
theme.SuccessColor = lipgloss.AdaptiveColor{
Dark: darkGreen,
Light: lightGreen,
}
theme.InfoColor = lipgloss.AdaptiveColor{
Dark: darkBlue,
Light: lightBlue,
}
// Text colors
theme.TextColor = lipgloss.AdaptiveColor{
Dark: darkForeground,
Light: lightForeground,
}
theme.TextMutedColor = lipgloss.AdaptiveColor{
Dark: darkComment,
Light: lightComment,
}
theme.TextEmphasizedColor = lipgloss.AdaptiveColor{
Dark: darkYellow,
Light: lightYellow,
}
// Background colors
theme.BackgroundColor = lipgloss.AdaptiveColor{
Dark: darkBackground,
Light: lightBackground,
}
theme.BackgroundSecondaryColor = lipgloss.AdaptiveColor{
Dark: darkCurrentLine,
Light: lightCurrentLine,
}
theme.BackgroundDarkerColor = lipgloss.AdaptiveColor{
Dark: "#191B29", // Darker background from palette
Light: "#f0f0f5", // Slightly lighter than background
}
// Border colors
theme.BorderNormalColor = lipgloss.AdaptiveColor{
Dark: darkBorder,
Light: lightBorder,
}
theme.BorderFocusedColor = lipgloss.AdaptiveColor{
Dark: darkBlue,
Light: lightBlue,
}
theme.BorderDimColor = lipgloss.AdaptiveColor{
Dark: darkSelection,
Light: lightSelection,
}
// Diff view colors
theme.DiffAddedColor = lipgloss.AdaptiveColor{
Dark: "#4fd6be", // teal from palette
Light: "#1e725c",
}
theme.DiffRemovedColor = lipgloss.AdaptiveColor{
Dark: "#c53b53", // red1 from palette
Light: "#c53b53",
}
theme.DiffContextColor = lipgloss.AdaptiveColor{
Dark: "#828bb8", // fg_dark from palette
Light: "#7086b5",
}
theme.DiffHunkHeaderColor = lipgloss.AdaptiveColor{
Dark: "#828bb8", // fg_dark from palette
Light: "#7086b5",
}
theme.DiffHighlightAddedColor = lipgloss.AdaptiveColor{
Dark: "#b8db87", // git.add from palette
Light: "#4db380",
}
theme.DiffHighlightRemovedColor = lipgloss.AdaptiveColor{
Dark: "#e26a75", // git.delete from palette
Light: "#f52a65",
}
theme.DiffAddedBgColor = lipgloss.AdaptiveColor{
Dark: "#20303b",
Light: "#d5e5d5",
}
theme.DiffRemovedBgColor = lipgloss.AdaptiveColor{
Dark: "#37222c",
Light: "#f7d8db",
}
theme.DiffContextBgColor = lipgloss.AdaptiveColor{
Dark: darkBackground,
Light: lightBackground,
}
theme.DiffLineNumberColor = lipgloss.AdaptiveColor{
Dark: "#545c7e", // dark3 from palette
Light: "#848cb5",
}
theme.DiffAddedLineNumberBgColor = lipgloss.AdaptiveColor{
Dark: "#1b2b34",
Light: "#c5d5c5",
}
theme.DiffRemovedLineNumberBgColor = lipgloss.AdaptiveColor{
Dark: "#2d1f26",
Light: "#e7c8cb",
}
// Markdown colors
theme.MarkdownTextColor = lipgloss.AdaptiveColor{
Dark: darkForeground,
Light: lightForeground,
}
theme.MarkdownHeadingColor = lipgloss.AdaptiveColor{
Dark: darkPurple,
Light: lightPurple,
}
theme.MarkdownLinkColor = lipgloss.AdaptiveColor{
Dark: darkBlue,
Light: lightBlue,
}
theme.MarkdownLinkTextColor = lipgloss.AdaptiveColor{
Dark: darkCyan,
Light: lightCyan,
}
theme.MarkdownCodeColor = lipgloss.AdaptiveColor{
Dark: darkGreen,
Light: lightGreen,
}
theme.MarkdownBlockQuoteColor = lipgloss.AdaptiveColor{
Dark: darkYellow,
Light: lightYellow,
}
theme.MarkdownEmphColor = lipgloss.AdaptiveColor{
Dark: darkYellow,
Light: lightYellow,
}
theme.MarkdownStrongColor = lipgloss.AdaptiveColor{
Dark: darkOrange,
Light: lightOrange,
}
theme.MarkdownHorizontalRuleColor = lipgloss.AdaptiveColor{
Dark: darkComment,
Light: lightComment,
}
theme.MarkdownListItemColor = lipgloss.AdaptiveColor{
Dark: darkBlue,
Light: lightBlue,
}
theme.MarkdownListEnumerationColor = lipgloss.AdaptiveColor{
Dark: darkCyan,
Light: lightCyan,
}
theme.MarkdownImageColor = lipgloss.AdaptiveColor{
Dark: darkBlue,
Light: lightBlue,
}
theme.MarkdownImageTextColor = lipgloss.AdaptiveColor{
Dark: darkCyan,
Light: lightCyan,
}
theme.MarkdownCodeBlockColor = lipgloss.AdaptiveColor{
Dark: darkForeground,
Light: lightForeground,
}
// Syntax highlighting colors
theme.SyntaxCommentColor = lipgloss.AdaptiveColor{
Dark: darkComment,
Light: lightComment,
}
theme.SyntaxKeywordColor = lipgloss.AdaptiveColor{
Dark: darkPurple,
Light: lightPurple,
}
theme.SyntaxFunctionColor = lipgloss.AdaptiveColor{
Dark: darkBlue,
Light: lightBlue,
}
theme.SyntaxVariableColor = lipgloss.AdaptiveColor{
Dark: darkRed,
Light: lightRed,
}
theme.SyntaxStringColor = lipgloss.AdaptiveColor{
Dark: darkGreen,
Light: lightGreen,
}
theme.SyntaxNumberColor = lipgloss.AdaptiveColor{
Dark: darkOrange,
Light: lightOrange,
}
theme.SyntaxTypeColor = lipgloss.AdaptiveColor{
Dark: darkYellow,
Light: lightYellow,
}
theme.SyntaxOperatorColor = lipgloss.AdaptiveColor{
Dark: darkCyan,
Light: lightCyan,
}
theme.SyntaxPunctuationColor = lipgloss.AdaptiveColor{
Dark: darkForeground,
Light: lightForeground,
}
return theme
}
func init() {
// Register the Tokyo Night theme with the theme manager
RegisterTheme("tokyonight", NewTokyoNightTheme())
}
+276
View File
@@ -0,0 +1,276 @@
package theme
import (
"github.com/charmbracelet/lipgloss"
)
// TronTheme implements the Theme interface with Tron-inspired colors.
// It provides both dark and light variants, though Tron is primarily a dark theme.
type TronTheme struct {
BaseTheme
}
// NewTronTheme creates a new instance of the Tron theme.
func NewTronTheme() *TronTheme {
// Tron color palette
// Inspired by the Tron movie's neon aesthetic
darkBackground := "#0c141f"
darkCurrentLine := "#1a2633"
darkSelection := "#1a2633"
darkForeground := "#caf0ff"
darkComment := "#4d6b87"
darkCyan := "#00d9ff"
darkBlue := "#007fff"
darkOrange := "#ff9000"
darkPink := "#ff00a0"
darkPurple := "#b73fff"
darkRed := "#ff3333"
darkYellow := "#ffcc00"
darkGreen := "#00ff8f"
darkBorder := "#1a2633"
// Light mode approximation
lightBackground := "#f0f8ff"
lightCurrentLine := "#e0f0ff"
lightSelection := "#d0e8ff"
lightForeground := "#0c141f"
lightComment := "#4d6b87"
lightCyan := "#0097b3"
lightBlue := "#0066cc"
lightOrange := "#cc7300"
lightPink := "#cc0080"
lightPurple := "#9932cc"
lightRed := "#cc2929"
lightYellow := "#cc9900"
lightGreen := "#00cc72"
lightBorder := "#d0e8ff"
theme := &TronTheme{}
// Base colors
theme.PrimaryColor = lipgloss.AdaptiveColor{
Dark: darkCyan,
Light: lightCyan,
}
theme.SecondaryColor = lipgloss.AdaptiveColor{
Dark: darkBlue,
Light: lightBlue,
}
theme.AccentColor = lipgloss.AdaptiveColor{
Dark: darkOrange,
Light: lightOrange,
}
// Status colors
theme.ErrorColor = lipgloss.AdaptiveColor{
Dark: darkRed,
Light: lightRed,
}
theme.WarningColor = lipgloss.AdaptiveColor{
Dark: darkOrange,
Light: lightOrange,
}
theme.SuccessColor = lipgloss.AdaptiveColor{
Dark: darkGreen,
Light: lightGreen,
}
theme.InfoColor = lipgloss.AdaptiveColor{
Dark: darkCyan,
Light: lightCyan,
}
// Text colors
theme.TextColor = lipgloss.AdaptiveColor{
Dark: darkForeground,
Light: lightForeground,
}
theme.TextMutedColor = lipgloss.AdaptiveColor{
Dark: darkComment,
Light: lightComment,
}
theme.TextEmphasizedColor = lipgloss.AdaptiveColor{
Dark: darkYellow,
Light: lightYellow,
}
// Background colors
theme.BackgroundColor = lipgloss.AdaptiveColor{
Dark: darkBackground,
Light: lightBackground,
}
theme.BackgroundSecondaryColor = lipgloss.AdaptiveColor{
Dark: darkCurrentLine,
Light: lightCurrentLine,
}
theme.BackgroundDarkerColor = lipgloss.AdaptiveColor{
Dark: "#070d14", // Slightly darker than background
Light: "#ffffff", // Slightly lighter than background
}
// Border colors
theme.BorderNormalColor = lipgloss.AdaptiveColor{
Dark: darkBorder,
Light: lightBorder,
}
theme.BorderFocusedColor = lipgloss.AdaptiveColor{
Dark: darkCyan,
Light: lightCyan,
}
theme.BorderDimColor = lipgloss.AdaptiveColor{
Dark: darkSelection,
Light: lightSelection,
}
// Diff view colors
theme.DiffAddedColor = lipgloss.AdaptiveColor{
Dark: darkGreen,
Light: lightGreen,
}
theme.DiffRemovedColor = lipgloss.AdaptiveColor{
Dark: darkRed,
Light: lightRed,
}
theme.DiffContextColor = lipgloss.AdaptiveColor{
Dark: darkComment,
Light: lightComment,
}
theme.DiffHunkHeaderColor = lipgloss.AdaptiveColor{
Dark: darkBlue,
Light: lightBlue,
}
theme.DiffHighlightAddedColor = lipgloss.AdaptiveColor{
Dark: "#00ff8f",
Light: "#a5d6a7",
}
theme.DiffHighlightRemovedColor = lipgloss.AdaptiveColor{
Dark: "#ff3333",
Light: "#ef9a9a",
}
theme.DiffAddedBgColor = lipgloss.AdaptiveColor{
Dark: "#0a2a1a",
Light: "#e8f5e9",
}
theme.DiffRemovedBgColor = lipgloss.AdaptiveColor{
Dark: "#2a0a0a",
Light: "#ffebee",
}
theme.DiffContextBgColor = lipgloss.AdaptiveColor{
Dark: darkBackground,
Light: lightBackground,
}
theme.DiffLineNumberColor = lipgloss.AdaptiveColor{
Dark: darkComment,
Light: lightComment,
}
theme.DiffAddedLineNumberBgColor = lipgloss.AdaptiveColor{
Dark: "#082015",
Light: "#c8e6c9",
}
theme.DiffRemovedLineNumberBgColor = lipgloss.AdaptiveColor{
Dark: "#200808",
Light: "#ffcdd2",
}
// Markdown colors
theme.MarkdownTextColor = lipgloss.AdaptiveColor{
Dark: darkForeground,
Light: lightForeground,
}
theme.MarkdownHeadingColor = lipgloss.AdaptiveColor{
Dark: darkCyan,
Light: lightCyan,
}
theme.MarkdownLinkColor = lipgloss.AdaptiveColor{
Dark: darkBlue,
Light: lightBlue,
}
theme.MarkdownLinkTextColor = lipgloss.AdaptiveColor{
Dark: darkCyan,
Light: lightCyan,
}
theme.MarkdownCodeColor = lipgloss.AdaptiveColor{
Dark: darkGreen,
Light: lightGreen,
}
theme.MarkdownBlockQuoteColor = lipgloss.AdaptiveColor{
Dark: darkYellow,
Light: lightYellow,
}
theme.MarkdownEmphColor = lipgloss.AdaptiveColor{
Dark: darkYellow,
Light: lightYellow,
}
theme.MarkdownStrongColor = lipgloss.AdaptiveColor{
Dark: darkOrange,
Light: lightOrange,
}
theme.MarkdownHorizontalRuleColor = lipgloss.AdaptiveColor{
Dark: darkComment,
Light: lightComment,
}
theme.MarkdownListItemColor = lipgloss.AdaptiveColor{
Dark: darkBlue,
Light: lightBlue,
}
theme.MarkdownListEnumerationColor = lipgloss.AdaptiveColor{
Dark: darkCyan,
Light: lightCyan,
}
theme.MarkdownImageColor = lipgloss.AdaptiveColor{
Dark: darkBlue,
Light: lightBlue,
}
theme.MarkdownImageTextColor = lipgloss.AdaptiveColor{
Dark: darkCyan,
Light: lightCyan,
}
theme.MarkdownCodeBlockColor = lipgloss.AdaptiveColor{
Dark: darkForeground,
Light: lightForeground,
}
// Syntax highlighting colors
theme.SyntaxCommentColor = lipgloss.AdaptiveColor{
Dark: darkComment,
Light: lightComment,
}
theme.SyntaxKeywordColor = lipgloss.AdaptiveColor{
Dark: darkCyan,
Light: lightCyan,
}
theme.SyntaxFunctionColor = lipgloss.AdaptiveColor{
Dark: darkGreen,
Light: lightGreen,
}
theme.SyntaxVariableColor = lipgloss.AdaptiveColor{
Dark: darkOrange,
Light: lightOrange,
}
theme.SyntaxStringColor = lipgloss.AdaptiveColor{
Dark: darkYellow,
Light: lightYellow,
}
theme.SyntaxNumberColor = lipgloss.AdaptiveColor{
Dark: darkBlue,
Light: lightBlue,
}
theme.SyntaxTypeColor = lipgloss.AdaptiveColor{
Dark: darkPurple,
Light: lightPurple,
}
theme.SyntaxOperatorColor = lipgloss.AdaptiveColor{
Dark: darkPink,
Light: lightPink,
}
theme.SyntaxPunctuationColor = lipgloss.AdaptiveColor{
Dark: darkForeground,
Light: lightForeground,
}
return theme
}
func init() {
// Register the Tron theme with the theme manager
RegisterTheme("tron", NewTronTheme())
}
+126 -7
View File
@@ -2,6 +2,7 @@ package tui
import (
"context"
"fmt"
"github.com/charmbracelet/bubbles/key"
tea "github.com/charmbracelet/bubbletea"
@@ -25,6 +26,8 @@ type keyMap struct {
Help key.Binding
SwitchSession key.Binding
Commands key.Binding
Models key.Binding
SwitchTheme key.Binding
}
var keys = keyMap{
@@ -43,14 +46,24 @@ var keys = keyMap{
),
SwitchSession: key.NewBinding(
key.WithKeys("ctrl+a"),
key.WithHelp("ctrl+a", "switch session"),
key.WithKeys("ctrl+s"),
key.WithHelp("ctrl+s", "switch session"),
),
Commands: key.NewBinding(
key.WithKeys("ctrl+k"),
key.WithHelp("ctrl+k", "commands"),
),
Models: key.NewBinding(
key.WithKeys("ctrl+m"),
key.WithHelp("ctrl+m", "model selection"),
),
SwitchTheme: key.NewBinding(
key.WithKeys("ctrl+t"),
key.WithHelp("ctrl+t", "switch theme"),
),
}
var helpEsc = key.NewBinding(
@@ -64,8 +77,8 @@ var returnKey = key.NewBinding(
)
var logsKeyReturnKey = key.NewBinding(
key.WithKeys("backspace", "q"),
key.WithHelp("backspace/q", "go back"),
key.WithKeys("esc", "backspace", "q"),
key.WithHelp("esc/q", "go back"),
)
type appModel struct {
@@ -93,8 +106,14 @@ type appModel struct {
commandDialog dialog.CommandDialog
commands []dialog.Command
showModelDialog bool
modelDialog dialog.ModelDialog
showInitDialog bool
initDialog dialog.InitDialogCmp
showThemeDialog bool
themeDialog dialog.ThemeDialog
}
func (a appModel) Init() tea.Cmd {
@@ -112,8 +131,12 @@ func (a appModel) Init() tea.Cmd {
cmds = append(cmds, cmd)
cmd = a.commandDialog.Init()
cmds = append(cmds, cmd)
cmd = a.modelDialog.Init()
cmds = append(cmds, cmd)
cmd = a.initDialog.Init()
cmds = append(cmds, cmd)
cmd = a.themeDialog.Init()
cmds = append(cmds, cmd)
// Check if we should show the init dialog
cmds = append(cmds, func() tea.Msg {
@@ -243,6 +266,29 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
a.showCommandDialog = false
return a, nil
case dialog.CloseThemeDialogMsg:
a.showThemeDialog = false
return a, nil
case dialog.ThemeChangedMsg:
a.pages[a.currentPage], cmd = a.pages[a.currentPage].Update(msg)
a.showThemeDialog = false
return a, tea.Batch(cmd, util.ReportInfo("Theme changed to: "+msg.ThemeName))
case dialog.CloseModelDialogMsg:
a.showModelDialog = false
return a, nil
case dialog.ModelSelectedMsg:
a.showModelDialog = false
model, err := a.app.CoderAgent.Update(config.AgentCoder, msg.Model.ID)
if err != nil {
return a, util.ReportError(err)
}
return a, util.ReportInfo(fmt.Sprintf("Model changed to %s", model.Name))
case dialog.ShowInitDialogMsg:
a.showInitDialog = msg.Show
return a, nil
@@ -298,6 +344,9 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
if a.showCommandDialog {
a.showCommandDialog = false
}
if a.showModelDialog {
a.showModelDialog = false
}
return a, nil
case key.Matches(msg, keys.SwitchSession):
if a.currentPage == page.ChatPage && !a.showQuit && !a.showPermissions && !a.showCommandDialog {
@@ -315,7 +364,7 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
}
return a, nil
case key.Matches(msg, keys.Commands):
if a.currentPage == page.ChatPage && !a.showQuit && !a.showPermissions && !a.showSessionDialog {
if a.currentPage == page.ChatPage && !a.showQuit && !a.showPermissions && !a.showSessionDialog && !a.showThemeDialog {
// Show commands dialog
if len(a.commands) == 0 {
return a, util.ReportWarn("No commands available")
@@ -325,6 +374,24 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return a, nil
}
return a, nil
case key.Matches(msg, keys.Models):
if a.showModelDialog {
a.showModelDialog = false
return a, nil
}
if a.currentPage == page.ChatPage && !a.showQuit && !a.showPermissions && !a.showSessionDialog && !a.showCommandDialog {
a.showModelDialog = true
return a, nil
}
return a, nil
case key.Matches(msg, keys.SwitchTheme):
if !a.showQuit && !a.showPermissions && !a.showSessionDialog && !a.showCommandDialog {
// Show theme switcher dialog
a.showThemeDialog = true
// Theme list is dynamically loaded by the dialog component
return a, a.themeDialog.Init()
}
return a, nil
case key.Matches(msg, logsKeyReturnKey):
if a.currentPage == page.LogsPage {
return a, a.moveToPage(page.ChatPage)
@@ -405,6 +472,16 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
}
}
if a.showModelDialog {
d, modelCmd := a.modelDialog.Update(msg)
a.modelDialog = d.(dialog.ModelDialog)
cmds = append(cmds, modelCmd)
// Only block key messages send all other messages down
if _, ok := msg.(tea.KeyMsg); ok {
return a, tea.Batch(cmds...)
}
}
if a.showInitDialog {
d, initCmd := a.initDialog.Update(msg)
a.initDialog = d.(dialog.InitDialogCmp)
@@ -415,6 +492,16 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
}
}
if a.showThemeDialog {
d, themeCmd := a.themeDialog.Update(msg)
a.themeDialog = d.(dialog.ThemeDialog)
cmds = append(cmds, themeCmd)
// Only block key messages send all other messages down
if _, ok := msg.(tea.KeyMsg); ok {
return a, tea.Batch(cmds...)
}
}
s, _ := a.status.Update(msg)
a.status = s.(core.StatusCmp)
a.pages[a.currentPage], cmd = a.pages[a.currentPage].Update(msg)
@@ -473,9 +560,9 @@ func (a appModel) View() string {
}
if !a.app.CoderAgent.IsBusy() {
a.status.SetHelpMsg("ctrl+? help")
a.status.SetHelpWidgetMsg("ctrl+? help")
} else {
a.status.SetHelpMsg("? help")
a.status.SetHelpWidgetMsg("? help")
}
if a.showHelp {
@@ -538,6 +625,21 @@ func (a appModel) View() string {
)
}
if a.showModelDialog {
overlay := a.modelDialog.View()
row := lipgloss.Height(appView) / 2
row -= lipgloss.Height(overlay) / 2
col := lipgloss.Width(appView) / 2
col -= lipgloss.Width(overlay) / 2
appView = layout.PlaceOverlay(
col,
row,
overlay,
appView,
true,
)
}
if a.showCommandDialog {
overlay := a.commandDialog.View()
row := lipgloss.Height(appView) / 2
@@ -564,6 +666,21 @@ func (a appModel) View() string {
)
}
if a.showThemeDialog {
overlay := a.themeDialog.View()
row := lipgloss.Height(appView) / 2
row -= lipgloss.Height(overlay) / 2
col := lipgloss.Width(appView) / 2
col -= lipgloss.Width(overlay) / 2
appView = layout.PlaceOverlay(
col,
row,
overlay,
appView,
true,
)
}
return appView
}
@@ -577,8 +694,10 @@ func New(app *app.App) tea.Model {
quit: dialog.NewQuitCmp(),
sessionDialog: dialog.NewSessionDialogCmp(),
commandDialog: dialog.NewCommandDialogCmp(),
modelDialog: dialog.NewModelDialogCmp(),
permissions: dialog.NewPermissionDialogCmp(),
initDialog: dialog.NewInitDialogCmp(),
themeDialog: dialog.NewThemeDialogCmp(),
app: app,
commands: []dialog.Command{},
pages: map[page.PageID]tea.Model{
+153 -49
View File
@@ -12,33 +12,63 @@
"model": {
"description": "Model ID for the agent",
"enum": [
"claude-3.7-sonnet",
"gpt-4o-mini",
"o1-pro",
"azure.gpt-4o-mini",
"openrouter.gpt-4.1-mini",
"openrouter.o1-mini",
"bedrock.claude-3.7-sonnet",
"meta-llama/llama-4-scout-17b-16e-instruct",
"openrouter.gpt-4o-mini",
"gemini-2.0-flash",
"deepseek-r1-distill-llama-70b",
"openrouter.claude-3.7-sonnet",
"openrouter.gpt-4.5-preview",
"azure.o3-mini",
"openrouter.claude-3.5-haiku",
"azure.o1-mini",
"openrouter.o1",
"openrouter.gemini-2.5",
"llama-3.3-70b-versatile",
"gpt-4.5-preview",
"openrouter.claude-3-opus",
"openrouter.claude-3.5-sonnet",
"o4-mini",
"gemini-2.0-flash-lite",
"azure.gpt-4.5-preview",
"openrouter.gpt-4o",
"o1",
"azure.gpt-4o",
"openrouter.gpt-4.1-nano",
"o3",
"gpt-4.1",
"azure.o1",
"claude-3-haiku",
"claude-3-opus",
"gpt-4.1-mini",
"gpt-4o",
"gpt-4o-mini",
"gemini-2.0-flash-lite",
"meta-llama/llama-4-maverick-17b-128e-instruct",
"gpt-4.1",
"gpt-4.5-preview",
"o1",
"gpt-4.1-nano",
"o3-mini",
"gemini-2.5-flash",
"gemini-2.0-flash",
"meta-llama/llama-4-scout-17b-16e-instruct",
"bedrock.claude-3.7-sonnet",
"o1-pro",
"o3",
"gemini-2.5",
"qwen-qwq",
"llama-3.3-70b-versatile",
"deepseek-r1-distill-llama-70b",
"claude-3.5-sonnet",
"claude-3-haiku",
"openrouter.o4-mini",
"openrouter.gemini-2.5-flash",
"claude-3.5-haiku",
"o4-mini",
"o1-mini"
"o3-mini",
"azure.o3",
"gpt-4o",
"azure.gpt-4.1",
"openrouter.claude-3-haiku",
"gpt-4.1-nano",
"azure.gpt-4.1-nano",
"claude-3.7-sonnet",
"gemini-2.5",
"azure.o4-mini",
"o1-mini",
"qwen-qwq",
"meta-llama/llama-4-maverick-17b-128e-instruct",
"openrouter.gpt-4.1",
"openrouter.o1-pro",
"openrouter.o3",
"claude-3.5-sonnet",
"gemini-2.5-flash",
"azure.gpt-4.1-mini",
"openrouter.o3-mini"
],
"type": "string"
},
@@ -72,33 +102,63 @@
"model": {
"description": "Model ID for the agent",
"enum": [
"claude-3.7-sonnet",
"gpt-4o-mini",
"o1-pro",
"azure.gpt-4o-mini",
"openrouter.gpt-4.1-mini",
"openrouter.o1-mini",
"bedrock.claude-3.7-sonnet",
"meta-llama/llama-4-scout-17b-16e-instruct",
"openrouter.gpt-4o-mini",
"gemini-2.0-flash",
"deepseek-r1-distill-llama-70b",
"openrouter.claude-3.7-sonnet",
"openrouter.gpt-4.5-preview",
"azure.o3-mini",
"openrouter.claude-3.5-haiku",
"azure.o1-mini",
"openrouter.o1",
"openrouter.gemini-2.5",
"llama-3.3-70b-versatile",
"gpt-4.5-preview",
"openrouter.claude-3-opus",
"openrouter.claude-3.5-sonnet",
"o4-mini",
"gemini-2.0-flash-lite",
"azure.gpt-4.5-preview",
"openrouter.gpt-4o",
"o1",
"azure.gpt-4o",
"openrouter.gpt-4.1-nano",
"o3",
"gpt-4.1",
"azure.o1",
"claude-3-haiku",
"claude-3-opus",
"gpt-4.1-mini",
"gpt-4o",
"gpt-4o-mini",
"gemini-2.0-flash-lite",
"meta-llama/llama-4-maverick-17b-128e-instruct",
"gpt-4.1",
"gpt-4.5-preview",
"o1",
"gpt-4.1-nano",
"o3-mini",
"gemini-2.5-flash",
"gemini-2.0-flash",
"meta-llama/llama-4-scout-17b-16e-instruct",
"bedrock.claude-3.7-sonnet",
"o1-pro",
"o3",
"gemini-2.5",
"qwen-qwq",
"llama-3.3-70b-versatile",
"deepseek-r1-distill-llama-70b",
"claude-3.5-sonnet",
"claude-3-haiku",
"openrouter.o4-mini",
"openrouter.gemini-2.5-flash",
"claude-3.5-haiku",
"o4-mini",
"o1-mini"
"o3-mini",
"azure.o3",
"gpt-4o",
"azure.gpt-4.1",
"openrouter.claude-3-haiku",
"gpt-4.1-nano",
"azure.gpt-4.1-nano",
"claude-3.7-sonnet",
"gemini-2.5",
"azure.o4-mini",
"o1-mini",
"qwen-qwq",
"meta-llama/llama-4-maverick-17b-128e-instruct",
"openrouter.gpt-4.1",
"openrouter.o1-pro",
"openrouter.o3",
"claude-3.5-sonnet",
"gemini-2.5-flash",
"azure.gpt-4.1-mini",
"openrouter.o3-mini"
],
"type": "string"
},
@@ -131,6 +191,26 @@
},
"type": "object"
},
"contextPaths": {
"default": [
".github/copilot-instructions.md",
".cursorrules",
".cursor/rules/",
"CLAUDE.md",
"CLAUDE.local.md",
"opencode.md",
"opencode.local.md",
"OpenCode.md",
"OpenCode.local.md",
"OPENCODE.md",
"OPENCODE.local.md"
],
"description": "Context paths for the application",
"items": {
"type": "string"
},
"type": "array"
},
"data": {
"description": "Storage configuration",
"properties": {
@@ -259,7 +339,9 @@
"openai",
"gemini",
"groq",
"bedrock"
"openrouter",
"bedrock",
"azure"
],
"type": "string"
}
@@ -269,6 +351,28 @@
"description": "LLM provider configurations",
"type": "object"
},
"tui": {
"description": "Terminal User Interface configuration",
"properties": {
"theme": {
"default": "opencode",
"description": "TUI theme name",
"enum": [
"opencode",
"catppuccin",
"dracula",
"flexoki",
"gruvbox",
"monokai",
"onedark",
"tokyonight",
"tron"
],
"type": "string"
}
},
"type": "object"
},
"wd": {
"description": "Working directory for the application",
"type": "string"