Compare commits

..
21 Commits
Author SHA1 Message Date
Dax Raad 1684042fb6 huge optimization for token usage with anthropic
publish / publish (push) Has been cancelled
2025-06-20 22:43:04 -04:00
Dax Raadandopencode 59f0004d34 Add --method option to upgrade command for manual installation method selection
publish / publish (push) Has been cancelled
🤖 Generated with [opencode](https://opencode.ai)

Co-Authored-By: opencode <noreply@opencode.ai>
2025-06-20 20:48:23 -04:00
Dax Raad da35a64fa1 handle brew upgrades better 2025-06-20 20:27:23 -04:00
Dax Raad 460338ca53 make IDs more random
publish / publish (push) Has been cancelled
2025-06-20 17:39:59 -04:00
Saatvik AryaandGitHub 53c18a64b4 docs: add API client generation instructions to README and AGENTS.md (#273) 2025-06-20 17:27:58 -04:00
Saatvik AryaandGitHub b8144c5654 fix: return false for missing AWS_PROFILE in amazon-bedrock provider (#277) 2025-06-20 17:27:27 -04:00
adamdottv 9081e17fcc fix(tui): visual tweaks to themes
publish / publish (push) Has been cancelled
2025-06-20 15:49:51 -05:00
adamdottv ef3fd5900f docs: cleanup casing
publish / publish (push) Has been cancelled
2025-06-20 15:35:25 -05:00
adamdottv 453d690c11 docs: new themes docs 2025-06-20 15:31:38 -05:00
adamdottv c45be6a645 feat(tui): one dark theme 2025-06-20 15:14:23 -05:00
adamdottv 7b9b177088 feat(tui): kanagawa theme 2025-06-20 15:14:23 -05:00
adamdottv 3cee5b0470 feat(tui): gruvbox theme 2025-06-20 15:14:23 -05:00
adamdottv 9246d1c901 feat(tui): catppuccin theme 2025-06-20 15:14:22 -05:00
adamdottv cc12abc83e feat(tui): nord theme 2025-06-20 15:14:22 -05:00
adamdottv 4f7e4a9436 feat(tui): custom themes 2025-06-20 15:14:22 -05:00
Márk MagyarandGitHub eee396f903 feat(tui): theme switcher with preview (#264) 2025-06-20 15:14:05 -05:00
Jay V 0d2f8e175a docs: share bugs 2025-06-20 15:50:12 -04:00
Jay V 4df40e0d9b docs: share page bugs 2025-06-20 15:50:12 -04:00
Dax Raad b72e17a8b7 fix issue with conversations hanging 2025-06-20 15:49:49 -04:00
Dax Raad 61160dc220 docs: readme 2025-06-20 15:22:41 -04:00
Dax Raadandopencode 98734ff28c Consolidate session context handling and add global config support
publish / publish (push) Has been cancelled
Refactored context file discovery by removing separate SessionContext module and integrating functionality into SystemPrompt.context(). Added support for finding AGENTS.md and CLAUDE.md files in global config directories.

🤖 Generated with [opencode](https://opencode.ai)

Co-Authored-By: opencode <noreply@opencode.ai>
2025-06-20 15:14:12 -04:00
42 changed files with 2445 additions and 1496 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
MIT License
Copyright (c) 2025 OpenCode
Copyright (c) 2025 opencode
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
+8 -21
View File
@@ -50,31 +50,18 @@ $ bun install
$ bun run packages/opencode/src/index.ts
```
### FAQ
#### Development Notes
#### How do I use this with OpenRouter?
**API Client Generation**: After making changes to the TypeScript API endpoints in `packages/opencode/src/server/server.ts`, you need to regenerate the Go client and OpenAPI specification:
OpenRouter is not in the Models.dev database yet, but you can configure it manually.
```json title="opencode.json"
{
"$schema": "https://opencode.ai/config.json",
"provider": {
"openrouter": {
"npm": "@openrouter/ai-sdk-provider",
"name": "OpenRouter",
"options": {},
"models": {
"anthropic/claude-3.5-sonnet": {
"name": "Claude 3.5 Sonnet"
}
}
}
}
}
```bash
$ cd packages/tui
$ go generate ./pkg/client/
```
And then to configure an api key you can do `opencode auth login` and select "Other -> 'openrouter'"
This updates the generated Go client code that the TUI uses to communicate with the backend server.
### FAQ
#### How is this different than Claude Code?
+5
View File
@@ -71,6 +71,7 @@
"astro": "5.7.13",
"diff": "8.0.2",
"js-base64": "3.7.7",
"lang-map": "0.4.0",
"luxon": "3.6.1",
"marked": "15.0.12",
"rehype-autolink-headings": "7.1.0",
@@ -1011,6 +1012,10 @@
"klona": ["klona@2.0.6", "", {}, "sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA=="],
"lang-map": ["lang-map@0.4.0", "", { "dependencies": { "language-map": "^1.1.0" } }, "sha512-oiSqZIEUnWdFeDNsp4HId4tAxdFbx5iMBOwA3666Fn2L8Khj8NiD9xRvMsGmKXopPVkaDFtSv3CJOmXFUB0Hcg=="],
"language-map": ["language-map@1.5.0", "", {}, "sha512-n7gFZpe+DwEAX9cXVTw43i3wiudWDDtSn28RmdnS/HCPr284dQI/SztsamWanRr75oSlKSaGbV2nmWCTzGCoVg=="],
"leven": ["leven@2.1.0", "", {}, "sha512-nvVPLpIHUxCUoRLrFqTgSxXJ614d8AgQoWl7zPe/2VadE8+1dpU3LBhowRuBAcuwruWtOdD8oYC9jDNJjXDPyA=="],
"longest-streak": ["longest-streak@3.1.0", "", {}, "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g=="],
+2
View File
@@ -7,6 +7,7 @@
- **Typecheck**: `bun run typecheck` (npm run typecheck)
- **Test**: `bun test` (runs all tests)
- **Single test**: `bun test test/tool/tool.test.ts` (specific test file)
- **API Client Generation**: `cd packages/tui && go generate ./pkg/client/` (after changes to server endpoints)
## Code Style
@@ -37,3 +38,4 @@
- **Validation**: All inputs validated with Zod schemas
- **Logging**: Use `Log.create({ service: "name" })` pattern
- **Storage**: Use `Storage` namespace for persistence
- **API Client**: Go TUI communicates with TypeScript server via generated client. When adding/modifying server endpoints in `packages/opencode/src/server/server.ts`, run `cd packages/tui && go generate ./pkg/client/` to update the Go client code and OpenAPI spec.
+15 -6
View File
@@ -7,17 +7,25 @@ export const UpgradeCommand = {
command: "upgrade [target]",
describe: "upgrade opencode to the latest version or a specific version",
builder: (yargs: Argv) => {
return yargs.positional("target", {
describe: "specific version to upgrade to (e.g., '0.1.48' or 'v0.1.48')",
type: "string",
})
return yargs
.positional("target", {
describe: "specific version to upgrade to (e.g., '0.1.48' or 'v0.1.48')",
type: "string",
})
.option("method", {
alias: "m",
describe: "installation method to use (curl, npm, pnpm, bun, brew)",
type: "string",
choices: ["curl", "npm", "pnpm", "bun", "brew"],
})
},
handler: async (args: { target?: string }) => {
handler: async (args: { target?: string; method?: string }) => {
UI.empty()
UI.println(UI.logo(" "))
UI.empty()
prompts.intro("Upgrade")
const method = await Installation.method()
const detectedMethod = await Installation.method()
const method = (args.method as Installation.Method) ?? detectedMethod
if (method === "unknown") {
prompts.log.error(
`opencode is installed to ${process.execPath} and seems to be managed by a package manager`,
@@ -25,6 +33,7 @@ export const UpgradeCommand = {
prompts.outro("Done")
return
}
prompts.log.info("Using method: " + method)
const target = args.target ?? (await Installation.latest())
prompts.log.info(`From ${Installation.VERSION}${target}`)
const spinner = prompts.spinner()
+12 -4
View File
@@ -41,6 +41,17 @@ export namespace Identifier {
return given
}
function randomBase62(length: number): string {
const chars =
"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
let result = ""
const bytes = randomBytes(length)
for (let i = 0; i < length; i++) {
result += chars[bytes[i] % 62]
}
return result
}
function generateNewID(
prefix: keyof typeof prefixes,
descending: boolean,
@@ -62,14 +73,11 @@ export namespace Identifier {
timeBytes[i] = Number((now >> BigInt(40 - 8 * i)) & BigInt(0xff))
}
const randLength = (LENGTH - 12) / 2
const random = randomBytes(randLength)
return (
prefixes[prefix] +
"_" +
timeBytes.toString("hex") +
random.toString("hex")
randomBase62(LENGTH - 12)
)
}
}
+19 -1
View File
@@ -3,12 +3,15 @@ import { $ } from "bun"
import { z } from "zod"
import { NamedError } from "../util/error"
import { Bus } from "../bus"
import { Log } from "../util/log"
declare global {
const OPENCODE_VERSION: string
}
export namespace Installation {
const log = Log.create({ service: "installation" })
export type Method = Awaited<ReturnType<typeof method>>
export const Event = {
@@ -66,6 +69,10 @@ export namespace Installation {
name: "bun" as const,
command: () => $`bun pm ls -g`.throws(false).text(),
},
{
name: "brew" as const,
command: () => $`brew list --formula opencode-ai`.throws(false).text(),
},
]
checks.sort((a, b) => {
@@ -97,18 +104,29 @@ export namespace Installation {
const cmd = (() => {
switch (method) {
case "curl":
return $`curl -fsSL https://opencode.ai/install | bash`
return $`curl -fsSL https://opencode.ai/install | bash`.env({
...process.env,
VERSION: target,
})
case "npm":
return $`npm install -g opencode-ai@${target}`
case "pnpm":
return $`pnpm install -g opencode-ai@${target}`
case "bun":
return $`bun install -g opencode-ai@${target}`
case "brew":
return $`brew install sst/tap/opencode`
default:
throw new Error(`Unknown method: ${method}`)
}
})()
const result = await cmd.quiet().throws(false)
log.info("upgraded", {
method,
target,
stdout: result.stdout.toString(),
stderr: result.stderr.toString(),
})
if (result.exitCode !== 0)
throw new UpgradeFailedError({
stderr: result.stderr.toString("utf8"),
+1 -1
View File
@@ -75,7 +75,7 @@ export namespace Provider {
}
},
"amazon-bedrock": async () => {
if (!process.env["AWS_PROFILE"]) false
if (!process.env["AWS_PROFILE"]) return false
const region = process.env["AWS_REGION"] ?? "us-east-1"
+15 -14
View File
@@ -1,24 +1,25 @@
import type { CoreMessage } from "ai"
import type { CoreMessage, LanguageModelV1Prompt } from "ai"
import { unique } from "remeda"
export namespace ProviderTransform {
export function message(
msg: CoreMessage,
index: number,
msgs: LanguageModelV1Prompt,
providerID: string,
modelID: string,
) {
if (
(providerID === "anthropic" || modelID.includes("anthropic")) &&
index < 4
) {
msg.providerOptions = {
...msg.providerOptions,
anthropic: {
cacheControl: { type: "ephemeral" },
},
if (providerID === "anthropic" || modelID.includes("anthropic")) {
const system = msgs.filter((msg) => msg.role === "system").slice(0, 2)
const final = msgs.filter((msg) => msg.role !== "system").slice(-2)
for (const msg of unique([...system, ...final])) {
msg.providerMetadata = {
...msg.providerMetadata,
anthropic: {
cacheControl: { type: "ephemeral" },
},
}
}
}
return msg
return msgs
}
}
+28
View File
@@ -290,6 +290,34 @@ export namespace Server {
return c.json(session)
},
)
.post(
"/session_unshare",
describeRoute({
description: "Unshare the session",
responses: {
200: {
description: "Successfully unshared session",
content: {
"application/json": {
schema: resolver(Session.Info),
},
},
},
},
}),
zValidator(
"json",
z.object({
sessionID: z.string(),
}),
),
async (c) => {
const body = c.req.valid("json")
await Session.unshare(body.sessionID)
const session = await Session.get(body.sessionID)
return c.json(session)
},
)
.post(
"/session_messages",
describeRoute({
-19
View File
@@ -1,19 +0,0 @@
import { App } from "../app/app"
import { Filesystem } from "../util/filesystem"
export namespace SessionContext {
const FILES = [
"AGENTS.md",
"CLAUDE.md",
"CONTEXT.md", // deprecated
]
export async function find() {
const { cwd, root } = App.info().path
const found = []
for (const item of FILES) {
const matches = await Filesystem.findUp(item, cwd, root)
found.push(...matches.map((x) => Bun.file(x).text()))
}
return Promise.all(found).then((parts) => parts.join("\n\n"))
}
}
+28 -7
View File
@@ -14,6 +14,7 @@ import {
type CoreMessage,
type UIMessage,
type ProviderMetadata,
wrapLanguageModel,
} from "ai"
import { z, ZodSchema } from "zod"
import { Decimal } from "decimal.js"
@@ -159,6 +160,14 @@ export namespace Session {
return share
}
export async function unshare(id: string) {
await Storage.remove("session/share/" + id)
await update(id, (draft) => {
draft.share = undefined
})
await Share.remove(id)
}
export async function update(id: string, editor: (session: Info) => void) {
const { sessions } = state()
const session = await get(id)
@@ -277,9 +286,7 @@ export namespace Session {
parts: toParts(input.parts),
},
]),
].map((msg, i) =>
ProviderTransform.message(msg, i, input.providerID, input.modelID),
),
],
model: model.language,
})
.then((result) => {
@@ -519,12 +526,26 @@ export namespace Session {
...convertToCoreMessages(
msgs.map(toUIMessage).filter((x) => x.parts.length > 0),
),
].map((msg, i) =>
ProviderTransform.message(msg, i, input.providerID, input.modelID),
),
],
temperature: model.info.temperature ? 0 : undefined,
tools: model.info.tool_call === false ? undefined : tools,
model: model.language,
model: wrapLanguageModel({
model: model.language,
middleware: [
{
async transformParams(args) {
if (args.type === "stream") {
args.params.prompt = ProviderTransform.message(
args.params.prompt,
input.providerID,
input.modelID,
)
}
return args.params
},
},
],
}),
})
try {
for await (const value of result.fullStream) {
+14 -1
View File
@@ -1,6 +1,9 @@
import { App } from "../app/app"
import { Ripgrep } from "../external/ripgrep"
import { Global } from "../global"
import { Filesystem } from "../util/filesystem"
import path from "path"
import os from "os"
import PROMPT_ANTHROPIC from "./prompt/anthropic.txt"
import PROMPT_ANTHROPIC_SPOOF from "./prompt/anthropic_spoof.txt"
@@ -101,7 +104,17 @@ export namespace SystemPrompt {
const matches = await Filesystem.findUp(item, cwd, root)
found.push(...matches.map((x) => Bun.file(x).text()))
}
return Promise.all(found)
found.push(
Bun.file(path.join(Global.Path.config, "AGENTS.md"))
.text()
.catch(() => ""),
)
found.push(
Bun.file(path.join(os.homedir(), ".claude", "CLAUDE.md"))
.text()
.catch(() => ""),
)
return Promise.all(found).then((result) => result.filter(Boolean))
}
export function summarize(providerID: string) {
+7
View File
@@ -70,4 +70,11 @@ export namespace Share {
.then((x) => x.json())
.then((x) => x as { url: string; secret: string })
}
export async function remove(id: string) {
return fetch(`${URL}/share_delete`, {
method: "POST",
body: JSON.stringify({ id }),
}).then((x) => x.json())
}
}
+5
View File
@@ -24,6 +24,11 @@ export namespace Storage {
}
})
export async function remove(key: string) {
const target = path.join(state().dir, key + ".json")
await fs.unlink(target).catch(() => {})
}
export async function readJSON<T>(key: string) {
return Bun.file(path.join(state().dir, key + ".json")).json() as Promise<T>
}
+12
View File
@@ -60,6 +60,9 @@ func New(
if err != nil {
return nil, err
}
if configResponse.StatusCode() != 200 || configResponse.JSON200 == nil {
return nil, fmt.Errorf("failed to get config: %d", configResponse.StatusCode())
}
configInfo := configResponse.JSON200
if configInfo.Keybinds == nil {
leader := "ctrl+x"
@@ -85,6 +88,15 @@ func New(
appState.Model = strings.Join(splits[1:], "/")
}
// Load themes from all directories
if err := theme.LoadThemesFromDirectories(
appInfo.Path.Config,
appInfo.Path.Root,
appInfo.Path.Cwd,
); err != nil {
slog.Warn("Failed to load themes from directories", "error", err)
}
if appState.Theme != "" {
theme.SetTheme(appState.Theme)
}
@@ -47,8 +47,10 @@ type themeDialog struct {
width int
height int
modal *modal.Modal
list list.List[themeItem]
modal *modal.Modal
list list.List[themeItem]
originalTheme string
themeApplied bool
}
func (t *themeDialog) Init() tea.Cmd {
@@ -64,26 +66,31 @@ func (t *themeDialog) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg.String() {
case "enter":
if item, idx := t.list.GetSelectedItem(); idx >= 0 {
previousTheme := theme.CurrentThemeName()
selectedTheme := item.name
if previousTheme == selectedTheme {
return t, util.CmdHandler(modal.CloseModalMsg{})
}
if err := theme.SetTheme(selectedTheme); err != nil {
// status.Error(err.Error())
return t, nil
}
t.themeApplied = true
return t, tea.Sequence(
util.CmdHandler(modal.CloseModalMsg{}),
util.CmdHandler(ThemeSelectedMsg{ThemeName: selectedTheme}),
)
}
}
}
_, prevIdx := t.list.GetSelectedItem()
var cmd tea.Cmd
listModel, cmd := t.list.Update(msg)
t.list = listModel.(list.List[themeItem])
if item, newIdx := t.list.GetSelectedItem(); newIdx >= 0 && newIdx != prevIdx {
theme.SetTheme(item.name)
return t, util.CmdHandler(ThemeSelectedMsg{ThemeName: item.name})
}
return t, cmd
}
@@ -92,6 +99,9 @@ func (t *themeDialog) Render(background string) string {
}
func (t *themeDialog) Close() tea.Cmd {
if !t.themeApplied {
theme.SetTheme(t.originalTheme)
}
return nil
}
@@ -120,7 +130,9 @@ func NewThemeDialog() ThemeDialog {
list.SetSelectedIndex(selectedIdx)
return &themeDialog{
list: list,
modal: modal.New(modal.WithTitle("Select Theme"), modal.WithMaxWidth(40)),
list: list,
modal: modal.New(modal.WithTitle("Select Theme"), modal.WithMaxWidth(40)),
originalTheme: currentTheme,
themeApplied: false,
}
}
@@ -114,7 +114,7 @@ func (m *Modal) Render(contentView string, background string) string {
titleLine := m.title + spacer + escText
titleLine = titleStyle.Render(titleLine)
finalContent = strings.Join([]string{titleLine, contentView}, "\n") + "\n"
finalContent = strings.Join([]string{titleLine, "", contentView}, "\n")
} else {
finalContent = contentView
}
-276
View File
@@ -1,276 +0,0 @@
package theme
import (
"github.com/charmbracelet/lipgloss/v2"
"github.com/charmbracelet/lipgloss/v2/compat"
)
// AyuTheme implements the Theme interface with Ayu Dark colors.
// It provides a modern dark theme inspired by the Ayu color scheme.
type AyuTheme struct {
BaseTheme
}
// NewAyuTheme creates a new instance of the Ayu Dark theme.
func NewAyuTheme() *AyuTheme {
// Ayu Dark color palette
// Base background colors
darkBg := "#0B0E14" // App background
darkBgAlt := "#0D1017" // Editor background
darkLine := "#11151C" // UI line separators
darkPanel := "#0F131A" // UI panel background
// Text colors
darkFg := "#BFBDB6" // Primary text
darkFgMuted := "#565B66" // Muted text
darkGutter := "#6C7380" // Gutter text
// Syntax highlighting colors
darkTag := "#39BAE6" // Tags and attributes
darkFunc := "#FFB454" // Functions
darkEntity := "#59C2FF" // Entities and variables
darkString := "#AAD94C" // Strings
darkRegexp := "#95E6CB" // Regular expressions
darkMarkup := "#F07178" // Markup elements
darkKeyword := "#FF8F40" // Keywords
darkSpecial := "#E6B673" // Special characters
darkComment := "#ACB6BF" // Comments
darkConstant := "#D2A6FF" // Constants
darkOperator := "#F29668" // Operators
// Version control colors
darkAdded := "#7FD962" // Added lines
darkRemoved := "#F26D78" // Removed lines
// Accent colors
darkAccent := "#E6B450" // Primary accent
darkError := "#D95757" // Error color
// Active state colors
darkIndentActive := "#6C7380" // Active indent guides
theme := &AyuTheme{}
// Base colors
theme.PrimaryColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkEntity),
Light: lipgloss.Color(darkEntity),
}
theme.SecondaryColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkConstant),
Light: lipgloss.Color(darkConstant),
}
theme.AccentColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkAccent),
Light: lipgloss.Color(darkAccent),
}
// Status colors
theme.ErrorColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkError),
Light: lipgloss.Color(darkError),
}
theme.WarningColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkSpecial),
Light: lipgloss.Color(darkSpecial),
}
theme.SuccessColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkAdded),
Light: lipgloss.Color(darkAdded),
}
theme.InfoColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkTag),
Light: lipgloss.Color(darkTag),
}
// Text colors
theme.TextColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkFg),
Light: lipgloss.Color(darkFg),
}
theme.TextMutedColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkFgMuted),
Light: lipgloss.Color(darkFgMuted),
}
// Background colors
theme.BackgroundColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkBg),
Light: lipgloss.Color(darkBg),
}
theme.BackgroundPanelColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkBgAlt),
Light: lipgloss.Color(darkBgAlt),
}
theme.BackgroundElementColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkPanel),
Light: lipgloss.Color(darkPanel),
}
// Border colors
theme.BorderColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkGutter),
Light: lipgloss.Color(darkGutter),
}
theme.BorderActiveColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkIndentActive),
Light: lipgloss.Color(darkIndentActive),
}
theme.BorderSubtleColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkLine),
Light: lipgloss.Color(darkLine),
}
// Diff view colors
theme.DiffAddedColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkAdded),
Light: lipgloss.Color(darkAdded),
}
theme.DiffRemovedColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkRemoved),
Light: lipgloss.Color(darkRemoved),
}
theme.DiffContextColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkFgMuted),
Light: lipgloss.Color(darkFgMuted),
}
theme.DiffHunkHeaderColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkGutter),
Light: lipgloss.Color(darkGutter),
}
theme.DiffHighlightAddedColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkAdded),
Light: lipgloss.Color(darkAdded),
}
theme.DiffHighlightRemovedColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkRemoved),
Light: lipgloss.Color(darkRemoved),
}
theme.DiffAddedBgColor = compat.AdaptiveColor{
Dark: lipgloss.Color("#1a2b1a"),
Light: lipgloss.Color("#1a2b1a"),
}
theme.DiffRemovedBgColor = compat.AdaptiveColor{
Dark: lipgloss.Color("#2b1a1a"),
Light: lipgloss.Color("#2b1a1a"),
}
theme.DiffContextBgColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkBgAlt),
Light: lipgloss.Color(darkBgAlt),
}
theme.DiffLineNumberColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkGutter),
Light: lipgloss.Color(darkGutter),
}
theme.DiffAddedLineNumberBgColor = compat.AdaptiveColor{
Dark: lipgloss.Color("#152b15"),
Light: lipgloss.Color("#152b15"),
}
theme.DiffRemovedLineNumberBgColor = compat.AdaptiveColor{
Dark: lipgloss.Color("#2b1515"),
Light: lipgloss.Color("#2b1515"),
}
// Markdown colors
theme.MarkdownTextColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkFg),
Light: lipgloss.Color(darkFg),
}
theme.MarkdownHeadingColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkFunc),
Light: lipgloss.Color(darkFunc),
}
theme.MarkdownLinkColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkTag),
Light: lipgloss.Color(darkTag),
}
theme.MarkdownLinkTextColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkEntity),
Light: lipgloss.Color(darkEntity),
}
theme.MarkdownCodeColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkString),
Light: lipgloss.Color(darkString),
}
theme.MarkdownBlockQuoteColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkSpecial),
Light: lipgloss.Color(darkSpecial),
}
theme.MarkdownEmphColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkKeyword),
Light: lipgloss.Color(darkKeyword),
}
theme.MarkdownStrongColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkMarkup),
Light: lipgloss.Color(darkMarkup),
}
theme.MarkdownHorizontalRuleColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkGutter),
Light: lipgloss.Color(darkGutter),
}
theme.MarkdownListItemColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkOperator),
Light: lipgloss.Color(darkOperator),
}
theme.MarkdownListEnumerationColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkConstant),
Light: lipgloss.Color(darkConstant),
}
theme.MarkdownImageColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkRegexp),
Light: lipgloss.Color(darkRegexp),
}
theme.MarkdownImageTextColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkEntity),
Light: lipgloss.Color(darkEntity),
}
theme.MarkdownCodeBlockColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkString),
Light: lipgloss.Color(darkString),
}
// Syntax highlighting colors
theme.SyntaxCommentColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkComment),
Light: lipgloss.Color(darkComment),
}
theme.SyntaxKeywordColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkKeyword),
Light: lipgloss.Color(darkKeyword),
}
theme.SyntaxFunctionColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkFunc),
Light: lipgloss.Color(darkFunc),
}
theme.SyntaxVariableColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkEntity),
Light: lipgloss.Color(darkEntity),
}
theme.SyntaxStringColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkString),
Light: lipgloss.Color(darkString),
}
theme.SyntaxNumberColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkConstant),
Light: lipgloss.Color(darkConstant),
}
theme.SyntaxTypeColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkSpecial),
Light: lipgloss.Color(darkSpecial),
}
theme.SyntaxOperatorColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkOperator),
Light: lipgloss.Color(darkOperator),
}
theme.SyntaxPunctuationColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkFg),
Light: lipgloss.Color(darkFg),
}
return theme
}
func init() {
// Register the Ayu theme with the theme manager
RegisterTheme("ayu", NewAyuTheme())
}
-298
View File
@@ -1,298 +0,0 @@
package theme
import (
"github.com/charmbracelet/lipgloss/v2"
"github.com/charmbracelet/lipgloss/v2/compat"
)
// EverforestTheme implements the Theme interface with Everforest colors.
// It provides both dark and light variants with Medium (default) contrast.
type EverforestTheme struct {
BaseTheme
}
// NewEverforestTheme creates a new instance of the Everforest Medium theme.
func NewEverforestTheme() *EverforestTheme {
// Everforest color palette - Medium variant
// Official colors from https://github.com/sainnhe/everforest/wiki
// Dark mode colors - using Everforest:Dark Medium contrast palette
darkStep1 := "#2d353b" // App background
darkStep2 := "#333c43" // Subtle background
darkStep3 := "#343f44" // UI element background
darkStep4 := "#3d484d" // Hovered UI element background
darkStep5 := "#475258" // Active/Selected UI element background
darkStep6 := "#7a8478" // Subtle borders and separators
darkStep7 := "#859289" // UI element border and focus rings
darkStep8 := "#9da9a0" // Hovered UI element border
darkStep9 := "#a7c080" // Solid backgrounds
darkStep10 := "#83c092" // Hovered solid backgrounds
darkStep11 := "#7a8478" // Low-contrast text
darkStep12 := "#d3c6aa" // High-contrast text
// Dark mode accent colors
darkPrimary := darkStep9 // Primary uses step 9 (green)
darkSecondary := "#7fbbb3" // Secondary (blue)
darkAccent := "#d699b6" // Accent (purple)
darkRed := "#e67e80" // Error (red)
darkOrange := "#e69875" // Warning (orange)
darkGreen := "#a7c080" // Success (green)
darkCyan := "#83c092" // Info (aqua)
darkYellow := "#dbbc7f" // Emphasized text
// Light mode colors for the Everforest:Light Medium contrast palette
lightStep1 := "#fdf6e3" // App background
lightStep2 := "#efebd4" // Subtle background
lightStep3 := "#f4f0d9" // UI element background
lightStep4 := "#efebd4" // Hovered UI element background
lightStep5 := "#e6e2cc" // Active/Selected UI element background
lightStep6 := "#a6b0a0" // Subtle borders and separators
lightStep7 := "#939f91" // UI element border and focus rings
lightStep8 := "#829181" // Hovered UI element border
lightStep9 := "#8da101" // Solid backgrounds
lightStep10 := "#35a77c" // Hovered solid backgrounds
lightStep11 := "#a6b0a0" // Low-contrast text
lightStep12 := "#5c6a72" // High-contrast text
// Light mode accent colors
lightPrimary := lightStep9 // Primary uses step 9 (green)
lightSecondary := "#3a94c5" // Secondary blue
lightAccent := "#df69ba" // Accent purple
lightRed := "#f85552" // Error red
lightOrange := "#f57d26" // Warning orange
lightGreen := "#8da101" // Success green
lightCyan := "#35a77c" // Info aqua
lightYellow := "#dfa000" // Emphasized text
// Unused variables. These could be used for hover states
_ = darkStep4
_ = darkStep5
_ = darkStep10
_ = lightStep4
_ = lightStep5
_ = lightStep10
theme := &EverforestTheme{}
// Base colors
theme.PrimaryColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkPrimary),
Light: lipgloss.Color(lightPrimary),
}
theme.SecondaryColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkSecondary),
Light: lipgloss.Color(lightSecondary),
}
theme.AccentColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkAccent),
Light: lipgloss.Color(lightAccent),
}
// Status colors
theme.ErrorColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkRed),
Light: lipgloss.Color(lightRed),
}
theme.WarningColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkOrange),
Light: lipgloss.Color(lightOrange),
}
theme.SuccessColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkGreen),
Light: lipgloss.Color(lightGreen),
}
theme.InfoColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkCyan),
Light: lipgloss.Color(lightCyan),
}
// Text colors
theme.TextColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkStep12),
Light: lipgloss.Color(lightStep12),
}
theme.TextMutedColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkStep11),
Light: lipgloss.Color(lightStep11),
}
// Background colors
theme.BackgroundColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkStep1),
Light: lipgloss.Color(lightStep1),
}
theme.BackgroundPanelColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkStep2),
Light: lipgloss.Color(lightStep2),
}
theme.BackgroundElementColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkStep3),
Light: lipgloss.Color(lightStep3),
}
// Border colors
theme.BorderColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkStep7),
Light: lipgloss.Color(lightStep7),
}
theme.BorderActiveColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkStep8),
Light: lipgloss.Color(lightStep8),
}
theme.BorderSubtleColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkStep6),
Light: lipgloss.Color(lightStep6),
}
// Diff view colors
theme.DiffAddedColor = compat.AdaptiveColor{
Dark: lipgloss.Color("#A7C080"),
Light: lipgloss.Color("#8DA101"),
}
theme.DiffRemovedColor = compat.AdaptiveColor{
Dark: lipgloss.Color("#E67E80"),
Light: lipgloss.Color("#F85552"),
}
theme.DiffContextColor = compat.AdaptiveColor{
Dark: lipgloss.Color("#7A8478"),
Light: lipgloss.Color("#A6B0A0"),
}
theme.DiffHunkHeaderColor = compat.AdaptiveColor{
Dark: lipgloss.Color("#859289"),
Light: lipgloss.Color("#939F91"),
}
theme.DiffHighlightAddedColor = compat.AdaptiveColor{
Dark: lipgloss.Color("#A7C080"),
Light: lipgloss.Color("#8DA101"),
}
theme.DiffHighlightRemovedColor = compat.AdaptiveColor{
Dark: lipgloss.Color("#E67E80"),
Light: lipgloss.Color("#F85552"),
}
theme.DiffAddedBgColor = compat.AdaptiveColor{
Dark: lipgloss.Color("#425047"),
Light: lipgloss.Color("#F0F1D2"),
}
theme.DiffRemovedBgColor = compat.AdaptiveColor{
Dark: lipgloss.Color("#543A48"),
Light: lipgloss.Color("#FBE3DA"),
}
theme.DiffContextBgColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkStep2),
Light: lipgloss.Color(lightStep2),
}
theme.DiffLineNumberColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkStep3),
Light: lipgloss.Color(lightStep3),
}
theme.DiffAddedLineNumberBgColor = compat.AdaptiveColor{
Dark: lipgloss.Color("#3A4A3F"),
Light: lipgloss.Color("#E8F2D1"),
}
theme.DiffRemovedLineNumberBgColor = compat.AdaptiveColor{
Dark: lipgloss.Color("#4A3A40"),
Light: lipgloss.Color("#FBDAD2"),
}
// Markdown colors
theme.MarkdownTextColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkStep12),
Light: lipgloss.Color(lightStep12),
}
theme.MarkdownHeadingColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkSecondary),
Light: lipgloss.Color(lightSecondary),
}
theme.MarkdownLinkColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkPrimary),
Light: lipgloss.Color(lightPrimary),
}
theme.MarkdownLinkTextColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkCyan),
Light: lipgloss.Color(lightCyan),
}
theme.MarkdownCodeColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkGreen),
Light: lipgloss.Color(lightGreen),
}
theme.MarkdownBlockQuoteColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkYellow),
Light: lipgloss.Color(lightYellow),
}
theme.MarkdownEmphColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkYellow),
Light: lipgloss.Color(lightYellow),
}
theme.MarkdownStrongColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkAccent),
Light: lipgloss.Color(lightAccent),
}
theme.MarkdownHorizontalRuleColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkStep11),
Light: lipgloss.Color(lightStep11),
}
theme.MarkdownListItemColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkPrimary),
Light: lipgloss.Color(lightPrimary),
}
theme.MarkdownListEnumerationColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkCyan),
Light: lipgloss.Color(lightCyan),
}
theme.MarkdownImageColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkPrimary),
Light: lipgloss.Color(lightPrimary),
}
theme.MarkdownImageTextColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkCyan),
Light: lipgloss.Color(lightCyan),
}
theme.MarkdownCodeBlockColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkStep12),
Light: lipgloss.Color(lightStep12),
}
// Syntax highlighting colors
theme.SyntaxCommentColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkStep11),
Light: lipgloss.Color(lightStep11),
}
theme.SyntaxKeywordColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkPrimary),
Light: lipgloss.Color(lightPrimary),
}
theme.SyntaxFunctionColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkSecondary),
Light: lipgloss.Color(lightSecondary),
}
theme.SyntaxVariableColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkRed),
Light: lipgloss.Color(lightRed),
}
theme.SyntaxStringColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkGreen),
Light: lipgloss.Color(lightGreen),
}
theme.SyntaxNumberColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkAccent),
Light: lipgloss.Color(lightAccent),
}
theme.SyntaxTypeColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkYellow),
Light: lipgloss.Color(lightYellow),
}
theme.SyntaxOperatorColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkCyan),
Light: lipgloss.Color(lightCyan),
}
theme.SyntaxPunctuationColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkStep12),
Light: lipgloss.Color(lightStep12),
}
return theme
}
func init() {
// Register the Everforest theme with the theme manager
RegisterTheme("everforest", NewEverforestTheme())
}
+394
View File
@@ -0,0 +1,394 @@
package theme
import (
"embed"
"encoding/json"
"fmt"
"image/color"
"os"
"path/filepath"
"strings"
"github.com/charmbracelet/lipgloss/v2"
"github.com/charmbracelet/lipgloss/v2/compat"
)
//go:embed themes/*.json
var themesFS embed.FS
type JSONTheme struct {
Defs map[string]any `json:"defs,omitempty"`
Theme map[string]any `json:"theme"`
}
type LoadedTheme struct {
BaseTheme
name string
}
type colorRef struct {
value any
resolved bool
}
func LoadThemesFromJSON() error {
entries, err := themesFS.ReadDir("themes")
if err != nil {
return fmt.Errorf("failed to read themes directory: %w", err)
}
for _, entry := range entries {
if !strings.HasSuffix(entry.Name(), ".json") {
continue
}
themeName := strings.TrimSuffix(entry.Name(), ".json")
data, err := themesFS.ReadFile(filepath.Join("themes", entry.Name()))
if err != nil {
return fmt.Errorf("failed to read theme file %s: %w", entry.Name(), err)
}
theme, err := parseJSONTheme(themeName, data)
if err != nil {
return fmt.Errorf("failed to parse theme %s: %w", themeName, err)
}
RegisterTheme(themeName, theme)
}
return nil
}
// LoadThemesFromDirectories loads themes from user directories in the correct override order.
// The hierarchy is (from lowest to highest priority):
// 1. Built-in themes (embedded)
// 2. USER_CONFIG/opencode/themes/*.json
// 3. PROJECT_ROOT/.opencode/themes/*.json
// 4. CWD/.opencode/themes/*.json
func LoadThemesFromDirectories(userConfig, projectRoot, cwd string) error {
if err := LoadThemesFromJSON(); err != nil {
return fmt.Errorf("failed to load built-in themes: %w", err)
}
dirs := []string{
filepath.Join(userConfig, "themes"),
filepath.Join(projectRoot, ".opencode", "themes"),
}
if cwd != projectRoot {
dirs = append(dirs, filepath.Join(cwd, ".opencode", "themes"))
}
for _, dir := range dirs {
if err := loadThemesFromDirectory(dir); err != nil {
fmt.Printf("Warning: Failed to load themes from %s: %v\n", dir, err)
}
}
return nil
}
func loadThemesFromDirectory(dir string) error {
if _, err := os.Stat(dir); os.IsNotExist(err) {
return nil // Directory doesn't exist, which is fine
}
entries, err := os.ReadDir(dir)
if err != nil {
return fmt.Errorf("failed to read directory: %w", err)
}
for _, entry := range entries {
if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".json") {
continue
}
themeName := strings.TrimSuffix(entry.Name(), ".json")
filePath := filepath.Join(dir, entry.Name())
data, err := os.ReadFile(filePath)
if err != nil {
fmt.Printf("Warning: Failed to read theme file %s: %v\n", filePath, err)
continue
}
theme, err := parseJSONTheme(themeName, data)
if err != nil {
fmt.Printf("Warning: Failed to parse theme %s: %v\n", filePath, err)
continue
}
RegisterTheme(themeName, theme)
}
return nil
}
func parseJSONTheme(name string, data []byte) (Theme, error) {
var jsonTheme JSONTheme
if err := json.Unmarshal(data, &jsonTheme); err != nil {
return nil, fmt.Errorf("failed to unmarshal JSON: %w", err)
}
theme := &LoadedTheme{
name: name,
}
colorMap := make(map[string]*colorRef)
for key, value := range jsonTheme.Defs {
colorMap[key] = &colorRef{value: value, resolved: false}
}
for key, value := range jsonTheme.Theme {
colorMap[key] = &colorRef{value: value, resolved: false}
}
resolver := &colorResolver{
colors: colorMap,
visited: make(map[string]bool),
}
for key, value := range jsonTheme.Theme {
resolved, err := resolver.resolveColor(key, value)
if err != nil {
return nil, fmt.Errorf("failed to resolve color %s: %w", key, err)
}
adaptiveColor, err := parseResolvedColor(resolved)
if err != nil {
return nil, fmt.Errorf("failed to parse color %s: %w", key, err)
}
if err := setThemeColor(theme, key, adaptiveColor); err != nil {
return nil, fmt.Errorf("failed to set color %s: %w", key, err)
}
}
return theme, nil
}
type colorResolver struct {
colors map[string]*colorRef
visited map[string]bool
}
func (r *colorResolver) resolveColor(key string, value any) (any, error) {
if r.visited[key] {
return nil, fmt.Errorf("circular reference detected for color %s", key)
}
r.visited[key] = true
defer func() { r.visited[key] = false }()
switch v := value.(type) {
case string:
if strings.HasPrefix(v, "#") {
return v, nil
}
return r.resolveReference(v)
case float64:
return v, nil
case map[string]any:
resolved := make(map[string]any)
if dark, ok := v["dark"]; ok {
resolvedDark, err := r.resolveColorValue(dark)
if err != nil {
return nil, fmt.Errorf("failed to resolve dark variant: %w", err)
}
resolved["dark"] = resolvedDark
}
if light, ok := v["light"]; ok {
resolvedLight, err := r.resolveColorValue(light)
if err != nil {
return nil, fmt.Errorf("failed to resolve light variant: %w", err)
}
resolved["light"] = resolvedLight
}
return resolved, nil
default:
return nil, fmt.Errorf("invalid color value type: %T", value)
}
}
func (r *colorResolver) resolveColorValue(value any) (any, error) {
switch v := value.(type) {
case string:
if strings.HasPrefix(v, "#") {
return v, nil
}
return r.resolveReference(v)
case float64:
return v, nil
default:
return nil, fmt.Errorf("invalid color value type: %T", value)
}
}
func (r *colorResolver) resolveReference(ref string) (any, error) {
colorRef, exists := r.colors[ref]
if !exists {
return nil, fmt.Errorf("color reference '%s' not found", ref)
}
if colorRef.resolved {
return colorRef.value, nil
}
resolved, err := r.resolveColor(ref, colorRef.value)
if err != nil {
return nil, err
}
colorRef.value = resolved
colorRef.resolved = true
return resolved, nil
}
func parseResolvedColor(value any) (compat.AdaptiveColor, error) {
switch v := value.(type) {
case string:
return compat.AdaptiveColor{
Dark: lipgloss.Color(v),
Light: lipgloss.Color(v),
}, nil
case float64:
colorStr := fmt.Sprintf("%d", int(v))
return compat.AdaptiveColor{
Dark: lipgloss.Color(colorStr),
Light: lipgloss.Color(colorStr),
}, nil
case map[string]any:
dark, darkOk := v["dark"]
light, lightOk := v["light"]
if !darkOk || !lightOk {
return compat.AdaptiveColor{}, fmt.Errorf("color object must have both 'dark' and 'light' keys")
}
darkColor, err := parseColorValue(dark)
if err != nil {
return compat.AdaptiveColor{}, fmt.Errorf("failed to parse dark color: %w", err)
}
lightColor, err := parseColorValue(light)
if err != nil {
return compat.AdaptiveColor{}, fmt.Errorf("failed to parse light color: %w", err)
}
return compat.AdaptiveColor{
Dark: darkColor,
Light: lightColor,
}, nil
default:
return compat.AdaptiveColor{}, fmt.Errorf("invalid resolved color type: %T", value)
}
}
func parseColorValue(value any) (color.Color, error) {
switch v := value.(type) {
case string:
return lipgloss.Color(v), nil
case float64:
return lipgloss.Color(fmt.Sprintf("%d", int(v))), nil
default:
return nil, fmt.Errorf("invalid color value type: %T", value)
}
}
func setThemeColor(theme *LoadedTheme, key string, color compat.AdaptiveColor) error {
switch key {
case "primary":
theme.PrimaryColor = color
case "secondary":
theme.SecondaryColor = color
case "accent":
theme.AccentColor = color
case "error":
theme.ErrorColor = color
case "warning":
theme.WarningColor = color
case "success":
theme.SuccessColor = color
case "info":
theme.InfoColor = color
case "text":
theme.TextColor = color
case "textMuted":
theme.TextMutedColor = color
case "background":
theme.BackgroundColor = color
case "backgroundPanel":
theme.BackgroundPanelColor = color
case "backgroundElement":
theme.BackgroundElementColor = color
case "border":
theme.BorderColor = color
case "borderActive":
theme.BorderActiveColor = color
case "borderSubtle":
theme.BorderSubtleColor = color
case "diffAdded":
theme.DiffAddedColor = color
case "diffRemoved":
theme.DiffRemovedColor = color
case "diffContext":
theme.DiffContextColor = color
case "diffHunkHeader":
theme.DiffHunkHeaderColor = color
case "diffHighlightAdded":
theme.DiffHighlightAddedColor = color
case "diffHighlightRemoved":
theme.DiffHighlightRemovedColor = color
case "diffAddedBg":
theme.DiffAddedBgColor = color
case "diffRemovedBg":
theme.DiffRemovedBgColor = color
case "diffContextBg":
theme.DiffContextBgColor = color
case "diffLineNumber":
theme.DiffLineNumberColor = color
case "diffAddedLineNumberBg":
theme.DiffAddedLineNumberBgColor = color
case "diffRemovedLineNumberBg":
theme.DiffRemovedLineNumberBgColor = color
case "markdownText":
theme.MarkdownTextColor = color
case "markdownHeading":
theme.MarkdownHeadingColor = color
case "markdownLink":
theme.MarkdownLinkColor = color
case "markdownLinkText":
theme.MarkdownLinkTextColor = color
case "markdownCode":
theme.MarkdownCodeColor = color
case "markdownBlockQuote":
theme.MarkdownBlockQuoteColor = color
case "markdownEmph":
theme.MarkdownEmphColor = color
case "markdownStrong":
theme.MarkdownStrongColor = color
case "markdownHorizontalRule":
theme.MarkdownHorizontalRuleColor = color
case "markdownListItem":
theme.MarkdownListItemColor = color
case "markdownListEnumeration":
theme.MarkdownListEnumerationColor = color
case "markdownImage":
theme.MarkdownImageColor = color
case "markdownImageText":
theme.MarkdownImageTextColor = color
case "markdownCodeBlock":
theme.MarkdownCodeBlockColor = color
case "syntaxComment":
theme.SyntaxCommentColor = color
case "syntaxKeyword":
theme.SyntaxKeywordColor = color
case "syntaxFunction":
theme.SyntaxFunctionColor = color
case "syntaxVariable":
theme.SyntaxVariableColor = color
case "syntaxString":
theme.SyntaxStringColor = color
case "syntaxNumber":
theme.SyntaxNumberColor = color
case "syntaxType":
theme.SyntaxTypeColor = color
case "syntaxOperator":
theme.SyntaxOperatorColor = color
case "syntaxPunctuation":
theme.SyntaxPunctuationColor = color
default:
// Ignore unknown keys for forward compatibility
return nil
}
return nil
}
+136
View File
@@ -0,0 +1,136 @@
package theme
import (
"os"
"path/filepath"
"slices"
"testing"
)
func TestLoadThemesFromJSON(t *testing.T) {
// Test loading themes
err := LoadThemesFromJSON()
if err != nil {
t.Fatalf("Failed to load themes: %v", err)
}
// Check that themes were loaded
themes := AvailableThemes()
if len(themes) == 0 {
t.Fatal("No themes were loaded")
}
// Check for expected themes
expectedThemes := []string{"tokyonight", "opencode", "everforest", "ayu", "example"}
for _, expected := range expectedThemes {
found := slices.Contains(themes, expected)
if !found {
t.Errorf("Expected theme %s not found", expected)
}
}
// Test getting a specific theme
tokyonight := GetTheme("tokyonight")
if tokyonight == nil {
t.Fatal("Failed to get tokyonight theme")
}
// Test theme colors
primary := tokyonight.Primary()
if primary.Dark == nil || primary.Light == nil {
t.Error("Primary color not properly set")
}
}
func TestColorReferenceResolution(t *testing.T) {
// Test the example theme which uses references
example := GetTheme("example")
if example == nil {
t.Fatal("Failed to get example theme")
}
// Check that brandBlue reference was resolved
primary := example.Primary()
if primary.Dark == nil || primary.Light == nil {
t.Error("Primary color (brandBlue reference) not resolved")
}
// Check that nested reference (borderActive -> primary -> brandBlue) works
borderActive := example.BorderActive()
if borderActive.Dark == nil || borderActive.Light == nil {
t.Error("BorderActive color (nested reference) not resolved")
}
}
func TestLoadThemesFromDirectories(t *testing.T) {
// Create temporary directories for testing
tempDir := t.TempDir()
userConfig := filepath.Join(tempDir, "config")
projectRoot := filepath.Join(tempDir, "project")
cwd := filepath.Join(tempDir, "cwd")
// Create theme directories
os.MkdirAll(filepath.Join(userConfig, "opencode", "themes"), 0755)
os.MkdirAll(filepath.Join(projectRoot, ".opencode", "themes"), 0755)
os.MkdirAll(filepath.Join(cwd, ".opencode", "themes"), 0755)
// Create test themes with same name to test override behavior
testTheme1 := `{
"theme": {
"primary": "#111111",
"secondary": "#222222",
"accent": "#333333",
"text": "#ffffff",
"textMuted": "#cccccc",
"background": "#000000"
}
}`
testTheme2 := `{
"theme": {
"primary": "#444444",
"secondary": "#555555",
"accent": "#666666",
"text": "#ffffff",
"textMuted": "#cccccc",
"background": "#000000"
}
}`
testTheme3 := `{
"theme": {
"primary": "#777777",
"secondary": "#888888",
"accent": "#999999",
"text": "#ffffff",
"textMuted": "#cccccc",
"background": "#000000"
}
}`
// Write themes to different directories
os.WriteFile(filepath.Join(userConfig, "opencode", "themes", "override-test.json"), []byte(testTheme1), 0644)
os.WriteFile(filepath.Join(projectRoot, ".opencode", "themes", "override-test.json"), []byte(testTheme2), 0644)
os.WriteFile(filepath.Join(cwd, ".opencode", "themes", "override-test.json"), []byte(testTheme3), 0644)
// Load themes
err := LoadThemesFromDirectories(userConfig, projectRoot, cwd)
if err != nil {
t.Fatalf("Failed to load themes from directories: %v", err)
}
// Check that the theme from CWD (highest priority) won
overrideTheme := GetTheme("override-test")
if overrideTheme == nil {
t.Fatal("Failed to get override-test theme")
}
// The primary color should be from testTheme3 (#777777)
primary := overrideTheme.Primary()
// We can't directly check the color value, but we can verify it was loaded
if primary.Dark == nil || primary.Light == nil {
t.Error("Override theme not properly loaded")
}
}
+1 -132
View File
@@ -2,13 +2,11 @@ package theme
import (
"fmt"
"log/slog"
"slices"
"strings"
"sync"
"github.com/alecthomas/chroma/v2/styles"
// "github.com/alecthomas/chroma/v2/styles"
)
// Manager handles theme registration, selection, and retrieval.
@@ -25,9 +23,6 @@ var globalManager = &Manager{
currentName: "",
}
// Default theme instance for custom theme defaulting
var defaultThemeColors = NewOpenCodeTheme()
// 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) {
@@ -89,6 +84,7 @@ func AvailableThemes() []string {
names = append(names, name)
}
slices.SortFunc(names, func(a, b string) int {
// list system theme first
if a == "opencode" {
return -1
} else if b == "opencode" {
@@ -107,130 +103,3 @@ func GetTheme(name string) Theme {
return globalManager.themes[name]
}
// LoadCustomTheme creates a new theme instance based on the custom theme colors
// defined in the configuration. It uses the default OpenCode theme as a base
// and overrides colors that are specified in the customTheme map.
func LoadCustomTheme(customTheme map[string]any) (Theme, error) {
// Create a new theme based on the default OpenCode theme
theme := NewOpenCodeTheme()
// Process each color in the custom theme map
for key, value := range customTheme {
adaptiveColor, err := ParseAdaptiveColor(value)
if err != nil {
slog.Warn("Invalid color definition in custom theme", "key", key, "error", err)
continue // Skip this color but continue processing others
}
// Set the color in the theme based on the key
switch strings.ToLower(key) {
case "primary":
theme.PrimaryColor = adaptiveColor
case "secondary":
theme.SecondaryColor = adaptiveColor
case "accent":
theme.AccentColor = adaptiveColor
case "error":
theme.ErrorColor = adaptiveColor
case "warning":
theme.WarningColor = adaptiveColor
case "success":
theme.SuccessColor = adaptiveColor
case "info":
theme.InfoColor = adaptiveColor
case "text":
theme.TextColor = adaptiveColor
case "textmuted":
theme.TextMutedColor = adaptiveColor
case "background":
theme.BackgroundColor = adaptiveColor
case "backgroundsubtle":
theme.BackgroundPanelColor = adaptiveColor
case "backgroundelement":
theme.BackgroundElementColor = adaptiveColor
case "border":
theme.BorderColor = adaptiveColor
case "borderactive":
theme.BorderActiveColor = adaptiveColor
case "bordersubtle":
theme.BorderSubtleColor = adaptiveColor
case "diffadded":
theme.DiffAddedColor = adaptiveColor
case "diffremoved":
theme.DiffRemovedColor = adaptiveColor
case "diffcontext":
theme.DiffContextColor = adaptiveColor
case "diffhunkheader":
theme.DiffHunkHeaderColor = adaptiveColor
case "diffhighlightadded":
theme.DiffHighlightAddedColor = adaptiveColor
case "diffhighlightremoved":
theme.DiffHighlightRemovedColor = adaptiveColor
case "diffaddedbg":
theme.DiffAddedBgColor = adaptiveColor
case "diffremovedbg":
theme.DiffRemovedBgColor = adaptiveColor
case "diffcontextbg":
theme.DiffContextBgColor = adaptiveColor
case "difflinenumber":
theme.DiffLineNumberColor = adaptiveColor
case "diffaddedlinenumberbg":
theme.DiffAddedLineNumberBgColor = adaptiveColor
case "diffremovedlinenumberbg":
theme.DiffRemovedLineNumberBgColor = adaptiveColor
case "syntaxcomment":
theme.SyntaxCommentColor = adaptiveColor
case "syntaxkeyword":
theme.SyntaxKeywordColor = adaptiveColor
case "syntaxfunction":
theme.SyntaxFunctionColor = adaptiveColor
case "syntaxvariable":
theme.SyntaxVariableColor = adaptiveColor
case "syntaxstring":
theme.SyntaxStringColor = adaptiveColor
case "syntaxnumber":
theme.SyntaxNumberColor = adaptiveColor
case "syntaxtype":
theme.SyntaxTypeColor = adaptiveColor
case "syntaxoperator":
theme.SyntaxOperatorColor = adaptiveColor
case "syntaxpunctuation":
theme.SyntaxPunctuationColor = adaptiveColor
case "markdowntext":
theme.MarkdownTextColor = adaptiveColor
case "markdownheading":
theme.MarkdownHeadingColor = adaptiveColor
case "markdownlink":
theme.MarkdownLinkColor = adaptiveColor
case "markdownlinktext":
theme.MarkdownLinkTextColor = adaptiveColor
case "markdowncode":
theme.MarkdownCodeColor = adaptiveColor
case "markdownblockquote":
theme.MarkdownBlockQuoteColor = adaptiveColor
case "markdownemph":
theme.MarkdownEmphColor = adaptiveColor
case "markdownstrong":
theme.MarkdownStrongColor = adaptiveColor
case "markdownhorizontalrule":
theme.MarkdownHorizontalRuleColor = adaptiveColor
case "markdownlistitem":
theme.MarkdownListItemColor = adaptiveColor
case "markdownlistitemenum":
theme.MarkdownListEnumerationColor = adaptiveColor
case "markdownimage":
theme.MarkdownImageColor = adaptiveColor
case "markdownimagetext":
theme.MarkdownImageTextColor = adaptiveColor
case "markdowncodeblock":
theme.MarkdownCodeBlockColor = adaptiveColor
case "markdownlistenumeration":
theme.MarkdownListEnumerationColor = adaptiveColor
default:
slog.Warn("Unknown color key in custom theme", "key", key)
}
}
return theme, nil
}
-297
View File
@@ -1,297 +0,0 @@
package theme
import (
"github.com/charmbracelet/lipgloss/v2"
"github.com/charmbracelet/lipgloss/v2/compat"
)
// 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 with Radix-inspired scale progression
// Dark mode colors - using a neutral gray scale as base
darkStep1 := "#0a0a0a" // App background
darkStep2 := "#141414" // Subtle background
darkStep3 := "#1e1e1e" // UI element background
darkStep4 := "#282828" // Hovered UI element background
darkStep5 := "#323232" // Active/Selected UI element background
darkStep6 := "#3c3c3c" // Subtle borders and separators
darkStep7 := "#484848" // UI element border and focus rings
darkStep8 := "#606060" // Hovered UI element border
darkStep9 := "#fab283" // Solid backgrounds (primary orange/gold)
darkStep10 := "#ffc09f" // Hovered solid backgrounds
darkStep11 := "#808080" // Low-contrast text (more muted)
darkStep12 := "#eeeeee" // High-contrast text
// Dark mode accent colors
darkPrimary := darkStep9 // Primary uses step 9 (solid background)
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
// Light mode colors - using a neutral gray scale as base
lightStep1 := "#ffffff" // App background
lightStep2 := "#fafafa" // Subtle background
lightStep3 := "#f5f5f5" // UI element background
lightStep4 := "#ebebeb" // Hovered UI element background
lightStep5 := "#e1e1e1" // Active/Selected UI element background
lightStep6 := "#d4d4d4" // Subtle borders and separators
lightStep7 := "#b8b8b8" // UI element border and focus rings
lightStep8 := "#a0a0a0" // Hovered UI element border
lightStep9 := "#3b7dd8" // Solid backgrounds (primary blue)
lightStep10 := "#2968c3" // Hovered solid backgrounds
lightStep11 := "#8a8a8a" // Low-contrast text (more muted)
lightStep12 := "#1a1a1a" // High-contrast text
// Light mode accent colors
lightPrimary := lightStep9 // Primary uses step 9 (solid background)
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
// Unused variables to avoid compiler errors (these could be used for hover states)
_ = darkStep4
_ = darkStep5
_ = darkStep10
_ = lightStep4
_ = lightStep5
_ = lightStep10
theme := &OpenCodeTheme{}
// Base colors
theme.PrimaryColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkPrimary),
Light: lipgloss.Color(lightPrimary),
}
theme.SecondaryColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkSecondary),
Light: lipgloss.Color(lightSecondary),
}
theme.AccentColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkAccent),
Light: lipgloss.Color(lightAccent),
}
// Status colors
theme.ErrorColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkRed),
Light: lipgloss.Color(lightRed),
}
theme.WarningColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkOrange),
Light: lipgloss.Color(lightOrange),
}
theme.SuccessColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkGreen),
Light: lipgloss.Color(lightGreen),
}
theme.InfoColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkCyan),
Light: lipgloss.Color(lightCyan),
}
// Text colors
theme.TextColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkStep12),
Light: lipgloss.Color(lightStep12),
}
theme.TextMutedColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkStep11),
Light: lipgloss.Color(lightStep11),
}
// Background colors
theme.BackgroundColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkStep1),
Light: lipgloss.Color(lightStep1),
}
theme.BackgroundPanelColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkStep2),
Light: lipgloss.Color(lightStep2),
}
theme.BackgroundElementColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkStep3),
Light: lipgloss.Color(lightStep3),
}
// Border colors
theme.BorderColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkStep7),
Light: lipgloss.Color(lightStep7),
}
theme.BorderActiveColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkStep8),
Light: lipgloss.Color(lightStep8),
}
theme.BorderSubtleColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkStep6),
Light: lipgloss.Color(lightStep6),
}
// Diff view colors
theme.DiffAddedColor = compat.AdaptiveColor{
Dark: lipgloss.Color("#478247"),
Light: lipgloss.Color("#2E7D32"),
}
theme.DiffRemovedColor = compat.AdaptiveColor{
Dark: lipgloss.Color("#7C4444"),
Light: lipgloss.Color("#C62828"),
}
theme.DiffContextColor = compat.AdaptiveColor{
Dark: lipgloss.Color("#a0a0a0"),
Light: lipgloss.Color("#757575"),
}
theme.DiffHunkHeaderColor = compat.AdaptiveColor{
Dark: lipgloss.Color("#a0a0a0"),
Light: lipgloss.Color("#757575"),
}
theme.DiffHighlightAddedColor = compat.AdaptiveColor{
Dark: lipgloss.Color("#DAFADA"),
Light: lipgloss.Color("#A5D6A7"),
}
theme.DiffHighlightRemovedColor = compat.AdaptiveColor{
Dark: lipgloss.Color("#FADADD"),
Light: lipgloss.Color("#EF9A9A"),
}
theme.DiffAddedBgColor = compat.AdaptiveColor{
Dark: lipgloss.Color("#303A30"),
Light: lipgloss.Color("#E8F5E9"),
}
theme.DiffRemovedBgColor = compat.AdaptiveColor{
Dark: lipgloss.Color("#3A3030"),
Light: lipgloss.Color("#FFEBEE"),
}
theme.DiffContextBgColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkStep2),
Light: lipgloss.Color(lightStep2),
}
theme.DiffLineNumberColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkStep3),
Light: lipgloss.Color(lightStep3),
}
theme.DiffAddedLineNumberBgColor = compat.AdaptiveColor{
Dark: lipgloss.Color("#293229"),
Light: lipgloss.Color("#C8E6C9"),
}
theme.DiffRemovedLineNumberBgColor = compat.AdaptiveColor{
Dark: lipgloss.Color("#332929"),
Light: lipgloss.Color("#FFCDD2"),
}
// Markdown colors
theme.MarkdownTextColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkStep12),
Light: lipgloss.Color(lightStep12),
}
theme.MarkdownHeadingColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkSecondary),
Light: lipgloss.Color(lightSecondary),
}
theme.MarkdownLinkColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkPrimary),
Light: lipgloss.Color(lightPrimary),
}
theme.MarkdownLinkTextColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkCyan),
Light: lipgloss.Color(lightCyan),
}
theme.MarkdownCodeColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkGreen),
Light: lipgloss.Color(lightGreen),
}
theme.MarkdownBlockQuoteColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkYellow),
Light: lipgloss.Color(lightYellow),
}
theme.MarkdownEmphColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkYellow),
Light: lipgloss.Color(lightYellow),
}
theme.MarkdownStrongColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkAccent),
Light: lipgloss.Color(lightAccent),
}
theme.MarkdownHorizontalRuleColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkStep11),
Light: lipgloss.Color(lightStep11),
}
theme.MarkdownListItemColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkPrimary),
Light: lipgloss.Color(lightPrimary),
}
theme.MarkdownListEnumerationColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkCyan),
Light: lipgloss.Color(lightCyan),
}
theme.MarkdownImageColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkPrimary),
Light: lipgloss.Color(lightPrimary),
}
theme.MarkdownImageTextColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkCyan),
Light: lipgloss.Color(lightCyan),
}
theme.MarkdownCodeBlockColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkStep12),
Light: lipgloss.Color(lightStep12),
}
// Syntax highlighting colors
theme.SyntaxCommentColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkStep11),
Light: lipgloss.Color(lightStep11),
}
theme.SyntaxKeywordColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkPrimary),
Light: lipgloss.Color(lightPrimary),
}
theme.SyntaxFunctionColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkPrimary),
Light: lipgloss.Color(lightPrimary),
}
theme.SyntaxVariableColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkRed),
Light: lipgloss.Color(lightRed),
}
theme.SyntaxStringColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkGreen),
Light: lipgloss.Color(lightGreen),
}
theme.SyntaxNumberColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkAccent),
Light: lipgloss.Color(lightAccent),
}
theme.SyntaxTypeColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkYellow),
Light: lipgloss.Color(lightYellow),
}
theme.SyntaxOperatorColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkCyan),
Light: lipgloss.Color(lightCyan),
}
theme.SyntaxPunctuationColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkStep12),
Light: lipgloss.Color(lightStep12),
}
return theme
}
func init() {
// Register the OpenCode theme with the theme manager
RegisterTheme("opencode", NewOpenCodeTheme())
}
-75
View File
@@ -1,10 +1,6 @@
package theme
import (
"fmt"
"regexp"
"github.com/charmbracelet/lipgloss/v2"
"github.com/charmbracelet/lipgloss/v2/compat"
)
@@ -215,74 +211,3 @@ func (t *BaseTheme) SyntaxNumber() compat.AdaptiveColor { return t.SyntaxNu
func (t *BaseTheme) SyntaxType() compat.AdaptiveColor { return t.SyntaxTypeColor }
func (t *BaseTheme) SyntaxOperator() compat.AdaptiveColor { return t.SyntaxOperatorColor }
func (t *BaseTheme) SyntaxPunctuation() compat.AdaptiveColor { return t.SyntaxPunctuationColor }
// ParseAdaptiveColor parses a color value from the config file into a compat.AdaptiveColor.
// It accepts either a string (hex color) or a map with "dark" and "light" keys.
func ParseAdaptiveColor(value any) (compat.AdaptiveColor, error) {
// Regular expression to validate hex color format
hexColorRegex := regexp.MustCompile(`^#[0-9a-fA-F]{6}$`)
// Case 1: String value (same color for both dark and light modes)
if hexColor, ok := value.(string); ok {
if !hexColorRegex.MatchString(hexColor) {
return compat.AdaptiveColor{}, fmt.Errorf("invalid hex color format: %s", hexColor)
}
return compat.AdaptiveColor{
Dark: lipgloss.Color(hexColor),
Light: lipgloss.Color(hexColor),
}, nil
}
// Case 2: Int value between 0 and 255
if numericVal, ok := value.(float64); ok {
intVal := int(numericVal)
if intVal < 0 || intVal > 255 {
return compat.AdaptiveColor{}, fmt.Errorf("invalid int color value (must be between 0 and 255): %d", intVal)
}
return compat.AdaptiveColor{
Dark: lipgloss.Color(fmt.Sprintf("%d", intVal)),
Light: lipgloss.Color(fmt.Sprintf("%d", intVal)),
}, nil
}
// Case 3: Map with dark and light keys
if colorMap, ok := value.(map[string]any); ok {
darkVal, darkOk := colorMap["dark"]
lightVal, lightOk := colorMap["light"]
if !darkOk || !lightOk {
return compat.AdaptiveColor{}, fmt.Errorf("color map must contain both 'dark' and 'light' keys")
}
darkHex, darkIsString := darkVal.(string)
lightHex, lightIsString := lightVal.(string)
if !darkIsString || !lightIsString {
darkVal, darkIsNumber := darkVal.(float64)
lightVal, lightIsNumber := lightVal.(float64)
if !darkIsNumber || !lightIsNumber {
return compat.AdaptiveColor{}, fmt.Errorf("color map values must be strings or ints")
}
darkInt := int(darkVal)
lightInt := int(lightVal)
return compat.AdaptiveColor{
Dark: lipgloss.Color(fmt.Sprintf("%d", darkInt)),
Light: lipgloss.Color(fmt.Sprintf("%d", lightInt)),
}, nil
}
if !hexColorRegex.MatchString(darkHex) || !hexColorRegex.MatchString(lightHex) {
return compat.AdaptiveColor{}, fmt.Errorf("invalid hex color format")
}
return compat.AdaptiveColor{
Dark: lipgloss.Color(darkHex),
Light: lipgloss.Color(lightHex),
}, nil
}
return compat.AdaptiveColor{}, fmt.Errorf("color must be either a hex string or an object with dark/light keys")
}
@@ -0,0 +1,81 @@
{
"$schema": "https://opencode.ai/theme.json",
"defs": {
"darkBg": "#0B0E14",
"darkBgAlt": "#0D1017",
"darkLine": "#11151C",
"darkPanel": "#0F131A",
"darkFg": "#BFBDB6",
"darkFgMuted": "#565B66",
"darkGutter": "#6C7380",
"darkTag": "#39BAE6",
"darkFunc": "#FFB454",
"darkEntity": "#59C2FF",
"darkString": "#AAD94C",
"darkRegexp": "#95E6CB",
"darkMarkup": "#F07178",
"darkKeyword": "#FF8F40",
"darkSpecial": "#E6B673",
"darkComment": "#ACB6BF",
"darkConstant": "#D2A6FF",
"darkOperator": "#F29668",
"darkAdded": "#7FD962",
"darkRemoved": "#F26D78",
"darkAccent": "#E6B450",
"darkError": "#D95757",
"darkIndentActive": "#6C7380"
},
"theme": {
"primary": "darkEntity",
"secondary": "darkConstant",
"accent": "darkAccent",
"error": "darkError",
"warning": "darkSpecial",
"success": "darkAdded",
"info": "darkTag",
"text": "darkFg",
"textMuted": "darkFgMuted",
"background": "darkBg",
"backgroundPanel": "darkPanel",
"backgroundElement": "darkBgAlt",
"border": "darkGutter",
"borderActive": "darkIndentActive",
"borderSubtle": "darkLine",
"diffAdded": "darkAdded",
"diffRemoved": "darkRemoved",
"diffContext": "darkComment",
"diffHunkHeader": "darkComment",
"diffHighlightAdded": "darkString",
"diffHighlightRemoved": "darkMarkup",
"diffAddedBg": "#20303b",
"diffRemovedBg": "#37222c",
"diffContextBg": "darkPanel",
"diffLineNumber": "darkGutter",
"diffAddedLineNumberBg": "#1b2b34",
"diffRemovedLineNumberBg": "#2d1f26",
"markdownText": "darkFg",
"markdownHeading": "darkConstant",
"markdownLink": "darkEntity",
"markdownLinkText": "darkTag",
"markdownCode": "darkString",
"markdownBlockQuote": "darkSpecial",
"markdownEmph": "darkSpecial",
"markdownStrong": "darkFunc",
"markdownHorizontalRule": "darkFgMuted",
"markdownListItem": "darkEntity",
"markdownListEnumeration": "darkTag",
"markdownImage": "darkEntity",
"markdownImageText": "darkTag",
"markdownCodeBlock": "darkFg",
"syntaxComment": "darkComment",
"syntaxKeyword": "darkKeyword",
"syntaxFunction": "darkFunc",
"syntaxVariable": "darkEntity",
"syntaxString": "darkString",
"syntaxNumber": "darkConstant",
"syntaxType": "darkSpecial",
"syntaxOperator": "darkOperator",
"syntaxPunctuation": "darkFg"
}
}
@@ -0,0 +1,113 @@
{
"$schema": "https://opencode.ai/theme.json",
"defs": {
"lightRosewater": "#dc8a78",
"lightFlamingo": "#dd7878",
"lightPink": "#ea76cb",
"lightMauve": "#8839ef",
"lightRed": "#d20f39",
"lightMaroon": "#e64553",
"lightPeach": "#fe640b",
"lightYellow": "#df8e1d",
"lightGreen": "#40a02b",
"lightTeal": "#179299",
"lightSky": "#04a5e5",
"lightSapphire": "#209fb5",
"lightBlue": "#1e66f5",
"lightLavender": "#7287fd",
"lightText": "#4c4f69",
"lightSubtext1": "#5c5f77",
"lightSubtext0": "#6c6f85",
"lightOverlay2": "#7c7f93",
"lightOverlay1": "#8c8fa1",
"lightOverlay0": "#9ca0b0",
"lightSurface2": "#acb0be",
"lightSurface1": "#bcc0cc",
"lightSurface0": "#ccd0da",
"lightBase": "#eff1f5",
"lightMantle": "#e6e9ef",
"lightCrust": "#dce0e8",
"darkRosewater": "#f5e0dc",
"darkFlamingo": "#f2cdcd",
"darkPink": "#f5c2e7",
"darkMauve": "#cba6f7",
"darkRed": "#f38ba8",
"darkMaroon": "#eba0ac",
"darkPeach": "#fab387",
"darkYellow": "#f9e2af",
"darkGreen": "#a6e3a1",
"darkTeal": "#94e2d5",
"darkSky": "#89dceb",
"darkSapphire": "#74c7ec",
"darkBlue": "#89b4fa",
"darkLavender": "#b4befe",
"darkText": "#cdd6f4",
"darkSubtext1": "#bac2de",
"darkSubtext0": "#a6adc8",
"darkOverlay2": "#9399b2",
"darkOverlay1": "#7f849c",
"darkOverlay0": "#6c7086",
"darkSurface2": "#585b70",
"darkSurface1": "#45475a",
"darkSurface0": "#313244",
"darkBase": "#1e1e2e",
"darkMantle": "#181825",
"darkCrust": "#11111b"
},
"theme": {
"primary": { "dark": "darkBlue", "light": "lightBlue" },
"secondary": { "dark": "darkMauve", "light": "lightMauve" },
"accent": { "dark": "darkPink", "light": "lightPink" },
"error": { "dark": "darkRed", "light": "lightRed" },
"warning": { "dark": "darkYellow", "light": "lightYellow" },
"success": { "dark": "darkGreen", "light": "lightGreen" },
"info": { "dark": "darkTeal", "light": "lightTeal" },
"text": { "dark": "darkText", "light": "lightText" },
"textMuted": { "dark": "darkSubtext1", "light": "lightSubtext1" },
"background": { "dark": "darkBase", "light": "lightBase" },
"backgroundPanel": { "dark": "darkMantle", "light": "lightMantle" },
"backgroundElement": { "dark": "darkCrust", "light": "lightCrust" },
"border": { "dark": "darkSurface0", "light": "lightSurface0" },
"borderActive": { "dark": "darkSurface1", "light": "lightSurface1" },
"borderSubtle": { "dark": "darkSurface2", "light": "lightSurface2" },
"diffAdded": { "dark": "darkGreen", "light": "lightGreen" },
"diffRemoved": { "dark": "darkRed", "light": "lightRed" },
"diffContext": { "dark": "darkOverlay2", "light": "lightOverlay2" },
"diffHunkHeader": { "dark": "darkPeach", "light": "lightPeach" },
"diffHighlightAdded": { "dark": "darkGreen", "light": "lightGreen" },
"diffHighlightRemoved": { "dark": "darkRed", "light": "lightRed" },
"diffAddedBg": { "dark": "#24312b", "light": "#d6f0d9" },
"diffRemovedBg": { "dark": "#3c2a32", "light": "#f6dfe2" },
"diffContextBg": { "dark": "darkMantle", "light": "lightMantle" },
"diffLineNumber": { "dark": "darkSurface1", "light": "lightSurface1" },
"diffAddedLineNumberBg": { "dark": "#1e2a25", "light": "#c9e3cb" },
"diffRemovedLineNumberBg": { "dark": "#32232a", "light": "#e9d3d6" },
"markdownText": { "dark": "darkText", "light": "lightText" },
"markdownHeading": { "dark": "darkMauve", "light": "lightMauve" },
"markdownLink": { "dark": "darkBlue", "light": "lightBlue" },
"markdownLinkText": { "dark": "darkSky", "light": "lightSky" },
"markdownCode": { "dark": "darkGreen", "light": "lightGreen" },
"markdownBlockQuote": { "dark": "darkYellow", "light": "lightYellow" },
"markdownEmph": { "dark": "darkYellow", "light": "lightYellow" },
"markdownStrong": { "dark": "darkPeach", "light": "lightPeach" },
"markdownHorizontalRule": {
"dark": "darkSubtext0",
"light": "lightSubtext0"
},
"markdownListItem": { "dark": "darkBlue", "light": "lightBlue" },
"markdownListEnumeration": { "dark": "darkSky", "light": "lightSky" },
"markdownImage": { "dark": "darkBlue", "light": "lightBlue" },
"markdownImageText": { "dark": "darkSky", "light": "lightSky" },
"markdownCodeBlock": { "dark": "darkText", "light": "lightText" },
"syntaxComment": { "dark": "darkOverlay2", "light": "lightOverlay2" },
"syntaxKeyword": { "dark": "darkMauve", "light": "lightMauve" },
"syntaxFunction": { "dark": "darkBlue", "light": "lightBlue" },
"syntaxVariable": { "dark": "darkRed", "light": "lightRed" },
"syntaxString": { "dark": "darkGreen", "light": "lightGreen" },
"syntaxNumber": { "dark": "darkPeach", "light": "lightPeach" },
"syntaxType": { "dark": "darkYellow", "light": "lightYellow" },
"syntaxOperator": { "dark": "darkSky", "light": "lightSky" },
"syntaxPunctuation": { "dark": "darkText", "light": "lightText" }
}
}
@@ -0,0 +1,242 @@
{
"$schema": "https://opencode.ai/theme.json",
"defs": {
"darkStep1": "#2d353b",
"darkStep2": "#333c43",
"darkStep3": "#343f44",
"darkStep4": "#3d484d",
"darkStep5": "#475258",
"darkStep6": "#7a8478",
"darkStep7": "#859289",
"darkStep8": "#9da9a0",
"darkStep9": "#a7c080",
"darkStep10": "#83c092",
"darkStep11": "#7a8478",
"darkStep12": "#d3c6aa",
"darkRed": "#e67e80",
"darkOrange": "#e69875",
"darkGreen": "#a7c080",
"darkCyan": "#83c092",
"darkYellow": "#dbbc7f",
"lightStep1": "#fdf6e3",
"lightStep2": "#efebd4",
"lightStep3": "#f4f0d9",
"lightStep4": "#efebd4",
"lightStep5": "#e6e2cc",
"lightStep6": "#a6b0a0",
"lightStep7": "#939f91",
"lightStep8": "#829181",
"lightStep9": "#8da101",
"lightStep10": "#35a77c",
"lightStep11": "#a6b0a0",
"lightStep12": "#5c6a72",
"lightRed": "#f85552",
"lightOrange": "#f57d26",
"lightGreen": "#8da101",
"lightCyan": "#35a77c",
"lightYellow": "#dfa000"
},
"theme": {
"primary": {
"dark": "darkStep9",
"light": "lightStep9"
},
"secondary": {
"dark": "#7fbbb3",
"light": "#3a94c5"
},
"accent": {
"dark": "#d699b6",
"light": "#df69ba"
},
"error": {
"dark": "darkRed",
"light": "lightRed"
},
"warning": {
"dark": "darkOrange",
"light": "lightOrange"
},
"success": {
"dark": "darkGreen",
"light": "lightGreen"
},
"info": {
"dark": "darkCyan",
"light": "lightCyan"
},
"text": {
"dark": "darkStep12",
"light": "lightStep12"
},
"textMuted": {
"dark": "darkStep11",
"light": "lightStep11"
},
"background": {
"dark": "darkStep1",
"light": "lightStep1"
},
"backgroundPanel": {
"dark": "darkStep2",
"light": "lightStep2"
},
"backgroundElement": {
"dark": "darkStep3",
"light": "lightStep3"
},
"border": {
"dark": "darkStep7",
"light": "lightStep7"
},
"borderActive": {
"dark": "darkStep8",
"light": "lightStep8"
},
"borderSubtle": {
"dark": "darkStep6",
"light": "lightStep6"
},
"diffAdded": {
"dark": "#4fd6be",
"light": "#1e725c"
},
"diffRemoved": {
"dark": "#c53b53",
"light": "#c53b53"
},
"diffContext": {
"dark": "#828bb8",
"light": "#7086b5"
},
"diffHunkHeader": {
"dark": "#828bb8",
"light": "#7086b5"
},
"diffHighlightAdded": {
"dark": "#b8db87",
"light": "#4db380"
},
"diffHighlightRemoved": {
"dark": "#e26a75",
"light": "#f52a65"
},
"diffAddedBg": {
"dark": "#20303b",
"light": "#d5e5d5"
},
"diffRemovedBg": {
"dark": "#37222c",
"light": "#f7d8db"
},
"diffContextBg": {
"dark": "darkStep2",
"light": "lightStep2"
},
"diffLineNumber": {
"dark": "darkStep3",
"light": "lightStep3"
},
"diffAddedLineNumberBg": {
"dark": "#1b2b34",
"light": "#c5d5c5"
},
"diffRemovedLineNumberBg": {
"dark": "#2d1f26",
"light": "#e7c8cb"
},
"markdownText": {
"dark": "darkStep12",
"light": "lightStep12"
},
"markdownHeading": {
"dark": "#d699b6",
"light": "#df69ba"
},
"markdownLink": {
"dark": "darkStep9",
"light": "lightStep9"
},
"markdownLinkText": {
"dark": "darkCyan",
"light": "lightCyan"
},
"markdownCode": {
"dark": "darkGreen",
"light": "lightGreen"
},
"markdownBlockQuote": {
"dark": "darkYellow",
"light": "lightYellow"
},
"markdownEmph": {
"dark": "darkYellow",
"light": "lightYellow"
},
"markdownStrong": {
"dark": "darkOrange",
"light": "lightOrange"
},
"markdownHorizontalRule": {
"dark": "darkStep11",
"light": "lightStep11"
},
"markdownListItem": {
"dark": "darkStep9",
"light": "lightStep9"
},
"markdownListEnumeration": {
"dark": "darkCyan",
"light": "lightCyan"
},
"markdownImage": {
"dark": "darkStep9",
"light": "lightStep9"
},
"markdownImageText": {
"dark": "darkCyan",
"light": "lightCyan"
},
"markdownCodeBlock": {
"dark": "darkStep12",
"light": "lightStep12"
},
"syntaxComment": {
"dark": "darkStep11",
"light": "lightStep11"
},
"syntaxKeyword": {
"dark": "#d699b6",
"light": "#df69ba"
},
"syntaxFunction": {
"dark": "darkStep9",
"light": "lightStep9"
},
"syntaxVariable": {
"dark": "darkRed",
"light": "lightRed"
},
"syntaxString": {
"dark": "darkGreen",
"light": "lightGreen"
},
"syntaxNumber": {
"dark": "darkOrange",
"light": "lightOrange"
},
"syntaxType": {
"dark": "darkYellow",
"light": "lightYellow"
},
"syntaxOperator": {
"dark": "darkCyan",
"light": "lightCyan"
},
"syntaxPunctuation": {
"dark": "darkStep12",
"light": "lightStep12"
}
}
}
@@ -0,0 +1,95 @@
{
"$schema": "https://opencode.ai/theme.json",
"defs": {
"darkBg0": "#282828",
"darkBg1": "#3c3836",
"darkBg2": "#504945",
"darkBg3": "#665c54",
"darkFg0": "#fbf1c7",
"darkFg1": "#ebdbb2",
"darkGray": "#928374",
"darkRed": "#cc241d",
"darkGreen": "#98971a",
"darkYellow": "#d79921",
"darkBlue": "#458588",
"darkPurple": "#b16286",
"darkAqua": "#689d6a",
"darkOrange": "#d65d0e",
"darkRedBright": "#fb4934",
"darkGreenBright": "#b8bb26",
"darkYellowBright": "#fabd2f",
"darkBlueBright": "#83a598",
"darkPurpleBright": "#d3869b",
"darkAquaBright": "#8ec07c",
"darkOrangeBright": "#fe8019",
"lightBg0": "#fbf1c7",
"lightBg1": "#ebdbb2",
"lightBg2": "#d5c4a1",
"lightBg3": "#bdae93",
"lightFg0": "#282828",
"lightFg1": "#3c3836",
"lightGray": "#7c6f64",
"lightRed": "#9d0006",
"lightGreen": "#79740e",
"lightYellow": "#b57614",
"lightBlue": "#076678",
"lightPurple": "#8f3f71",
"lightAqua": "#427b58",
"lightOrange": "#af3a03"
},
"theme": {
"primary": { "dark": "darkBlueBright", "light": "lightBlue" },
"secondary": { "dark": "darkPurpleBright", "light": "lightPurple" },
"accent": { "dark": "darkAquaBright", "light": "lightAqua" },
"error": { "dark": "darkRedBright", "light": "lightRed" },
"warning": { "dark": "darkOrangeBright", "light": "lightOrange" },
"success": { "dark": "darkGreenBright", "light": "lightGreen" },
"info": { "dark": "darkYellowBright", "light": "lightYellow" },
"text": { "dark": "darkFg1", "light": "lightFg1" },
"textMuted": { "dark": "darkGray", "light": "lightGray" },
"background": { "dark": "darkBg0", "light": "lightBg0" },
"backgroundPanel": { "dark": "darkBg1", "light": "lightBg1" },
"backgroundElement": { "dark": "darkBg2", "light": "lightBg2" },
"border": { "dark": "darkBg3", "light": "lightBg3" },
"borderActive": { "dark": "darkFg1", "light": "lightFg1" },
"borderSubtle": { "dark": "darkBg2", "light": "lightBg2" },
"diffAdded": { "dark": "darkGreen", "light": "lightGreen" },
"diffRemoved": { "dark": "darkRed", "light": "lightRed" },
"diffContext": { "dark": "darkGray", "light": "lightGray" },
"diffHunkHeader": { "dark": "darkAqua", "light": "lightAqua" },
"diffHighlightAdded": { "dark": "darkGreenBright", "light": "lightGreen" },
"diffHighlightRemoved": { "dark": "darkRedBright", "light": "lightRed" },
"diffAddedBg": { "dark": "#32302f", "light": "#e2e0b5" },
"diffRemovedBg": { "dark": "#322929", "light": "#e9d8d5" },
"diffContextBg": { "dark": "darkBg1", "light": "lightBg1" },
"diffLineNumber": { "dark": "darkBg3", "light": "lightBg3" },
"diffAddedLineNumberBg": { "dark": "#2a2827", "light": "#d4d2a9" },
"diffRemovedLineNumberBg": { "dark": "#2a2222", "light": "#d8cbc8" },
"markdownText": { "dark": "darkFg1", "light": "lightFg1" },
"markdownHeading": { "dark": "darkBlueBright", "light": "lightBlue" },
"markdownLink": { "dark": "darkAquaBright", "light": "lightAqua" },
"markdownLinkText": { "dark": "darkGreenBright", "light": "lightGreen" },
"markdownCode": { "dark": "darkYellowBright", "light": "lightYellow" },
"markdownBlockQuote": { "dark": "darkGray", "light": "lightGray" },
"markdownEmph": { "dark": "darkPurpleBright", "light": "lightPurple" },
"markdownStrong": { "dark": "darkOrangeBright", "light": "lightOrange" },
"markdownHorizontalRule": { "dark": "darkGray", "light": "lightGray" },
"markdownListItem": { "dark": "darkBlueBright", "light": "lightBlue" },
"markdownListEnumeration": {
"dark": "darkAquaBright",
"light": "lightAqua"
},
"markdownImage": { "dark": "darkAquaBright", "light": "lightAqua" },
"markdownImageText": { "dark": "darkGreenBright", "light": "lightGreen" },
"markdownCodeBlock": { "dark": "darkFg1", "light": "lightFg1" },
"syntaxComment": { "dark": "darkGray", "light": "lightGray" },
"syntaxKeyword": { "dark": "darkRedBright", "light": "lightRed" },
"syntaxFunction": { "dark": "darkGreenBright", "light": "lightGreen" },
"syntaxVariable": { "dark": "darkBlueBright", "light": "lightBlue" },
"syntaxString": { "dark": "darkYellowBright", "light": "lightYellow" },
"syntaxNumber": { "dark": "darkPurpleBright", "light": "lightPurple" },
"syntaxType": { "dark": "darkAquaBright", "light": "lightAqua" },
"syntaxOperator": { "dark": "darkOrangeBright", "light": "lightOrange" },
"syntaxPunctuation": { "dark": "darkFg1", "light": "lightFg1" }
}
}
@@ -0,0 +1,77 @@
{
"$schema": "https://opencode.ai/theme.json",
"defs": {
"sumiInk0": "#1F1F28",
"sumiInk1": "#2A2A37",
"sumiInk2": "#363646",
"sumiInk3": "#54546D",
"fujiWhite": "#DCD7BA",
"oldWhite": "#C8C093",
"fujiGray": "#727169",
"oniViolet": "#957FB8",
"crystalBlue": "#7E9CD8",
"carpYellow": "#C38D9D",
"sakuraPink": "#D27E99",
"waveAqua": "#76946A",
"roninYellow": "#D7A657",
"dragonRed": "#E82424",
"lotusGreen": "#98BB6C",
"waveBlue": "#2D4F67",
"lightBg": "#F2E9DE",
"lightPaper": "#EAE4D7",
"lightText": "#54433A",
"lightGray": "#9E9389"
},
"theme": {
"primary": { "dark": "crystalBlue", "light": "waveBlue" },
"secondary": { "dark": "oniViolet", "light": "oniViolet" },
"accent": { "dark": "sakuraPink", "light": "sakuraPink" },
"error": { "dark": "dragonRed", "light": "dragonRed" },
"warning": { "dark": "roninYellow", "light": "roninYellow" },
"success": { "dark": "lotusGreen", "light": "lotusGreen" },
"info": { "dark": "waveAqua", "light": "waveAqua" },
"text": { "dark": "fujiWhite", "light": "lightText" },
"textMuted": { "dark": "fujiGray", "light": "lightGray" },
"background": { "dark": "sumiInk0", "light": "lightBg" },
"backgroundPanel": { "dark": "sumiInk1", "light": "lightPaper" },
"backgroundElement": { "dark": "sumiInk2", "light": "#E3DCD2" },
"border": { "dark": "sumiInk3", "light": "#D4CBBF" },
"borderActive": { "dark": "carpYellow", "light": "carpYellow" },
"borderSubtle": { "dark": "sumiInk2", "light": "#DCD4C9" },
"diffAdded": { "dark": "lotusGreen", "light": "lotusGreen" },
"diffRemoved": { "dark": "dragonRed", "light": "dragonRed" },
"diffContext": { "dark": "fujiGray", "light": "lightGray" },
"diffHunkHeader": { "dark": "waveBlue", "light": "waveBlue" },
"diffHighlightAdded": { "dark": "#A9D977", "light": "#89AF5B" },
"diffHighlightRemoved": { "dark": "#F24A4A", "light": "#D61F1F" },
"diffAddedBg": { "dark": "#252E25", "light": "#EAF3E4" },
"diffRemovedBg": { "dark": "#362020", "light": "#FBE6E6" },
"diffContextBg": { "dark": "sumiInk1", "light": "lightPaper" },
"diffLineNumber": { "dark": "sumiInk3", "light": "#C7BEB4" },
"diffAddedLineNumberBg": { "dark": "#202820", "light": "#DDE8D6" },
"diffRemovedLineNumberBg": { "dark": "#2D1C1C", "light": "#F2DADA" },
"markdownText": { "dark": "fujiWhite", "light": "lightText" },
"markdownHeading": { "dark": "oniViolet", "light": "oniViolet" },
"markdownLink": { "dark": "crystalBlue", "light": "waveBlue" },
"markdownLinkText": { "dark": "waveAqua", "light": "waveAqua" },
"markdownCode": { "dark": "lotusGreen", "light": "lotusGreen" },
"markdownBlockQuote": { "dark": "fujiGray", "light": "lightGray" },
"markdownEmph": { "dark": "carpYellow", "light": "carpYellow" },
"markdownStrong": { "dark": "roninYellow", "light": "roninYellow" },
"markdownHorizontalRule": { "dark": "fujiGray", "light": "lightGray" },
"markdownListItem": { "dark": "crystalBlue", "light": "waveBlue" },
"markdownListEnumeration": { "dark": "waveAqua", "light": "waveAqua" },
"markdownImage": { "dark": "crystalBlue", "light": "waveBlue" },
"markdownImageText": { "dark": "waveAqua", "light": "waveAqua" },
"markdownCodeBlock": { "dark": "fujiWhite", "light": "lightText" },
"syntaxComment": { "dark": "fujiGray", "light": "lightGray" },
"syntaxKeyword": { "dark": "oniViolet", "light": "oniViolet" },
"syntaxFunction": { "dark": "crystalBlue", "light": "waveBlue" },
"syntaxVariable": { "dark": "fujiWhite", "light": "lightText" },
"syntaxString": { "dark": "lotusGreen", "light": "lotusGreen" },
"syntaxNumber": { "dark": "roninYellow", "light": "roninYellow" },
"syntaxType": { "dark": "carpYellow", "light": "carpYellow" },
"syntaxOperator": { "dark": "sakuraPink", "light": "sakuraPink" },
"syntaxPunctuation": { "dark": "fujiWhite", "light": "lightText" }
}
}
@@ -0,0 +1,223 @@
{
"$schema": "https://opencode.ai/theme.json",
"defs": {
"nord0": "#2E3440",
"nord1": "#3B4252",
"nord2": "#434C5E",
"nord3": "#4C566A",
"nord4": "#D8DEE9",
"nord5": "#E5E9F0",
"nord6": "#ECEFF4",
"nord7": "#8FBCBB",
"nord8": "#88C0D0",
"nord9": "#81A1C1",
"nord10": "#5E81AC",
"nord11": "#BF616A",
"nord12": "#D08770",
"nord13": "#EBCB8B",
"nord14": "#A3BE8C",
"nord15": "#B48EAD"
},
"theme": {
"primary": {
"dark": "nord8",
"light": "nord10"
},
"secondary": {
"dark": "nord9",
"light": "nord9"
},
"accent": {
"dark": "nord7",
"light": "nord7"
},
"error": {
"dark": "nord11",
"light": "nord11"
},
"warning": {
"dark": "nord12",
"light": "nord12"
},
"success": {
"dark": "nord14",
"light": "nord14"
},
"info": {
"dark": "nord8",
"light": "nord10"
},
"text": {
"dark": "nord4",
"light": "nord0"
},
"textMuted": {
"dark": "nord3",
"light": "nord1"
},
"background": {
"dark": "nord0",
"light": "nord6"
},
"backgroundPanel": {
"dark": "nord1",
"light": "nord5"
},
"backgroundElement": {
"dark": "nord1",
"light": "nord4"
},
"border": {
"dark": "nord2",
"light": "nord3"
},
"borderActive": {
"dark": "nord3",
"light": "nord2"
},
"borderSubtle": {
"dark": "nord2",
"light": "nord3"
},
"diffAdded": {
"dark": "nord14",
"light": "nord14"
},
"diffRemoved": {
"dark": "nord11",
"light": "nord11"
},
"diffContext": {
"dark": "nord3",
"light": "nord3"
},
"diffHunkHeader": {
"dark": "nord3",
"light": "nord3"
},
"diffHighlightAdded": {
"dark": "nord14",
"light": "nord14"
},
"diffHighlightRemoved": {
"dark": "nord11",
"light": "nord11"
},
"diffAddedBg": {
"dark": "#3B4252",
"light": "#E5E9F0"
},
"diffRemovedBg": {
"dark": "#3B4252",
"light": "#E5E9F0"
},
"diffContextBg": {
"dark": "nord1",
"light": "nord5"
},
"diffLineNumber": {
"dark": "nord2",
"light": "nord4"
},
"diffAddedLineNumberBg": {
"dark": "#3B4252",
"light": "#E5E9F0"
},
"diffRemovedLineNumberBg": {
"dark": "#3B4252",
"light": "#E5E9F0"
},
"markdownText": {
"dark": "nord4",
"light": "nord0"
},
"markdownHeading": {
"dark": "nord8",
"light": "nord10"
},
"markdownLink": {
"dark": "nord9",
"light": "nord9"
},
"markdownLinkText": {
"dark": "nord7",
"light": "nord7"
},
"markdownCode": {
"dark": "nord14",
"light": "nord14"
},
"markdownBlockQuote": {
"dark": "nord3",
"light": "nord3"
},
"markdownEmph": {
"dark": "nord12",
"light": "nord12"
},
"markdownStrong": {
"dark": "nord13",
"light": "nord13"
},
"markdownHorizontalRule": {
"dark": "nord3",
"light": "nord3"
},
"markdownListItem": {
"dark": "nord8",
"light": "nord10"
},
"markdownListEnumeration": {
"dark": "nord7",
"light": "nord7"
},
"markdownImage": {
"dark": "nord9",
"light": "nord9"
},
"markdownImageText": {
"dark": "nord7",
"light": "nord7"
},
"markdownCodeBlock": {
"dark": "nord4",
"light": "nord0"
},
"syntaxComment": {
"dark": "nord3",
"light": "nord3"
},
"syntaxKeyword": {
"dark": "nord9",
"light": "nord9"
},
"syntaxFunction": {
"dark": "nord8",
"light": "nord8"
},
"syntaxVariable": {
"dark": "nord7",
"light": "nord7"
},
"syntaxString": {
"dark": "nord14",
"light": "nord14"
},
"syntaxNumber": {
"dark": "nord15",
"light": "nord15"
},
"syntaxType": {
"dark": "nord7",
"light": "nord7"
},
"syntaxOperator": {
"dark": "nord9",
"light": "nord9"
},
"syntaxPunctuation": {
"dark": "nord4",
"light": "nord0"
}
}
}
@@ -0,0 +1,84 @@
{
"$schema": "https://opencode.ai/theme.json",
"defs": {
"darkBg": "#282c34",
"darkBgAlt": "#21252b",
"darkBgPanel": "#353b45",
"darkFg": "#abb2bf",
"darkFgMuted": "#5c6370",
"darkPurple": "#c678dd",
"darkBlue": "#61afef",
"darkRed": "#e06c75",
"darkGreen": "#98c379",
"darkYellow": "#e5c07b",
"darkOrange": "#d19a66",
"darkCyan": "#56b6c2",
"lightBg": "#fafafa",
"lightBgAlt": "#f0f0f1",
"lightBgPanel": "#eaeaeb",
"lightFg": "#383a42",
"lightFgMuted": "#a0a1a7",
"lightPurple": "#a626a4",
"lightBlue": "#4078f2",
"lightRed": "#e45649",
"lightGreen": "#50a14f",
"lightYellow": "#c18401",
"lightOrange": "#986801",
"lightCyan": "#0184bc"
},
"theme": {
"primary": { "dark": "darkBlue", "light": "lightBlue" },
"secondary": { "dark": "darkPurple", "light": "lightPurple" },
"accent": { "dark": "darkCyan", "light": "lightCyan" },
"error": { "dark": "darkRed", "light": "lightRed" },
"warning": { "dark": "darkYellow", "light": "lightYellow" },
"success": { "dark": "darkGreen", "light": "lightGreen" },
"info": { "dark": "darkOrange", "light": "lightOrange" },
"text": { "dark": "darkFg", "light": "lightFg" },
"textMuted": { "dark": "darkFgMuted", "light": "lightFgMuted" },
"background": { "dark": "darkBg", "light": "lightBg" },
"backgroundPanel": { "dark": "darkBgAlt", "light": "lightBgAlt" },
"backgroundElement": { "dark": "darkBgPanel", "light": "lightBgPanel" },
"border": { "dark": "#393f4a", "light": "#d1d1d2" },
"borderActive": { "dark": "darkBlue", "light": "lightBlue" },
"borderSubtle": { "dark": "#2c313a", "light": "#e0e0e1" },
"diffAdded": { "dark": "darkGreen", "light": "lightGreen" },
"diffRemoved": { "dark": "darkRed", "light": "lightRed" },
"diffContext": { "dark": "darkFgMuted", "light": "lightFgMuted" },
"diffHunkHeader": { "dark": "darkCyan", "light": "lightCyan" },
"diffHighlightAdded": { "dark": "#aad482", "light": "#489447" },
"diffHighlightRemoved": { "dark": "#e8828b", "light": "#d65145" },
"diffAddedBg": { "dark": "#2c382b", "light": "#eafbe9" },
"diffRemovedBg": { "dark": "#3a2d2f", "light": "#fce9e8" },
"diffContextBg": { "dark": "darkBgAlt", "light": "lightBgAlt" },
"diffLineNumber": { "dark": "#495162", "light": "#c9c9ca" },
"diffAddedLineNumberBg": { "dark": "#283427", "light": "#e1f3df" },
"diffRemovedLineNumberBg": { "dark": "#36292b", "light": "#f5e2e1" },
"markdownText": { "dark": "darkFg", "light": "lightFg" },
"markdownHeading": { "dark": "darkPurple", "light": "lightPurple" },
"markdownLink": { "dark": "darkBlue", "light": "lightBlue" },
"markdownLinkText": { "dark": "darkCyan", "light": "lightCyan" },
"markdownCode": { "dark": "darkGreen", "light": "lightGreen" },
"markdownBlockQuote": { "dark": "darkFgMuted", "light": "lightFgMuted" },
"markdownEmph": { "dark": "darkYellow", "light": "lightYellow" },
"markdownStrong": { "dark": "darkOrange", "light": "lightOrange" },
"markdownHorizontalRule": {
"dark": "darkFgMuted",
"light": "lightFgMuted"
},
"markdownListItem": { "dark": "darkBlue", "light": "lightBlue" },
"markdownListEnumeration": { "dark": "darkCyan", "light": "lightCyan" },
"markdownImage": { "dark": "darkBlue", "light": "lightBlue" },
"markdownImageText": { "dark": "darkCyan", "light": "lightCyan" },
"markdownCodeBlock": { "dark": "darkFg", "light": "lightFg" },
"syntaxComment": { "dark": "darkFgMuted", "light": "lightFgMuted" },
"syntaxKeyword": { "dark": "darkPurple", "light": "lightPurple" },
"syntaxFunction": { "dark": "darkBlue", "light": "lightBlue" },
"syntaxVariable": { "dark": "darkRed", "light": "lightRed" },
"syntaxString": { "dark": "darkGreen", "light": "lightGreen" },
"syntaxNumber": { "dark": "darkOrange", "light": "lightOrange" },
"syntaxType": { "dark": "darkYellow", "light": "lightYellow" },
"syntaxOperator": { "dark": "darkCyan", "light": "lightCyan" },
"syntaxPunctuation": { "dark": "darkFg", "light": "lightFg" }
}
}
@@ -0,0 +1,246 @@
{
"$schema": "https://opencode.ai/theme.json",
"defs": {
"darkStep1": "#0a0a0a",
"darkStep2": "#141414",
"darkStep3": "#1e1e1e",
"darkStep4": "#282828",
"darkStep5": "#323232",
"darkStep6": "#3c3c3c",
"darkStep7": "#484848",
"darkStep8": "#606060",
"darkStep9": "#fab283",
"darkStep10": "#ffc09f",
"darkStep11": "#808080",
"darkStep12": "#eeeeee",
"darkSecondary": "#5c9cf5",
"darkAccent": "#9d7cd8",
"darkRed": "#e06c75",
"darkOrange": "#f5a742",
"darkGreen": "#7fd88f",
"darkCyan": "#56b6c2",
"darkYellow": "#e5c07b",
"lightStep1": "#ffffff",
"lightStep2": "#fafafa",
"lightStep3": "#f5f5f5",
"lightStep4": "#ebebeb",
"lightStep5": "#e1e1e1",
"lightStep6": "#d4d4d4",
"lightStep7": "#b8b8b8",
"lightStep8": "#a0a0a0",
"lightStep9": "#3b7dd8",
"lightStep10": "#2968c3",
"lightStep11": "#8a8a8a",
"lightStep12": "#1a1a1a",
"lightSecondary": "#7b5bb6",
"lightAccent": "#d68c27",
"lightRed": "#d1383d",
"lightOrange": "#d68c27",
"lightGreen": "#3d9a57",
"lightCyan": "#318795",
"lightYellow": "#b0851f"
},
"theme": {
"primary": {
"dark": "darkStep9",
"light": "lightStep9"
},
"secondary": {
"dark": "darkSecondary",
"light": "lightSecondary"
},
"accent": {
"dark": "darkAccent",
"light": "lightAccent"
},
"error": {
"dark": "darkRed",
"light": "lightRed"
},
"warning": {
"dark": "darkOrange",
"light": "lightOrange"
},
"success": {
"dark": "darkGreen",
"light": "lightGreen"
},
"info": {
"dark": "darkCyan",
"light": "lightCyan"
},
"text": {
"dark": "darkStep12",
"light": "lightStep12"
},
"textMuted": {
"dark": "darkStep11",
"light": "lightStep11"
},
"background": {
"dark": "darkStep1",
"light": "lightStep1"
},
"backgroundPanel": {
"dark": "darkStep2",
"light": "lightStep2"
},
"backgroundElement": {
"dark": "darkStep3",
"light": "lightStep3"
},
"border": {
"dark": "darkStep7",
"light": "lightStep7"
},
"borderActive": {
"dark": "darkStep8",
"light": "lightStep8"
},
"borderSubtle": {
"dark": "darkStep6",
"light": "lightStep6"
},
"diffAdded": {
"dark": "#4fd6be",
"light": "#1e725c"
},
"diffRemoved": {
"dark": "#c53b53",
"light": "#c53b53"
},
"diffContext": {
"dark": "#828bb8",
"light": "#7086b5"
},
"diffHunkHeader": {
"dark": "#828bb8",
"light": "#7086b5"
},
"diffHighlightAdded": {
"dark": "#b8db87",
"light": "#4db380"
},
"diffHighlightRemoved": {
"dark": "#e26a75",
"light": "#f52a65"
},
"diffAddedBg": {
"dark": "#20303b",
"light": "#d5e5d5"
},
"diffRemovedBg": {
"dark": "#37222c",
"light": "#f7d8db"
},
"diffContextBg": {
"dark": "darkStep2",
"light": "lightStep2"
},
"diffLineNumber": {
"dark": "darkStep3",
"light": "lightStep3"
},
"diffAddedLineNumberBg": {
"dark": "#1b2b34",
"light": "#c5d5c5"
},
"diffRemovedLineNumberBg": {
"dark": "#2d1f26",
"light": "#e7c8cb"
},
"markdownText": {
"dark": "darkStep12",
"light": "lightStep12"
},
"markdownHeading": {
"dark": "darkAccent",
"light": "lightAccent"
},
"markdownLink": {
"dark": "darkStep9",
"light": "lightStep9"
},
"markdownLinkText": {
"dark": "darkCyan",
"light": "lightCyan"
},
"markdownCode": {
"dark": "darkGreen",
"light": "lightGreen"
},
"markdownBlockQuote": {
"dark": "darkYellow",
"light": "lightYellow"
},
"markdownEmph": {
"dark": "darkYellow",
"light": "lightYellow"
},
"markdownStrong": {
"dark": "darkOrange",
"light": "lightOrange"
},
"markdownHorizontalRule": {
"dark": "darkStep11",
"light": "lightStep11"
},
"markdownListItem": {
"dark": "darkStep9",
"light": "lightStep9"
},
"markdownListEnumeration": {
"dark": "darkCyan",
"light": "lightCyan"
},
"markdownImage": {
"dark": "darkStep9",
"light": "lightStep9"
},
"markdownImageText": {
"dark": "darkCyan",
"light": "lightCyan"
},
"markdownCodeBlock": {
"dark": "darkStep12",
"light": "lightStep12"
},
"syntaxComment": {
"dark": "darkStep11",
"light": "lightStep11"
},
"syntaxKeyword": {
"dark": "darkAccent",
"light": "lightAccent"
},
"syntaxFunction": {
"dark": "darkStep9",
"light": "lightStep9"
},
"syntaxVariable": {
"dark": "darkRed",
"light": "lightRed"
},
"syntaxString": {
"dark": "darkGreen",
"light": "lightGreen"
},
"syntaxNumber": {
"dark": "darkOrange",
"light": "lightOrange"
},
"syntaxType": {
"dark": "darkYellow",
"light": "lightYellow"
},
"syntaxOperator": {
"dark": "darkCyan",
"light": "lightCyan"
},
"syntaxPunctuation": {
"dark": "darkStep12",
"light": "lightStep12"
}
}
}
@@ -0,0 +1,244 @@
{
"$schema": "https://opencode.ai/theme.json",
"defs": {
"darkStep1": "#1a1b26",
"darkStep2": "#1e2030",
"darkStep3": "#222436",
"darkStep4": "#292e42",
"darkStep5": "#3b4261",
"darkStep6": "#545c7e",
"darkStep7": "#737aa2",
"darkStep8": "#9099b2",
"darkStep9": "#82aaff",
"darkStep10": "#89b4fa",
"darkStep11": "#828bb8",
"darkStep12": "#c8d3f5",
"darkRed": "#ff757f",
"darkOrange": "#ff966c",
"darkYellow": "#ffc777",
"darkGreen": "#c3e88d",
"darkCyan": "#86e1fc",
"darkPurple": "#c099ff",
"lightStep1": "#e1e2e7",
"lightStep2": "#d5d6db",
"lightStep3": "#c8c9ce",
"lightStep4": "#b9bac1",
"lightStep5": "#a8aecb",
"lightStep6": "#9699a8",
"lightStep7": "#737a8c",
"lightStep8": "#5a607d",
"lightStep9": "#2e7de9",
"lightStep10": "#1a6ce7",
"lightStep11": "#8990a3",
"lightStep12": "#3760bf",
"lightRed": "#f52a65",
"lightOrange": "#b15c00",
"lightYellow": "#8c6c3e",
"lightGreen": "#587539",
"lightCyan": "#007197",
"lightPurple": "#9854f1"
},
"theme": {
"primary": {
"dark": "darkStep9",
"light": "lightStep9"
},
"secondary": {
"dark": "darkPurple",
"light": "lightPurple"
},
"accent": {
"dark": "darkOrange",
"light": "lightOrange"
},
"error": {
"dark": "darkRed",
"light": "lightRed"
},
"warning": {
"dark": "darkOrange",
"light": "lightOrange"
},
"success": {
"dark": "darkGreen",
"light": "lightGreen"
},
"info": {
"dark": "darkStep9",
"light": "lightStep9"
},
"text": {
"dark": "darkStep12",
"light": "lightStep12"
},
"textMuted": {
"dark": "darkStep11",
"light": "lightStep11"
},
"background": {
"dark": "darkStep1",
"light": "lightStep1"
},
"backgroundPanel": {
"dark": "darkStep2",
"light": "lightStep2"
},
"backgroundElement": {
"dark": "darkStep3",
"light": "lightStep3"
},
"border": {
"dark": "darkStep7",
"light": "lightStep7"
},
"borderActive": {
"dark": "darkStep8",
"light": "lightStep8"
},
"borderSubtle": {
"dark": "darkStep6",
"light": "lightStep6"
},
"diffAdded": {
"dark": "#4fd6be",
"light": "#1e725c"
},
"diffRemoved": {
"dark": "#c53b53",
"light": "#c53b53"
},
"diffContext": {
"dark": "#828bb8",
"light": "#7086b5"
},
"diffHunkHeader": {
"dark": "#828bb8",
"light": "#7086b5"
},
"diffHighlightAdded": {
"dark": "#b8db87",
"light": "#4db380"
},
"diffHighlightRemoved": {
"dark": "#e26a75",
"light": "#f52a65"
},
"diffAddedBg": {
"dark": "#20303b",
"light": "#d5e5d5"
},
"diffRemovedBg": {
"dark": "#37222c",
"light": "#f7d8db"
},
"diffContextBg": {
"dark": "darkStep2",
"light": "lightStep2"
},
"diffLineNumber": {
"dark": "darkStep3",
"light": "lightStep3"
},
"diffAddedLineNumberBg": {
"dark": "#1b2b34",
"light": "#c5d5c5"
},
"diffRemovedLineNumberBg": {
"dark": "#2d1f26",
"light": "#e7c8cb"
},
"markdownText": {
"dark": "darkStep12",
"light": "lightStep12"
},
"markdownHeading": {
"dark": "darkPurple",
"light": "lightPurple"
},
"markdownLink": {
"dark": "darkStep9",
"light": "lightStep9"
},
"markdownLinkText": {
"dark": "darkCyan",
"light": "lightCyan"
},
"markdownCode": {
"dark": "darkGreen",
"light": "lightGreen"
},
"markdownBlockQuote": {
"dark": "darkYellow",
"light": "lightYellow"
},
"markdownEmph": {
"dark": "darkYellow",
"light": "lightYellow"
},
"markdownStrong": {
"dark": "darkOrange",
"light": "lightOrange"
},
"markdownHorizontalRule": {
"dark": "darkStep11",
"light": "lightStep11"
},
"markdownListItem": {
"dark": "darkStep9",
"light": "lightStep9"
},
"markdownListEnumeration": {
"dark": "darkCyan",
"light": "lightCyan"
},
"markdownImage": {
"dark": "darkStep9",
"light": "lightStep9"
},
"markdownImageText": {
"dark": "darkCyan",
"light": "lightCyan"
},
"markdownCodeBlock": {
"dark": "darkStep12",
"light": "lightStep12"
},
"syntaxComment": {
"dark": "darkStep11",
"light": "lightStep11"
},
"syntaxKeyword": {
"dark": "darkPurple",
"light": "lightPurple"
},
"syntaxFunction": {
"dark": "darkStep9",
"light": "lightStep9"
},
"syntaxVariable": {
"dark": "darkRed",
"light": "lightRed"
},
"syntaxString": {
"dark": "darkGreen",
"light": "lightGreen"
},
"syntaxNumber": {
"dark": "darkOrange",
"light": "lightOrange"
},
"syntaxType": {
"dark": "darkYellow",
"light": "lightYellow"
},
"syntaxOperator": {
"dark": "darkCyan",
"light": "lightCyan"
},
"syntaxPunctuation": {
"dark": "darkStep12",
"light": "lightStep12"
}
}
}
-295
View File
@@ -1,295 +0,0 @@
package theme
import (
"github.com/charmbracelet/lipgloss/v2"
"github.com/charmbracelet/lipgloss/v2/compat"
)
// 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 with Radix-inspired scale progression
// Dark mode colors - Tokyo Night Moon variant
darkStep1 := "#1a1b26" // App background (bg)
darkStep2 := "#1e2030" // Subtle background (bg_dark)
darkStep3 := "#222436" // UI element background (bg_highlight)
darkStep4 := "#292e42" // Hovered UI element background
darkStep5 := "#3b4261" // Active/Selected UI element background (bg_visual)
darkStep6 := "#545c7e" // Subtle borders and separators (dark3)
darkStep7 := "#737aa2" // UI element border and focus rings (dark5)
darkStep8 := "#9099b2" // Hovered UI element border
darkStep9 := "#82aaff" // Solid backgrounds (blue)
darkStep10 := "#89b4fa" // Hovered solid backgrounds
darkStep11 := "#828bb8" // Low-contrast text (using fg_dark for better contrast)
darkStep12 := "#c8d3f5" // High-contrast text (fg)
// Dark mode accent colors
darkRed := "#ff757f"
darkOrange := "#ff966c"
darkYellow := "#ffc777"
darkGreen := "#c3e88d"
darkCyan := "#86e1fc"
darkBlue := darkStep9 // Using step 9 for primary
darkPurple := "#c099ff"
// Light mode colors - Tokyo Night Day variant
lightStep1 := "#e1e2e7" // App background
lightStep2 := "#d5d6db" // Subtle background
lightStep3 := "#c8c9ce" // UI element background
lightStep4 := "#b9bac1" // Hovered UI element background
lightStep5 := "#a8aecb" // Active/Selected UI element background
lightStep6 := "#9699a8" // Subtle borders and separators
lightStep7 := "#737a8c" // UI element border and focus rings
lightStep8 := "#5a607d" // Hovered UI element border
lightStep9 := "#2e7de9" // Solid backgrounds (blue)
lightStep10 := "#1a6ce7" // Hovered solid backgrounds
lightStep11 := "#8990a3" // Low-contrast text (more muted)
lightStep12 := "#3760bf" // High-contrast text
// Light mode accent colors
lightRed := "#f52a65"
lightOrange := "#b15c00"
lightYellow := "#8c6c3e"
lightGreen := "#587539"
lightCyan := "#007197"
lightBlue := lightStep9 // Using step 9 for primary
lightPurple := "#9854f1"
// Unused variables to avoid compiler errors (these could be used for hover states)
_ = darkStep4
_ = darkStep5
_ = darkStep10
_ = lightStep4
_ = lightStep5
_ = lightStep10
theme := &TokyoNightTheme{}
// Base colors
theme.PrimaryColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkBlue),
Light: lipgloss.Color(lightBlue),
}
theme.SecondaryColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkPurple),
Light: lipgloss.Color(lightPurple),
}
theme.AccentColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkOrange),
Light: lipgloss.Color(lightOrange),
}
// Status colors
theme.ErrorColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkRed),
Light: lipgloss.Color(lightRed),
}
theme.WarningColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkOrange),
Light: lipgloss.Color(lightOrange),
}
theme.SuccessColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkGreen),
Light: lipgloss.Color(lightGreen),
}
theme.InfoColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkBlue),
Light: lipgloss.Color(lightBlue),
}
// Text colors
theme.TextColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkStep12),
Light: lipgloss.Color(lightStep12),
}
theme.TextMutedColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkStep11),
Light: lipgloss.Color(lightStep11),
}
// Background colors
theme.BackgroundColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkStep1),
Light: lipgloss.Color(lightStep1),
}
theme.BackgroundPanelColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkStep2),
Light: lipgloss.Color(lightStep2),
}
theme.BackgroundElementColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkStep3),
Light: lipgloss.Color(lightStep3),
}
// Border colors
theme.BorderColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkStep7),
Light: lipgloss.Color(lightStep7),
}
theme.BorderActiveColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkStep8),
Light: lipgloss.Color(lightStep8),
}
theme.BorderSubtleColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkStep6),
Light: lipgloss.Color(lightStep6),
}
// Diff view colors
theme.DiffAddedColor = compat.AdaptiveColor{
Dark: lipgloss.Color("#4fd6be"), // teal from palette
Light: lipgloss.Color("#1e725c"),
}
theme.DiffRemovedColor = compat.AdaptiveColor{
Dark: lipgloss.Color("#c53b53"), // red1 from palette
Light: lipgloss.Color("#c53b53"),
}
theme.DiffContextColor = compat.AdaptiveColor{
Dark: lipgloss.Color("#828bb8"), // fg_dark from palette
Light: lipgloss.Color("#7086b5"),
}
theme.DiffHunkHeaderColor = compat.AdaptiveColor{
Dark: lipgloss.Color("#828bb8"), // fg_dark from palette
Light: lipgloss.Color("#7086b5"),
}
theme.DiffHighlightAddedColor = compat.AdaptiveColor{
Dark: lipgloss.Color("#b8db87"), // git.add from palette
Light: lipgloss.Color("#4db380"),
}
theme.DiffHighlightRemovedColor = compat.AdaptiveColor{
Dark: lipgloss.Color("#e26a75"), // git.delete from palette
Light: lipgloss.Color("#f52a65"),
}
theme.DiffAddedBgColor = compat.AdaptiveColor{
Dark: lipgloss.Color("#20303b"),
Light: lipgloss.Color("#d5e5d5"),
}
theme.DiffRemovedBgColor = compat.AdaptiveColor{
Dark: lipgloss.Color("#37222c"),
Light: lipgloss.Color("#f7d8db"),
}
theme.DiffContextBgColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkStep2),
Light: lipgloss.Color(lightStep2),
}
theme.DiffLineNumberColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkStep3), // dark3 from palette
Light: lipgloss.Color(lightStep3),
}
theme.DiffAddedLineNumberBgColor = compat.AdaptiveColor{
Dark: lipgloss.Color("#1b2b34"),
Light: lipgloss.Color("#c5d5c5"),
}
theme.DiffRemovedLineNumberBgColor = compat.AdaptiveColor{
Dark: lipgloss.Color("#2d1f26"),
Light: lipgloss.Color("#e7c8cb"),
}
// Markdown colors
theme.MarkdownTextColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkStep12),
Light: lipgloss.Color(lightStep12),
}
theme.MarkdownHeadingColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkPurple),
Light: lipgloss.Color(lightPurple),
}
theme.MarkdownLinkColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkBlue),
Light: lipgloss.Color(lightBlue),
}
theme.MarkdownLinkTextColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkCyan),
Light: lipgloss.Color(lightCyan),
}
theme.MarkdownCodeColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkGreen),
Light: lipgloss.Color(lightGreen),
}
theme.MarkdownBlockQuoteColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkYellow),
Light: lipgloss.Color(lightYellow),
}
theme.MarkdownEmphColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkYellow),
Light: lipgloss.Color(lightYellow),
}
theme.MarkdownStrongColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkOrange),
Light: lipgloss.Color(lightOrange),
}
theme.MarkdownHorizontalRuleColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkStep11),
Light: lipgloss.Color(lightStep11),
}
theme.MarkdownListItemColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkBlue),
Light: lipgloss.Color(lightBlue),
}
theme.MarkdownListEnumerationColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkCyan),
Light: lipgloss.Color(lightCyan),
}
theme.MarkdownImageColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkBlue),
Light: lipgloss.Color(lightBlue),
}
theme.MarkdownImageTextColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkCyan),
Light: lipgloss.Color(lightCyan),
}
theme.MarkdownCodeBlockColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkStep12),
Light: lipgloss.Color(lightStep12),
}
// Syntax highlighting colors
theme.SyntaxCommentColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkStep11),
Light: lipgloss.Color(lightStep11),
}
theme.SyntaxKeywordColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkPurple),
Light: lipgloss.Color(lightPurple),
}
theme.SyntaxFunctionColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkBlue),
Light: lipgloss.Color(lightBlue),
}
theme.SyntaxVariableColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkRed),
Light: lipgloss.Color(lightRed),
}
theme.SyntaxStringColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkGreen),
Light: lipgloss.Color(lightGreen),
}
theme.SyntaxNumberColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkOrange),
Light: lipgloss.Color(lightOrange),
}
theme.SyntaxTypeColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkYellow),
Light: lipgloss.Color(lightYellow),
}
theme.SyntaxOperatorColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkCyan),
Light: lipgloss.Color(lightCyan),
}
theme.SyntaxPunctuationColor = compat.AdaptiveColor{
Dark: lipgloss.Color(darkStep12),
Light: lipgloss.Color(lightStep12),
}
return theme
}
func init() {
// Register the Tokyo Night theme with the theme manager
RegisterTheme("tokyonight", NewTokyoNightTheme())
}
+8 -3
View File
@@ -77,8 +77,9 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch keyString {
// Escape always closes current modal
case "esc", "ctrl+c":
cmd := a.modal.Close()
a.modal = nil
return a, nil
return a, cmd
}
// Pass all other key presses to the modal
@@ -194,8 +195,12 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
}
slog.Debug("Background color", "isDark", msg.IsDark())
case modal.CloseModalMsg:
var cmd tea.Cmd
if a.modal != nil {
cmd = a.modal.Close()
}
a.modal = nil
return a, nil
return a, cmd
case commands.ExecuteCommandMsg:
updated, cmd := a.executeCommand(commands.Command(msg))
return updated, cmd
@@ -341,7 +346,7 @@ func (a appModel) View() string {
layoutView,
a.status.View(),
}
appView := lipgloss.JoinVertical(lipgloss.Top, components...)
appView := strings.Join(components, "\n")
if a.modal != nil {
appView = a.modal.Render(appView)
+1
View File
@@ -21,6 +21,7 @@
"astro": "5.7.13",
"diff": "8.0.2",
"js-base64": "3.7.7",
"lang-map": "0.4.0",
"luxon": "3.6.1",
"marked": "15.0.12",
"rehype-autolink-headings": "7.1.0",
+25 -8
View File
@@ -11,6 +11,7 @@ import {
createEffect,
createSignal,
} from "solid-js"
import map from "lang-map"
import { DateTime } from "luxon"
import { createStore, reconcile } from "solid-js/store"
import type { Diagnostic } from "vscode-languageserver-types"
@@ -85,7 +86,7 @@ function scrollToAnchor(id: string) {
}
function stripWorkingDirectory(filePath: string, workingDir?: string) {
if (workingDir === undefined) return filePath
if (filePath === undefined || workingDir === undefined) return filePath
const prefix = workingDir.endsWith("/") ? workingDir : workingDir + "/"
@@ -100,8 +101,24 @@ function stripWorkingDirectory(filePath: string, workingDir?: string) {
return filePath
}
function getFileType(path: string) {
return path.split(".").pop()
function getShikiLang(filename: string) {
const ext = filename
.split('.')
.pop()
?.toLowerCase() ?? ''
// map.languages(ext) returns an array of matching Linguist language names (e.g. ['TypeScript'])
const langs = map.languages(ext)
const type = langs?.[0]?.toLowerCase()
// Overrride any specific language mappings
const overrides: Record<string, string> = {
"conf": "shellscript"
}
return type
? overrides[type] ?? type
: 'plaintext'
}
function formatDuration(ms: number): string {
@@ -980,7 +997,7 @@ export default function Share(props: {
const prompts = assistant().system || []
return prompts.filter(
(p: string) =>
!p.startsWith("You are Claude Code"),
!p.startsWith("You are Claude"),
)
})
return (
@@ -1379,7 +1396,7 @@ export default function Share(props: {
<Show when={showResults()}>
<div data-part-tool-code>
<CodeBlock
lang={getFileType(filePath())}
lang={getShikiLang(filePath())}
code={preview()}
/>
</div>
@@ -1483,8 +1500,8 @@ export default function Share(props: {
<Show when={showResults()}>
<div data-part-tool-code>
<CodeBlock
lang={getFileType(filePath())}
code={content()}
lang={getShikiLang(filePath())}
code={args.content}
/>
</div>
</Show>
@@ -1557,7 +1574,7 @@ export default function Share(props: {
<DiffView
class={styles["diff-code-block"]}
diff={diff()}
lang={getFileType(filePath())}
lang={getShikiLang(filePath())}
/>
</div>
</Match>
+2 -2
View File
@@ -63,7 +63,7 @@
h1 {
font-size: 2.75rem;
font-weight: 500;
line-height: 1.15;
line-height: 1.2;
letter-spacing: -0.05em;
display: -webkit-box;
-webkit-box-orient: vertical;
@@ -360,7 +360,7 @@
[data-part-type="tool-fetch"] {
[data-part-tool-result] {
[data-part-tool-code] {
width: var(--md-tool-width);
max-width: var(--md-tool-width);
border: 1px solid var(--sl-color-divider);
background-color: var(--sl-color-bg-surface);
border-radius: 0.25rem;
+273 -26
View File
@@ -2,43 +2,290 @@
title: Themes
---
opencode will support most common terminal themes and you'll soon be able to create your own custom theme.
opencode supports a flexible JSON-based theme system that allows users to create and customize themes easily.
---
## Theme Loading Hierarchy
## Built-in
Themes are loaded from multiple directories in the following order (later directories override earlier ones):
The following built-in themes are available:
1. **Built-in themes** - Embedded in the binary
2. **User config directory** - `~/.config/opencode/themes/*.json` (or `$XDG_CONFIG_HOME/opencode/themes/*.json`)
3. **Project root directory** - `<project-root>/.opencode/themes/*.json`
4. **Current working directory** - `./.opencode/themes/*.json`
- `opencode`
If multiple directories contain a theme with the same name, the theme from the directory with higher priority will be used.
![opencode theme](../../../assets/themes/opencode.png)
## Creating a Custom Theme
- `ayu`
To create a custom theme, create a JSON file in one of the theme directories:
![ayu theme](../../../assets/themes/ayu.png)
```bash no-frame
# For user-wide themes
mkdir -p ~/.config/opencode/themes
vim ~/.config/opencode/themes/my-theme.json
- `everforest`
![everforest theme](../../../assets/themes/everforest.png)
- `tokyonight`
![tokyonight theme](../../../assets/themes/tokyonight.png)
---
## Configure
To select a theme, type in:
```bash frame="none"
/themes
# For project-specific themes
mkdir -p .opencode/themes
vim .opencode/themes/my-theme.json
```
Your selected theme will be used the next time you start opencode.
## Theme JSON Format
You can also configure it in your opencode config.
Themes use a flexible JSON format with support for:
- **Hex colors**: `"#ffffff"`
- **ANSI colors**: `3` (0-255)
- **Color references**: `"primary"` or custom definitions
- **Dark/light variants**: `{"dark": "#000", "light": "#fff"}`
### Example Theme
```json no-frame
{
"$schema": "https://opencode.ai/theme.json",
"defs": {
"nord0": "#2E3440",
"nord1": "#3B4252",
"nord2": "#434C5E",
"nord3": "#4C566A",
"nord4": "#D8DEE9",
"nord5": "#E5E9F0",
"nord6": "#ECEFF4",
"nord7": "#8FBCBB",
"nord8": "#88C0D0",
"nord9": "#81A1C1",
"nord10": "#5E81AC",
"nord11": "#BF616A",
"nord12": "#D08770",
"nord13": "#EBCB8B",
"nord14": "#A3BE8C",
"nord15": "#B48EAD"
},
"theme": {
"primary": {
"dark": "nord8",
"light": "nord10"
},
"secondary": {
"dark": "nord9",
"light": "nord9"
},
"accent": {
"dark": "nord7",
"light": "nord7"
},
"error": {
"dark": "nord11",
"light": "nord11"
},
"warning": {
"dark": "nord12",
"light": "nord12"
},
"success": {
"dark": "nord14",
"light": "nord14"
},
"info": {
"dark": "nord8",
"light": "nord10"
},
"text": {
"dark": "nord4",
"light": "nord0"
},
"textMuted": {
"dark": "nord3",
"light": "nord1"
},
"background": {
"dark": "nord0",
"light": "nord6"
},
"backgroundPanel": {
"dark": "nord1",
"light": "nord5"
},
"backgroundElement": {
"dark": "nord1",
"light": "nord4"
},
"border": {
"dark": "nord2",
"light": "nord3"
},
"borderActive": {
"dark": "nord3",
"light": "nord2"
},
"borderSubtle": {
"dark": "nord2",
"light": "nord3"
},
"diffAdded": {
"dark": "nord14",
"light": "nord14"
},
"diffRemoved": {
"dark": "nord11",
"light": "nord11"
},
"diffContext": {
"dark": "nord3",
"light": "nord3"
},
"diffHunkHeader": {
"dark": "nord3",
"light": "nord3"
},
"diffHighlightAdded": {
"dark": "nord14",
"light": "nord14"
},
"diffHighlightRemoved": {
"dark": "nord11",
"light": "nord11"
},
"diffAddedBg": {
"dark": "#3B4252",
"light": "#E5E9F0"
},
"diffRemovedBg": {
"dark": "#3B4252",
"light": "#E5E9F0"
},
"diffContextBg": {
"dark": "nord1",
"light": "nord5"
},
"diffLineNumber": {
"dark": "nord2",
"light": "nord4"
},
"diffAddedLineNumberBg": {
"dark": "#3B4252",
"light": "#E5E9F0"
},
"diffRemovedLineNumberBg": {
"dark": "#3B4252",
"light": "#E5E9F0"
},
"markdownText": {
"dark": "nord4",
"light": "nord0"
},
"markdownHeading": {
"dark": "nord8",
"light": "nord10"
},
"markdownLink": {
"dark": "nord9",
"light": "nord9"
},
"markdownLinkText": {
"dark": "nord7",
"light": "nord7"
},
"markdownCode": {
"dark": "nord14",
"light": "nord14"
},
"markdownBlockQuote": {
"dark": "nord3",
"light": "nord3"
},
"markdownEmph": {
"dark": "nord12",
"light": "nord12"
},
"markdownStrong": {
"dark": "nord13",
"light": "nord13"
},
"markdownHorizontalRule": {
"dark": "nord3",
"light": "nord3"
},
"markdownListItem": {
"dark": "nord8",
"light": "nord10"
},
"markdownListEnumeration": {
"dark": "nord7",
"light": "nord7"
},
"markdownImage": {
"dark": "nord9",
"light": "nord9"
},
"markdownImageText": {
"dark": "nord7",
"light": "nord7"
},
"markdownCodeBlock": {
"dark": "nord4",
"light": "nord0"
},
"syntaxComment": {
"dark": "nord3",
"light": "nord3"
},
"syntaxKeyword": {
"dark": "nord9",
"light": "nord9"
},
"syntaxFunction": {
"dark": "nord8",
"light": "nord8"
},
"syntaxVariable": {
"dark": "nord7",
"light": "nord7"
},
"syntaxString": {
"dark": "nord14",
"light": "nord14"
},
"syntaxNumber": {
"dark": "nord15",
"light": "nord15"
},
"syntaxType": {
"dark": "nord7",
"light": "nord7"
},
"syntaxOperator": {
"dark": "nord9",
"light": "nord9"
},
"syntaxPunctuation": {
"dark": "nord4",
"light": "nord0"
}
}
}
```
### Color Definitions
The `defs` section (optional) allows you to define reusable colors that can be referenced in the theme.
## Built-in Themes
opencode comes with several built-in themes:
- `opencode` - Default opencode theme
- `tokyonight` - Tokyonight theme
- `everforest` - Everforest theme
- `ayu` - Ayu dark theme
- `catppuccin` - Catppuccin theme
- `gruvbox` - Gruvbox theme
- `kanagawa` - Kanagawa theme
- `nord` - Nord theme
- and more (see ./packages/tui/internal/theme/themes)
## Using a Theme
To use a theme, set it in your opencode configuration or select it from the theme dialog in the TUI.
```json title="opencode.json" {3}
{
+7
View File
@@ -11,6 +11,13 @@ const { id } = Astro.params;
const res = await fetch(`${apiUrl}/share_data?id=${id}`);
const data = await res.json();
if (!data.info) {
return new Response(null, {
status: 404,
statusText: 'Not found'
});
}
const models: Set<string> = new Set();
const version = data.info.version ? `v${data.info.version}` : "v0.0.1";
View File