mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-03 00:36:20 -04:00
Compare commits
69 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8ca713b737 | |||
| 5b54554fd5 | |||
| 4bc651f958 | |||
| 3b6976a9c8 | |||
| 863d5c1e8e | |||
| 97e19e9677 | |||
| b27851461f | |||
| 209687377a | |||
| 90face1c09 | |||
| 936e2ce48b | |||
| 16ee8ee379 | |||
| ac39308dad | |||
| 346b49219d | |||
| d84c1f20c7 | |||
| dfb8777555 | |||
| 008af18156 | |||
| ab23167f80 | |||
| b17ec46463 | |||
| 2e26b58d16 | |||
| 31b56e5a05 | |||
| 47c401cf25 | |||
| fab8dc9e6f | |||
| f39a2b1f16 | |||
| 66830ced4e | |||
| 9d3fad754d | |||
| dcd3131f58 | |||
| 3d02e07161 | |||
| 4dbc6a43a6 | |||
| 5394b5188b | |||
| 8e680b3957 | |||
| 1b8cd796d6 | |||
| 35fba793d0 | |||
| 5358d43b74 | |||
| f777347bac | |||
| 17c8b914df | |||
| 43b467dd12 | |||
| 0e0770921e | |||
| 8edbb74352 | |||
| e6bfa95758 | |||
| e4120b6287 | |||
| ccbc9e00f2 | |||
| 7d13baadc8 | |||
| 9acc83697f | |||
| db24bf87c0 | |||
| f4c0d2d2fd | |||
| d240f4c676 | |||
| 9c90cdbe08 | |||
| fc7af31fe5 | |||
| 2f8d23ec66 | |||
| 77ae3fb9b9 | |||
| 4e7f6c47fd | |||
| 50469ed750 | |||
| aaab785493 | |||
| 9751937894 | |||
| 0fc8dfc77e | |||
| 81b7df61ec | |||
| 8217b96d4a | |||
| 7dd0918d32 | |||
| 4b26b43855 | |||
| 9d7cfda9fe | |||
| a3cf18c905 | |||
| 0b1a8ae699 | |||
| eb70b1e5c8 | |||
| 00a3d818b6 | |||
| 2384c7e734 | |||
| 1bad3d9894 | |||
| 4f715e66dc | |||
| ec001ca02f | |||
| a2d3b9f0c8 |
@@ -20,6 +20,9 @@
|
||||
"packages/opencode": {
|
||||
"name": "opencode",
|
||||
"version": "0.0.0",
|
||||
"bin": {
|
||||
"opencode": "./bin/opencode",
|
||||
},
|
||||
"dependencies": {
|
||||
"@clack/prompts": "0.11.0",
|
||||
"@flystorage/file-storage": "1.1.0",
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
@echo off
|
||||
setlocal enabledelayedexpansion
|
||||
|
||||
if defined OPENCODE_BIN_PATH (
|
||||
set "resolved=%OPENCODE_BIN_PATH%"
|
||||
goto :execute
|
||||
)
|
||||
|
||||
rem Get the directory of this script
|
||||
set "script_dir=%~dp0"
|
||||
set "script_dir=%script_dir:~0,-1%"
|
||||
|
||||
rem Detect platform and architecture
|
||||
set "platform=win32"
|
||||
|
||||
rem Detect architecture
|
||||
if "%PROCESSOR_ARCHITECTURE%"=="AMD64" (
|
||||
set "arch=x64"
|
||||
) else if "%PROCESSOR_ARCHITECTURE%"=="ARM64" (
|
||||
set "arch=arm64"
|
||||
) else if "%PROCESSOR_ARCHITECTURE%"=="x86" (
|
||||
set "arch=x86"
|
||||
) else (
|
||||
set "arch=x64"
|
||||
)
|
||||
|
||||
set "name=opencode-!platform!-!arch!"
|
||||
set "binary=opencode.exe"
|
||||
|
||||
rem Search for the binary starting from script location
|
||||
set "resolved="
|
||||
set "current_dir=%script_dir%"
|
||||
|
||||
:search_loop
|
||||
set "candidate=%current_dir%\node_modules\%name%\bin\%binary%"
|
||||
if exist "%candidate%" (
|
||||
set "resolved=%candidate%"
|
||||
goto :execute
|
||||
)
|
||||
|
||||
rem Move up one directory
|
||||
for %%i in ("%current_dir%") do set "parent_dir=%%~dpi"
|
||||
set "parent_dir=%parent_dir:~0,-1%"
|
||||
|
||||
rem Check if we've reached the root
|
||||
if "%current_dir%"=="%parent_dir%" goto :not_found
|
||||
set "current_dir=%parent_dir%"
|
||||
goto :search_loop
|
||||
|
||||
:not_found
|
||||
echo It seems that your package manager failed to install the right version of the OpenCode CLI for your platform. You can try manually installing the "%name%" package >&2
|
||||
exit /b 1
|
||||
|
||||
:execute
|
||||
rem Execute the binary with all arguments
|
||||
"%resolved%" %*
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"version": "0.0.0",
|
||||
"version": "0.0.5",
|
||||
"name": "opencode",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
@@ -8,6 +8,9 @@
|
||||
"typecheck": "tsc --noEmit",
|
||||
"dev": "bun run ./src/index.ts"
|
||||
},
|
||||
"bin": {
|
||||
"opencode": "./bin/opencode"
|
||||
},
|
||||
"exports": {
|
||||
"./*": "./src/*.ts"
|
||||
},
|
||||
|
||||
@@ -29,7 +29,7 @@ const targets = [
|
||||
["linux", "x64"],
|
||||
["darwin", "x64"],
|
||||
["darwin", "arm64"],
|
||||
// ["windows", "x64"],
|
||||
["windows", "x64"],
|
||||
]
|
||||
|
||||
await $`rm -rf dist`
|
||||
|
||||
@@ -46,7 +46,7 @@ export namespace App {
|
||||
const data = path.join(
|
||||
Global.Path.data,
|
||||
"project",
|
||||
git ? git.split(path.sep).filter(Boolean).join("-") : "global",
|
||||
git ? directory(git) : "global",
|
||||
)
|
||||
const stateFile = Bun.file(path.join(data, APP_JSON))
|
||||
const state = (await stateFile.json().catch(() => ({}))) as {
|
||||
@@ -133,4 +133,13 @@ export namespace App {
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
function directory(input: string): string {
|
||||
return input
|
||||
.split(path.sep)
|
||||
.filter(Boolean)
|
||||
.join("-")
|
||||
.replace(/[^A-Za-z0-9_]/g, "-")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -26,7 +26,9 @@ export namespace BunProc {
|
||||
},
|
||||
})
|
||||
const code = await result.exited
|
||||
// @ts-ignore
|
||||
const stdout = await result.stdout.text()
|
||||
// @ts-ignore
|
||||
const stderr = await result.stderr.text()
|
||||
log.info("done", {
|
||||
code,
|
||||
|
||||
@@ -7,6 +7,9 @@ import open from "open"
|
||||
import { UI } from "../ui"
|
||||
import { ModelsDev } from "../../provider/models"
|
||||
import { map, pipe, sortBy, values } from "remeda"
|
||||
import path from "path"
|
||||
import os from "os"
|
||||
import { Global } from "../../global"
|
||||
|
||||
export const AuthCommand = cmd({
|
||||
command: "auth",
|
||||
@@ -26,16 +29,46 @@ export const AuthListCommand = cmd({
|
||||
describe: "list providers",
|
||||
async handler() {
|
||||
UI.empty()
|
||||
prompts.intro("Credentials")
|
||||
const authPath = path.join(Global.Path.data, "auth.json")
|
||||
const homedir = os.homedir()
|
||||
const displayPath = authPath.startsWith(homedir)
|
||||
? authPath.replace(homedir, "~")
|
||||
: authPath
|
||||
prompts.intro(`Credentials ${UI.Style.TEXT_DIM}${displayPath}`)
|
||||
const results = await Auth.all().then((x) => Object.entries(x))
|
||||
const database = await ModelsDev.get()
|
||||
|
||||
for (const [providerID, result] of results) {
|
||||
const name = database[providerID]?.name || providerID
|
||||
prompts.log.info(`${name} ${UI.Style.TEXT_DIM}(${result.type})`)
|
||||
prompts.log.info(`${name} ${UI.Style.TEXT_DIM}${result.type}`)
|
||||
}
|
||||
|
||||
prompts.outro(`${results.length} credentials`)
|
||||
|
||||
// Environment variables section
|
||||
const activeEnvVars: Array<{ provider: string, envVar: string }> = []
|
||||
|
||||
for (const [providerID, provider] of Object.entries(database)) {
|
||||
for (const envVar of provider.env) {
|
||||
if (process.env[envVar]) {
|
||||
activeEnvVars.push({
|
||||
provider: provider.name || providerID,
|
||||
envVar
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (activeEnvVars.length > 0) {
|
||||
UI.empty()
|
||||
prompts.intro("Environment")
|
||||
|
||||
for (const { provider, envVar } of activeEnvVars) {
|
||||
prompts.log.info(`${provider} ${UI.Style.TEXT_DIM}${envVar}`)
|
||||
}
|
||||
|
||||
prompts.outro(`${activeEnvVars.length} environment variables`)
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import { App } from "../../app/app"
|
||||
import { Provider } from "../../provider/provider"
|
||||
import { cmd } from "./cmd"
|
||||
|
||||
export const ModelsCommand = cmd({
|
||||
command: "models",
|
||||
describe: "list all available models",
|
||||
handler: async () => {
|
||||
await App.provide({ cwd: process.cwd() }, async () => {
|
||||
const providers = await Provider.list()
|
||||
|
||||
for (const [providerID, provider] of Object.entries(providers)) {
|
||||
for (const modelID of Object.keys(provider.info.models)) {
|
||||
console.log(`${providerID}/${modelID}`)
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
})
|
||||
@@ -79,6 +79,8 @@ export const RunCommand = cmd({
|
||||
return
|
||||
}
|
||||
|
||||
const isPiped = !process.stdout.isTTY
|
||||
|
||||
UI.empty()
|
||||
UI.println(UI.logo())
|
||||
UI.empty()
|
||||
@@ -90,8 +92,8 @@ export const RunCommand = cmd({
|
||||
await Session.share(session.id)
|
||||
UI.println(
|
||||
UI.Style.TEXT_INFO_BOLD +
|
||||
"~ https://opencode.ai/s/" +
|
||||
session.id.slice(-8),
|
||||
"~ https://opencode.ai/s/" +
|
||||
session.id.slice(-8),
|
||||
)
|
||||
}
|
||||
UI.empty()
|
||||
@@ -109,8 +111,8 @@ export const RunCommand = cmd({
|
||||
UI.println(
|
||||
color + `|`,
|
||||
UI.Style.TEXT_NORMAL +
|
||||
UI.Style.TEXT_DIM +
|
||||
` ${type.padEnd(7, " ")}`,
|
||||
UI.Style.TEXT_DIM +
|
||||
` ${type.padEnd(7, " ")}`,
|
||||
"",
|
||||
UI.Style.TEXT_NORMAL + title,
|
||||
)
|
||||
@@ -134,7 +136,7 @@ export const RunCommand = cmd({
|
||||
part.toolInvocation.toolName,
|
||||
UI.Style.TEXT_INFO_BOLD,
|
||||
]
|
||||
printEvent(color, tool, metadata.title)
|
||||
printEvent(color, tool, metadata?.title || "Unknown")
|
||||
}
|
||||
|
||||
if (part.type === "text") {
|
||||
@@ -147,7 +149,8 @@ export const RunCommand = cmd({
|
||||
printEvent(UI.Style.TEXT_NORMAL_BOLD, "Text", part.text)
|
||||
}
|
||||
})
|
||||
await Session.chat({
|
||||
|
||||
const result = await Session.chat({
|
||||
sessionID: session.id,
|
||||
providerID,
|
||||
modelID,
|
||||
@@ -158,8 +161,14 @@ export const RunCommand = cmd({
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
if (isPiped) {
|
||||
const match = result.parts.findLast((x) => x.type === "text")
|
||||
if (match) process.stdout.write(match.text)
|
||||
}
|
||||
UI.empty()
|
||||
},
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import { App } from "../../app/app"
|
||||
import { Provider } from "../../provider/provider"
|
||||
import { Server } from "../../server/server"
|
||||
import { Share } from "../../share/share"
|
||||
import { cmd } from "./cmd"
|
||||
|
||||
export const ServeCommand = cmd({
|
||||
command: "serve",
|
||||
builder: (yargs) =>
|
||||
yargs
|
||||
.option("port", {
|
||||
alias: ["p"],
|
||||
type: "number",
|
||||
describe: "port to listen on",
|
||||
default: 4096,
|
||||
})
|
||||
.option("hostname", {
|
||||
alias: ["h"],
|
||||
type: "string",
|
||||
describe: "hostname to listen on",
|
||||
default: "127.0.0.1",
|
||||
}),
|
||||
describe: "starts a headless opencode server",
|
||||
handler: async (args) => {
|
||||
const cwd = process.cwd()
|
||||
await App.provide({ cwd }, async () => {
|
||||
const providers = await Provider.list()
|
||||
if (Object.keys(providers).length === 0) {
|
||||
return "needs_provider"
|
||||
}
|
||||
|
||||
const hostname = args.hostname
|
||||
const port = args.port
|
||||
|
||||
await Share.init()
|
||||
const server = Server.listen({
|
||||
port,
|
||||
hostname,
|
||||
})
|
||||
|
||||
console.log(
|
||||
`opencode server listening on http://${server.hostname}:${server.port}`,
|
||||
)
|
||||
|
||||
await new Promise(() => {})
|
||||
|
||||
server.stop()
|
||||
})
|
||||
},
|
||||
})
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Config } from "../config/config"
|
||||
import { MCP } from "../mcp"
|
||||
import { UI } from "./ui"
|
||||
|
||||
export function FormatError(input: unknown) {
|
||||
if (MCP.Failed.isInstance(input))
|
||||
@@ -13,4 +14,6 @@ export function FormatError(input: unknown) {
|
||||
(issue) => "↳ " + issue.message + " " + issue.path.join("."),
|
||||
) ?? []),
|
||||
].join("\n")
|
||||
|
||||
if (UI.CancelledError.isInstance(input)) return ""
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { z } from "zod"
|
||||
import { EOL } from "os"
|
||||
import { NamedError } from "../util/error"
|
||||
|
||||
export namespace UI {
|
||||
@@ -29,7 +30,7 @@ export namespace UI {
|
||||
|
||||
export function println(...message: string[]) {
|
||||
print(...message)
|
||||
Bun.stderr.write("\n")
|
||||
Bun.stderr.write(EOL)
|
||||
}
|
||||
|
||||
export function print(...message: string[]) {
|
||||
@@ -52,7 +53,7 @@ export namespace UI {
|
||||
result.push(row[0])
|
||||
result.push("\x1b[0m")
|
||||
result.push(row[1])
|
||||
result.push("\n")
|
||||
result.push(EOL)
|
||||
}
|
||||
return result.join("").trimEnd()
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Server } from "./server/server"
|
||||
import fs from "fs/promises"
|
||||
import path from "path"
|
||||
import { Share } from "./share/share"
|
||||
import url from "node:url"
|
||||
import { Global } from "./global"
|
||||
import yargs from "yargs"
|
||||
import { hideBin } from "yargs/helpers"
|
||||
@@ -13,6 +14,7 @@ import { ScrapCommand } from "./cli/cmd/scrap"
|
||||
import { Log } from "./util/log"
|
||||
import { AuthCommand, AuthLoginCommand } from "./cli/cmd/auth"
|
||||
import { UpgradeCommand } from "./cli/cmd/upgrade"
|
||||
import { ModelsCommand } from "./cli/cmd/models"
|
||||
import { Provider } from "./provider/provider"
|
||||
import { UI } from "./cli/ui"
|
||||
import { Installation } from "./installation"
|
||||
@@ -20,13 +22,25 @@ import { Bus } from "./bus"
|
||||
import { Config } from "./config/config"
|
||||
import { NamedError } from "./util/error"
|
||||
import { FormatError } from "./cli/error"
|
||||
import { ServeCommand } from "./cli/cmd/serve"
|
||||
|
||||
const cancel = new AbortController()
|
||||
|
||||
process.on("unhandledRejection", (e) => {
|
||||
Log.Default.error("rejection", {
|
||||
e: e instanceof Error ? e.message : e,
|
||||
})
|
||||
})
|
||||
|
||||
process.on("uncaughtException", (e) => {
|
||||
Log.Default.error("exception", {
|
||||
e: e instanceof Error ? e.message : e,
|
||||
})
|
||||
})
|
||||
|
||||
const cli = yargs(hideBin(process.argv))
|
||||
.scriptName("opencode")
|
||||
.help("help", "show help")
|
||||
.alias("help", "h")
|
||||
.version("version", "show version number", Installation.VERSION)
|
||||
.alias("version", "v")
|
||||
.option("print-logs", {
|
||||
@@ -52,7 +66,12 @@ const cli = yargs(hideBin(process.argv))
|
||||
handler: async (args) => {
|
||||
while (true) {
|
||||
const cwd = args.project ? path.resolve(args.project) : process.cwd()
|
||||
process.chdir(cwd)
|
||||
try {
|
||||
process.chdir(cwd)
|
||||
} catch (e) {
|
||||
UI.error("Failed to change directory to " + cwd)
|
||||
return
|
||||
}
|
||||
const result = await App.provide({ cwd }, async (app) => {
|
||||
const providers = await Provider.list()
|
||||
if (Object.keys(providers).length === 0) {
|
||||
@@ -60,13 +79,22 @@ const cli = yargs(hideBin(process.argv))
|
||||
}
|
||||
|
||||
await Share.init()
|
||||
const server = Server.listen()
|
||||
const server = Server.listen({
|
||||
port: 0,
|
||||
hostname: "127.0.0.1",
|
||||
})
|
||||
|
||||
let cmd = ["go", "run", "./main.go"]
|
||||
let cwd = new URL("../../tui/cmd/opencode", import.meta.url).pathname
|
||||
let cwd = url.fileURLToPath(
|
||||
new URL("../../tui/cmd/opencode", import.meta.url),
|
||||
)
|
||||
if (Bun.embeddedFiles.length > 0) {
|
||||
const blob = Bun.embeddedFiles[0] as File
|
||||
const binary = path.join(Global.Path.cache, "tui", blob.name)
|
||||
let binaryName = blob.name
|
||||
if (process.platform === "win32" && !binaryName.endsWith(".exe")) {
|
||||
binaryName += ".exe"
|
||||
}
|
||||
const binary = path.join(Global.Path.cache, "tui", binaryName)
|
||||
const file = Bun.file(binary)
|
||||
if (!(await file.exists())) {
|
||||
await Bun.write(file, blob, { mode: 0o755 })
|
||||
@@ -129,6 +157,8 @@ const cli = yargs(hideBin(process.argv))
|
||||
.command(ScrapCommand)
|
||||
.command(AuthCommand)
|
||||
.command(UpgradeCommand)
|
||||
.command(ServeCommand)
|
||||
.command(ModelsCommand)
|
||||
.fail((msg) => {
|
||||
if (
|
||||
msg.startsWith("Unknown argument") ||
|
||||
@@ -159,7 +189,7 @@ try {
|
||||
Log.Default.error("fatal", data)
|
||||
const formatted = FormatError(e)
|
||||
if (formatted) UI.error(formatted)
|
||||
if (!formatted)
|
||||
if (formatted === undefined)
|
||||
UI.error(
|
||||
"Unexpected error, check log file at " + Log.file() + " for more details",
|
||||
)
|
||||
|
||||
@@ -23,25 +23,25 @@ import { AuthCopilot } from "../auth/copilot"
|
||||
import { ModelsDev } from "./models"
|
||||
import { NamedError } from "../util/error"
|
||||
import { Auth } from "../auth"
|
||||
import { TaskTool } from "../tool/task"
|
||||
|
||||
export namespace Provider {
|
||||
const log = Log.create({ service: "provider" })
|
||||
|
||||
type CustomLoader = (provider: ModelsDev.Provider) => Promise<
|
||||
| {
|
||||
getModel?: (sdk: any, modelID: string) => Promise<any>
|
||||
options: Record<string, any>
|
||||
}
|
||||
| false
|
||||
>
|
||||
type CustomLoader = (
|
||||
provider: ModelsDev.Provider,
|
||||
api?: string,
|
||||
) => Promise<{
|
||||
autoload: boolean
|
||||
getModel?: (sdk: any, modelID: string) => Promise<any>
|
||||
options?: Record<string, any>
|
||||
}>
|
||||
|
||||
type Source = "env" | "config" | "custom" | "api"
|
||||
|
||||
const CUSTOM_LOADERS: Record<string, CustomLoader> = {
|
||||
async anthropic(provider) {
|
||||
const access = await AuthAnthropic.access()
|
||||
if (!access) return false
|
||||
if (!access) return { autoload: false }
|
||||
for (const model of Object.values(provider.models)) {
|
||||
model.cost = {
|
||||
input: 0,
|
||||
@@ -49,6 +49,7 @@ export namespace Provider {
|
||||
}
|
||||
}
|
||||
return {
|
||||
autoload: true,
|
||||
options: {
|
||||
apiKey: "",
|
||||
async fetch(input: any, init: any) {
|
||||
@@ -69,9 +70,9 @@ export namespace Provider {
|
||||
},
|
||||
"github-copilot": async (provider) => {
|
||||
const copilot = await AuthCopilot()
|
||||
if (!copilot) return false
|
||||
if (!copilot) return { autoload: false }
|
||||
let info = await Auth.get("github-copilot")
|
||||
if (!info || info.type !== "oauth") return false
|
||||
if (!info || info.type !== "oauth") return { autoload: false }
|
||||
|
||||
if (provider && provider.models) {
|
||||
for (const model of Object.values(provider.models)) {
|
||||
@@ -83,6 +84,7 @@ export namespace Provider {
|
||||
}
|
||||
|
||||
return {
|
||||
autoload: true,
|
||||
options: {
|
||||
apiKey: "",
|
||||
async fetch(input: any, init: any) {
|
||||
@@ -115,6 +117,7 @@ export namespace Provider {
|
||||
},
|
||||
openai: async () => {
|
||||
return {
|
||||
autoload: false,
|
||||
async getModel(sdk: any, modelID: string) {
|
||||
return sdk.responses(modelID)
|
||||
},
|
||||
@@ -123,7 +126,7 @@ export namespace Provider {
|
||||
},
|
||||
"amazon-bedrock": async () => {
|
||||
if (!process.env["AWS_PROFILE"] && !process.env["AWS_ACCESS_KEY_ID"])
|
||||
return false
|
||||
return { autoload: false }
|
||||
|
||||
const region = process.env["AWS_REGION"] ?? "us-east-1"
|
||||
|
||||
@@ -131,6 +134,7 @@ export namespace Provider {
|
||||
await BunProc.install("@aws-sdk/credential-providers")
|
||||
)
|
||||
return {
|
||||
autoload: true,
|
||||
options: {
|
||||
region,
|
||||
credentialProvider: fromNodeProviderChain(),
|
||||
@@ -256,8 +260,13 @@ export namespace Provider {
|
||||
for (const [providerID, fn] of Object.entries(CUSTOM_LOADERS)) {
|
||||
if (disabled.has(providerID)) continue
|
||||
const result = await fn(database[providerID])
|
||||
if (result) {
|
||||
mergeProvider(providerID, result.options, "custom", result.getModel)
|
||||
if (result && (result.autoload || providers[providerID])) {
|
||||
mergeProvider(
|
||||
providerID,
|
||||
result.options ?? {},
|
||||
"custom",
|
||||
result.getModel,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -401,7 +410,7 @@ export namespace Provider {
|
||||
// MultiEditTool,
|
||||
WriteTool,
|
||||
TodoWriteTool,
|
||||
TaskTool,
|
||||
// TaskTool,
|
||||
TodoReadTool,
|
||||
]
|
||||
|
||||
|
||||
@@ -579,10 +579,10 @@ export namespace Server {
|
||||
return result
|
||||
}
|
||||
|
||||
export function listen() {
|
||||
export function listen(opts: { port: number; hostname: string }) {
|
||||
const server = Bun.serve({
|
||||
port: 0,
|
||||
hostname: "0.0.0.0",
|
||||
port: opts.port,
|
||||
hostname: opts.hostname,
|
||||
idleTimeout: 0,
|
||||
fetch: app().fetch,
|
||||
})
|
||||
|
||||
@@ -287,7 +287,10 @@ export namespace Session {
|
||||
if (
|
||||
model.info.limit.context &&
|
||||
tokens >
|
||||
(model.info.limit.context - (model.info.limit.output ?? 0)) * 0.9
|
||||
Math.max(
|
||||
(model.info.limit.context - (model.info.limit.output ?? 0)) * 0.9,
|
||||
0,
|
||||
)
|
||||
) {
|
||||
await summarize({
|
||||
sessionID: input.sessionID,
|
||||
@@ -657,6 +660,21 @@ export namespace Session {
|
||||
}
|
||||
break
|
||||
|
||||
case "finish":
|
||||
log.info("message finish", {
|
||||
reason: value.finishReason,
|
||||
})
|
||||
const assistant = next.metadata!.assistant!
|
||||
const usage = getUsage(
|
||||
model.info,
|
||||
value.usage,
|
||||
value.providerMetadata,
|
||||
)
|
||||
assistant.cost = usage.cost
|
||||
await updateMessage(next)
|
||||
if (value.finishReason === "length")
|
||||
throw new Message.OutputLengthError({})
|
||||
break
|
||||
default:
|
||||
l.info("unhandled", {
|
||||
type: value.type,
|
||||
@@ -670,6 +688,9 @@ export namespace Session {
|
||||
error: e,
|
||||
})
|
||||
switch (true) {
|
||||
case Message.OutputLengthError.isInstance(e):
|
||||
next.metadata.error = e
|
||||
break
|
||||
case LoadAPIKeyError.isInstance(e):
|
||||
next.metadata.error = new Provider.AuthError(
|
||||
{
|
||||
|
||||
@@ -4,6 +4,11 @@ import { Provider } from "../provider/provider"
|
||||
import { NamedError } from "../util/error"
|
||||
|
||||
export namespace Message {
|
||||
export const OutputLengthError = NamedError.create(
|
||||
"MessageOutputLengthError",
|
||||
z.object({}),
|
||||
)
|
||||
|
||||
export const ToolCall = z
|
||||
.object({
|
||||
state: z.literal("call"),
|
||||
@@ -145,6 +150,7 @@ export namespace Message {
|
||||
.discriminatedUnion("name", [
|
||||
Provider.AuthError.Schema,
|
||||
NamedError.Unknown.Schema,
|
||||
OutputLengthError.Schema,
|
||||
])
|
||||
.optional(),
|
||||
sessionID: z.string(),
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
// the approaches in this edit tool are sourced from
|
||||
// https://github.com/cline/cline/blob/main/evals/diff-edits/diff-apply/diff-06-23-25.ts
|
||||
// https://github.com/google-gemini/gemini-cli/blob/main/packages/core/src/utils/editCorrector.ts
|
||||
|
||||
import { z } from "zod"
|
||||
import * as path from "path"
|
||||
import { Tool } from "./tool"
|
||||
@@ -29,6 +33,10 @@ export const EditTool = Tool.define({
|
||||
throw new Error("filePath is required")
|
||||
}
|
||||
|
||||
if (params.oldString === params.newString) {
|
||||
throw new Error("oldString and newString must be different")
|
||||
}
|
||||
|
||||
const app = App.info()
|
||||
const filepath = path.isAbsolute(params.filePath)
|
||||
? params.filePath
|
||||
@@ -55,35 +63,19 @@ export const EditTool = Tool.define({
|
||||
}
|
||||
|
||||
const file = Bun.file(filepath)
|
||||
if (!(await file.exists())) throw new Error(`File ${filepath} not found`)
|
||||
const stats = await file.stat()
|
||||
const stats = await file.stat().catch(() => {})
|
||||
if (!stats) throw new Error(`File ${filepath} not found`)
|
||||
if (stats.isDirectory())
|
||||
throw new Error(`Path is a directory, not a file: ${filepath}`)
|
||||
await FileTimes.assert(ctx.sessionID, filepath)
|
||||
contentOld = await file.text()
|
||||
const index = contentOld.indexOf(params.oldString)
|
||||
if (index === -1)
|
||||
throw new Error(
|
||||
`oldString not found in file. Make sure it matches exactly, including whitespace and line breaks`,
|
||||
)
|
||||
|
||||
if (params.replaceAll) {
|
||||
contentNew = contentOld.replaceAll(params.oldString, params.newString)
|
||||
}
|
||||
|
||||
if (!params.replaceAll) {
|
||||
const lastIndex = contentOld.lastIndexOf(params.oldString)
|
||||
if (index !== lastIndex)
|
||||
throw new Error(
|
||||
`oldString appears multiple times in the file. Please provide more context to ensure a unique match`,
|
||||
)
|
||||
|
||||
contentNew =
|
||||
contentOld.substring(0, index) +
|
||||
params.newString +
|
||||
contentOld.substring(index + params.oldString.length)
|
||||
}
|
||||
|
||||
contentNew = replace(
|
||||
contentOld,
|
||||
params.oldString,
|
||||
params.newString,
|
||||
params.replaceAll,
|
||||
)
|
||||
await file.write(contentNew)
|
||||
})()
|
||||
|
||||
@@ -116,6 +108,326 @@ export const EditTool = Tool.define({
|
||||
},
|
||||
})
|
||||
|
||||
export type Replacer = (
|
||||
content: string,
|
||||
find: string,
|
||||
) => Generator<string, void, unknown>
|
||||
|
||||
export const SimpleReplacer: Replacer = function* (_content, find) {
|
||||
yield find
|
||||
}
|
||||
|
||||
export const LineTrimmedReplacer: Replacer = function* (content, find) {
|
||||
const originalLines = content.split("\n")
|
||||
const searchLines = find.split("\n")
|
||||
|
||||
if (searchLines[searchLines.length - 1] === "") {
|
||||
searchLines.pop()
|
||||
}
|
||||
|
||||
for (let i = 0; i <= originalLines.length - searchLines.length; i++) {
|
||||
let matches = true
|
||||
|
||||
for (let j = 0; j < searchLines.length; j++) {
|
||||
const originalTrimmed = originalLines[i + j].trim()
|
||||
const searchTrimmed = searchLines[j].trim()
|
||||
|
||||
if (originalTrimmed !== searchTrimmed) {
|
||||
matches = false
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (matches) {
|
||||
let matchStartIndex = 0
|
||||
for (let k = 0; k < i; k++) {
|
||||
matchStartIndex += originalLines[k].length + 1
|
||||
}
|
||||
|
||||
let matchEndIndex = matchStartIndex
|
||||
for (let k = 0; k < searchLines.length; k++) {
|
||||
matchEndIndex += originalLines[i + k].length + 1
|
||||
}
|
||||
|
||||
yield content.substring(matchStartIndex, matchEndIndex)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const BlockAnchorReplacer: Replacer = function* (content, find) {
|
||||
const originalLines = content.split("\n")
|
||||
const searchLines = find.split("\n")
|
||||
|
||||
if (searchLines.length < 3) {
|
||||
return
|
||||
}
|
||||
|
||||
if (searchLines[searchLines.length - 1] === "") {
|
||||
searchLines.pop()
|
||||
}
|
||||
|
||||
const firstLineSearch = searchLines[0].trim()
|
||||
const lastLineSearch = searchLines[searchLines.length - 1].trim()
|
||||
|
||||
// Find blocks where first line matches the search first line
|
||||
for (let i = 0; i < originalLines.length; i++) {
|
||||
if (originalLines[i].trim() !== firstLineSearch) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Look for the matching last line after this first line
|
||||
for (let j = i + 2; j < originalLines.length; j++) {
|
||||
if (originalLines[j].trim() === lastLineSearch) {
|
||||
// Found a potential block from i to j
|
||||
let matchStartIndex = 0
|
||||
for (let k = 0; k < i; k++) {
|
||||
matchStartIndex += originalLines[k].length + 1
|
||||
}
|
||||
|
||||
let matchEndIndex = matchStartIndex
|
||||
for (let k = 0; k <= j - i; k++) {
|
||||
matchEndIndex += originalLines[i + k].length
|
||||
if (k < j - i) {
|
||||
matchEndIndex += 1 // Add newline character except for the last line
|
||||
}
|
||||
}
|
||||
|
||||
yield content.substring(matchStartIndex, matchEndIndex)
|
||||
break // Only match the first occurrence of the last line
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const WhitespaceNormalizedReplacer: Replacer = function* (
|
||||
content,
|
||||
find,
|
||||
) {
|
||||
const normalizeWhitespace = (text: string) => text.replace(/\s+/g, " ").trim()
|
||||
const normalizedFind = normalizeWhitespace(find)
|
||||
|
||||
// Handle single line matches
|
||||
const lines = content.split("\n")
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i]
|
||||
if (normalizeWhitespace(line) === normalizedFind) {
|
||||
yield line
|
||||
}
|
||||
|
||||
// Also check for substring matches within lines
|
||||
const normalizedLine = normalizeWhitespace(line)
|
||||
if (normalizedLine.includes(normalizedFind)) {
|
||||
// Find the actual substring in the original line that matches
|
||||
const words = find.trim().split(/\s+/)
|
||||
if (words.length > 0) {
|
||||
const pattern = words
|
||||
.map((word) => word.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"))
|
||||
.join("\\s+")
|
||||
try {
|
||||
const regex = new RegExp(pattern)
|
||||
const match = line.match(regex)
|
||||
if (match) {
|
||||
yield match[0]
|
||||
}
|
||||
} catch (e) {
|
||||
// Invalid regex pattern, skip
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle multi-line matches
|
||||
const findLines = find.split("\n")
|
||||
if (findLines.length > 1) {
|
||||
for (let i = 0; i <= lines.length - findLines.length; i++) {
|
||||
const block = lines.slice(i, i + findLines.length)
|
||||
if (normalizeWhitespace(block.join("\n")) === normalizedFind) {
|
||||
yield block.join("\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const IndentationFlexibleReplacer: Replacer = function* (content, find) {
|
||||
const removeIndentation = (text: string) => {
|
||||
const lines = text.split("\n")
|
||||
const nonEmptyLines = lines.filter((line) => line.trim().length > 0)
|
||||
if (nonEmptyLines.length === 0) return text
|
||||
|
||||
const minIndent = Math.min(
|
||||
...nonEmptyLines.map((line) => {
|
||||
const match = line.match(/^(\s*)/)
|
||||
return match ? match[1].length : 0
|
||||
}),
|
||||
)
|
||||
|
||||
return lines
|
||||
.map((line) => (line.trim().length === 0 ? line : line.slice(minIndent)))
|
||||
.join("\n")
|
||||
}
|
||||
|
||||
const normalizedFind = removeIndentation(find)
|
||||
const contentLines = content.split("\n")
|
||||
const findLines = find.split("\n")
|
||||
|
||||
for (let i = 0; i <= contentLines.length - findLines.length; i++) {
|
||||
const block = contentLines.slice(i, i + findLines.length).join("\n")
|
||||
if (removeIndentation(block) === normalizedFind) {
|
||||
yield block
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const EscapeNormalizedReplacer: Replacer = function* (content, find) {
|
||||
const unescapeString = (str: string): string => {
|
||||
return str.replace(/\\(n|t|r|'|"|`|\\|\n|\$)/g, (match, capturedChar) => {
|
||||
switch (capturedChar) {
|
||||
case "n":
|
||||
return "\n"
|
||||
case "t":
|
||||
return "\t"
|
||||
case "r":
|
||||
return "\r"
|
||||
case "'":
|
||||
return "'"
|
||||
case '"':
|
||||
return '"'
|
||||
case "`":
|
||||
return "`"
|
||||
case "\\":
|
||||
return "\\"
|
||||
case "\n":
|
||||
return "\n"
|
||||
case "$":
|
||||
return "$"
|
||||
default:
|
||||
return match
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const unescapedFind = unescapeString(find)
|
||||
|
||||
// Try direct match with unescaped find string
|
||||
if (content.includes(unescapedFind)) {
|
||||
yield unescapedFind
|
||||
}
|
||||
|
||||
// Also try finding escaped versions in content that match unescaped find
|
||||
const lines = content.split("\n")
|
||||
const findLines = unescapedFind.split("\n")
|
||||
|
||||
for (let i = 0; i <= lines.length - findLines.length; i++) {
|
||||
const block = lines.slice(i, i + findLines.length).join("\n")
|
||||
const unescapedBlock = unescapeString(block)
|
||||
|
||||
if (unescapedBlock === unescapedFind) {
|
||||
yield block
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const MultiOccurrenceReplacer: Replacer = function* (content, find) {
|
||||
// This replacer yields all exact matches, allowing the replace function
|
||||
// to handle multiple occurrences based on replaceAll parameter
|
||||
let startIndex = 0
|
||||
|
||||
while (true) {
|
||||
const index = content.indexOf(find, startIndex)
|
||||
if (index === -1) break
|
||||
|
||||
yield find
|
||||
startIndex = index + find.length
|
||||
}
|
||||
}
|
||||
|
||||
export const TrimmedBoundaryReplacer: Replacer = function* (content, find) {
|
||||
const trimmedFind = find.trim()
|
||||
|
||||
if (trimmedFind === find) {
|
||||
// Already trimmed, no point in trying
|
||||
return
|
||||
}
|
||||
|
||||
// Try to find the trimmed version
|
||||
if (content.includes(trimmedFind)) {
|
||||
yield trimmedFind
|
||||
}
|
||||
|
||||
// Also try finding blocks where trimmed content matches
|
||||
const lines = content.split("\n")
|
||||
const findLines = find.split("\n")
|
||||
|
||||
for (let i = 0; i <= lines.length - findLines.length; i++) {
|
||||
const block = lines.slice(i, i + findLines.length).join("\n")
|
||||
|
||||
if (block.trim() === trimmedFind) {
|
||||
yield block
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const ContextAwareReplacer: Replacer = function* (content, find) {
|
||||
const findLines = find.split("\n")
|
||||
if (findLines.length < 3) {
|
||||
// Need at least 3 lines to have meaningful context
|
||||
return
|
||||
}
|
||||
|
||||
// Remove trailing empty line if present
|
||||
if (findLines[findLines.length - 1] === "") {
|
||||
findLines.pop()
|
||||
}
|
||||
|
||||
const contentLines = content.split("\n")
|
||||
|
||||
// Extract first and last lines as context anchors
|
||||
const firstLine = findLines[0].trim()
|
||||
const lastLine = findLines[findLines.length - 1].trim()
|
||||
|
||||
// Find blocks that start and end with the context anchors
|
||||
for (let i = 0; i < contentLines.length; i++) {
|
||||
if (contentLines[i].trim() !== firstLine) continue
|
||||
|
||||
// Look for the matching last line
|
||||
for (let j = i + 2; j < contentLines.length; j++) {
|
||||
if (contentLines[j].trim() === lastLine) {
|
||||
// Found a potential context block
|
||||
const blockLines = contentLines.slice(i, j + 1)
|
||||
const block = blockLines.join("\n")
|
||||
|
||||
// Check if the middle content has reasonable similarity
|
||||
// (simple heuristic: at least 50% of non-empty lines should match when trimmed)
|
||||
if (blockLines.length === findLines.length) {
|
||||
let matchingLines = 0
|
||||
let totalNonEmptyLines = 0
|
||||
|
||||
for (let k = 1; k < blockLines.length - 1; k++) {
|
||||
const blockLine = blockLines[k].trim()
|
||||
const findLine = findLines[k].trim()
|
||||
|
||||
if (blockLine.length > 0 || findLine.length > 0) {
|
||||
totalNonEmptyLines++
|
||||
if (blockLine === findLine) {
|
||||
matchingLines++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
totalNonEmptyLines === 0 ||
|
||||
matchingLines / totalNonEmptyLines >= 0.5
|
||||
) {
|
||||
yield block
|
||||
break // Only match the first occurrence
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function trimDiff(diff: string): string {
|
||||
const lines = diff.split("\n")
|
||||
const contentLines = lines.filter(
|
||||
@@ -151,3 +463,42 @@ function trimDiff(diff: string): string {
|
||||
|
||||
return trimmedLines.join("\n")
|
||||
}
|
||||
|
||||
export function replace(
|
||||
content: string,
|
||||
oldString: string,
|
||||
newString: string,
|
||||
replaceAll = false,
|
||||
): string {
|
||||
if (oldString === newString) {
|
||||
throw new Error("oldString and newString must be different")
|
||||
}
|
||||
|
||||
for (const replacer of [
|
||||
SimpleReplacer,
|
||||
LineTrimmedReplacer,
|
||||
BlockAnchorReplacer,
|
||||
WhitespaceNormalizedReplacer,
|
||||
IndentationFlexibleReplacer,
|
||||
EscapeNormalizedReplacer,
|
||||
TrimmedBoundaryReplacer,
|
||||
ContextAwareReplacer,
|
||||
MultiOccurrenceReplacer,
|
||||
]) {
|
||||
for (const search of replacer(content, oldString)) {
|
||||
const index = content.indexOf(search)
|
||||
if (index === -1) continue
|
||||
if (replaceAll) {
|
||||
return content.replaceAll(search, newString)
|
||||
}
|
||||
const lastIndex = content.lastIndexOf(search)
|
||||
if (index !== lastIndex) continue
|
||||
return (
|
||||
content.substring(0, index) +
|
||||
newString +
|
||||
content.substring(index + search.length)
|
||||
)
|
||||
}
|
||||
}
|
||||
throw new Error("oldString not found in content or was found multiple times")
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ export namespace Log {
|
||||
await fs.mkdir(dir, { recursive: true })
|
||||
cleanup(dir)
|
||||
if (options.print) return
|
||||
logpath = path.join(dir, new Date().toISOString().split(".")[0] + ".log")
|
||||
logpath = path.join(dir, new Date().toISOString().split(".")[0].replace(/:/g, "") + ".log")
|
||||
const logfile = Bun.file(logpath)
|
||||
await fs.truncate(logpath).catch(() => {})
|
||||
const writer = logfile.writer()
|
||||
|
||||
@@ -0,0 +1,413 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { replace } from "../../src/tool/edit"
|
||||
|
||||
interface TestCase {
|
||||
content: string
|
||||
find: string
|
||||
replace: string
|
||||
all?: boolean
|
||||
fail?: boolean
|
||||
}
|
||||
|
||||
const testCases: TestCase[] = [
|
||||
// SimpleReplacer cases
|
||||
{
|
||||
content: ["function hello() {", ' console.log("world");', "}"].join("\n"),
|
||||
find: 'console.log("world");',
|
||||
replace: 'console.log("universe");',
|
||||
},
|
||||
{
|
||||
content: [
|
||||
"if (condition) {",
|
||||
" doSomething();",
|
||||
" doSomethingElse();",
|
||||
"}",
|
||||
].join("\n"),
|
||||
find: [" doSomething();", " doSomethingElse();"].join("\n"),
|
||||
replace: [" doNewThing();", " doAnotherThing();"].join("\n"),
|
||||
},
|
||||
|
||||
// LineTrimmedReplacer cases
|
||||
{
|
||||
content: ["function test() {", ' console.log("hello");', "}"].join("\n"),
|
||||
find: 'console.log("hello");',
|
||||
replace: 'console.log("goodbye");',
|
||||
},
|
||||
{
|
||||
content: ["const x = 5; ", "const y = 10;"].join("\n"),
|
||||
find: "const x = 5;",
|
||||
replace: "const x = 15;",
|
||||
},
|
||||
{
|
||||
content: [" if (true) {", " return false;", " }"].join("\n"),
|
||||
find: ["if (true) {", "return false;", "}"].join("\n"),
|
||||
replace: ["if (false) {", "return true;", "}"].join("\n"),
|
||||
},
|
||||
|
||||
// BlockAnchorReplacer cases
|
||||
{
|
||||
content: [
|
||||
"function calculate(a, b) {",
|
||||
" const temp = a + b;",
|
||||
" const result = temp * 2;",
|
||||
" return result;",
|
||||
"}",
|
||||
].join("\n"),
|
||||
find: [
|
||||
"function calculate(a, b) {",
|
||||
" // different middle content",
|
||||
" return result;",
|
||||
"}",
|
||||
].join("\n"),
|
||||
replace: ["function calculate(a, b) {", " return a * b * 2;", "}"].join(
|
||||
"\n",
|
||||
),
|
||||
},
|
||||
{
|
||||
content: [
|
||||
"class MyClass {",
|
||||
" constructor() {",
|
||||
" this.value = 0;",
|
||||
" }",
|
||||
" ",
|
||||
" getValue() {",
|
||||
" return this.value;",
|
||||
" }",
|
||||
"}",
|
||||
].join("\n"),
|
||||
find: ["class MyClass {", " // different implementation", "}"].join("\n"),
|
||||
replace: [
|
||||
"class MyClass {",
|
||||
" constructor() {",
|
||||
" this.value = 42;",
|
||||
" }",
|
||||
"}",
|
||||
].join("\n"),
|
||||
},
|
||||
|
||||
// WhitespaceNormalizedReplacer cases
|
||||
{
|
||||
content: ["function test() {", '\tconsole.log("hello");', "}"].join("\n"),
|
||||
find: ' console.log("hello");',
|
||||
replace: ' console.log("world");',
|
||||
},
|
||||
{
|
||||
content: "const x = 5;",
|
||||
find: "const x = 5;",
|
||||
replace: "const x = 10;",
|
||||
},
|
||||
{
|
||||
content: "if\t( condition\t) {",
|
||||
find: "if ( condition ) {",
|
||||
replace: "if (newCondition) {",
|
||||
},
|
||||
|
||||
// IndentationFlexibleReplacer cases
|
||||
{
|
||||
content: [
|
||||
" function nested() {",
|
||||
' console.log("deeply nested");',
|
||||
" return true;",
|
||||
" }",
|
||||
].join("\n"),
|
||||
find: [
|
||||
"function nested() {",
|
||||
' console.log("deeply nested");',
|
||||
" return true;",
|
||||
"}",
|
||||
].join("\n"),
|
||||
replace: [
|
||||
"function nested() {",
|
||||
' console.log("updated");',
|
||||
" return false;",
|
||||
"}",
|
||||
].join("\n"),
|
||||
},
|
||||
{
|
||||
content: [
|
||||
" if (true) {",
|
||||
' console.log("level 1");',
|
||||
' console.log("level 2");',
|
||||
" }",
|
||||
].join("\n"),
|
||||
find: [
|
||||
"if (true) {",
|
||||
'console.log("level 1");',
|
||||
' console.log("level 2");',
|
||||
"}",
|
||||
].join("\n"),
|
||||
replace: ["if (true) {", 'console.log("updated");', "}"].join("\n"),
|
||||
},
|
||||
|
||||
// replaceAll option cases
|
||||
{
|
||||
content: [
|
||||
'console.log("test");',
|
||||
'console.log("test");',
|
||||
'console.log("test");',
|
||||
].join("\n"),
|
||||
find: 'console.log("test");',
|
||||
replace: 'console.log("updated");',
|
||||
all: true,
|
||||
},
|
||||
{
|
||||
content: ['console.log("test");', 'console.log("test");'].join("\n"),
|
||||
find: 'console.log("test");',
|
||||
replace: 'console.log("updated");',
|
||||
all: false,
|
||||
},
|
||||
|
||||
// Error cases
|
||||
{
|
||||
content: 'console.log("hello");',
|
||||
find: "nonexistent string",
|
||||
replace: "updated",
|
||||
fail: true,
|
||||
},
|
||||
{
|
||||
content: ["test", "test", "different content", "test"].join("\n"),
|
||||
find: "test",
|
||||
replace: "updated",
|
||||
all: false,
|
||||
fail: true,
|
||||
},
|
||||
|
||||
// Edge cases
|
||||
{
|
||||
content: "",
|
||||
find: "",
|
||||
replace: "new content",
|
||||
},
|
||||
{
|
||||
content: "const regex = /[.*+?^${}()|[\\\\]\\\\\\\\]/g;",
|
||||
find: "/[.*+?^${}()|[\\\\]\\\\\\\\]/g",
|
||||
replace: "/\\\\w+/g",
|
||||
},
|
||||
{
|
||||
content: 'const message = "Hello 世界! 🌍";',
|
||||
find: "Hello 世界! 🌍",
|
||||
replace: "Hello World! 🌎",
|
||||
},
|
||||
|
||||
// EscapeNormalizedReplacer cases
|
||||
{
|
||||
content: 'console.log("Hello\nWorld");',
|
||||
find: 'console.log("Hello\\nWorld");',
|
||||
replace: 'console.log("Hello\nUniverse");',
|
||||
},
|
||||
{
|
||||
content: "const str = 'It's working';",
|
||||
find: "const str = 'It\\'s working';",
|
||||
replace: "const str = 'It's fixed';",
|
||||
},
|
||||
{
|
||||
content: "const template = `Hello ${name}`;",
|
||||
find: "const template = `Hello \\${name}`;",
|
||||
replace: "const template = `Hi ${name}`;",
|
||||
},
|
||||
{
|
||||
content: "const path = 'C:\\Users\\test';",
|
||||
find: "const path = 'C:\\\\Users\\\\test';",
|
||||
replace: "const path = 'C:\\Users\\admin';",
|
||||
},
|
||||
|
||||
// MultiOccurrenceReplacer cases (with replaceAll)
|
||||
{
|
||||
content: ["debug('start');", "debug('middle');", "debug('end');"].join(
|
||||
"\n",
|
||||
),
|
||||
find: "debug",
|
||||
replace: "log",
|
||||
all: true,
|
||||
},
|
||||
{
|
||||
content: "const x = 1; const y = 1; const z = 1;",
|
||||
find: "1",
|
||||
replace: "2",
|
||||
all: true,
|
||||
},
|
||||
|
||||
// TrimmedBoundaryReplacer cases
|
||||
{
|
||||
content: [" function test() {", " return true;", " }"].join("\n"),
|
||||
find: ["function test() {", " return true;", "}"].join("\n"),
|
||||
replace: ["function test() {", " return false;", "}"].join("\n"),
|
||||
},
|
||||
{
|
||||
content: "\n const value = 42; \n",
|
||||
find: "const value = 42;",
|
||||
replace: "const value = 24;",
|
||||
},
|
||||
{
|
||||
content: ["", " if (condition) {", " doSomething();", " }", ""].join(
|
||||
"\n",
|
||||
),
|
||||
find: ["if (condition) {", " doSomething();", "}"].join("\n"),
|
||||
replace: ["if (condition) {", " doNothing();", "}"].join("\n"),
|
||||
},
|
||||
|
||||
// ContextAwareReplacer cases
|
||||
{
|
||||
content: [
|
||||
"function calculate(a, b) {",
|
||||
" const temp = a + b;",
|
||||
" const result = temp * 2;",
|
||||
" return result;",
|
||||
"}",
|
||||
].join("\n"),
|
||||
find: [
|
||||
"function calculate(a, b) {",
|
||||
" // some different content here",
|
||||
" // more different content",
|
||||
" return result;",
|
||||
"}",
|
||||
].join("\n"),
|
||||
replace: ["function calculate(a, b) {", " return (a + b) * 2;", "}"].join(
|
||||
"\n",
|
||||
),
|
||||
},
|
||||
{
|
||||
content: [
|
||||
"class TestClass {",
|
||||
" constructor() {",
|
||||
" this.value = 0;",
|
||||
" }",
|
||||
" ",
|
||||
" method() {",
|
||||
" return this.value;",
|
||||
" }",
|
||||
"}",
|
||||
].join("\n"),
|
||||
find: [
|
||||
"class TestClass {",
|
||||
" // different implementation",
|
||||
" // with multiple lines",
|
||||
"}",
|
||||
].join("\n"),
|
||||
replace: ["class TestClass {", " getValue() { return 42; }", "}"].join(
|
||||
"\n",
|
||||
),
|
||||
},
|
||||
|
||||
// Combined edge cases for new replacers
|
||||
{
|
||||
content: '\tconsole.log("test");\t',
|
||||
find: 'console.log("test");',
|
||||
replace: 'console.log("updated");',
|
||||
},
|
||||
{
|
||||
content: [" ", "function test() {", " return 'value';", "}", " "].join(
|
||||
"\n",
|
||||
),
|
||||
find: ["function test() {", "return 'value';", "}"].join("\n"),
|
||||
replace: ["function test() {", "return 'new value';", "}"].join("\n"),
|
||||
},
|
||||
|
||||
// Test for same oldString and newString (should fail)
|
||||
{
|
||||
content: 'console.log("test");',
|
||||
find: 'console.log("test");',
|
||||
replace: 'console.log("test");',
|
||||
fail: true,
|
||||
},
|
||||
|
||||
// Additional tests for fixes made
|
||||
|
||||
// WhitespaceNormalizedReplacer - test regex special characters that could cause errors
|
||||
{
|
||||
content: 'const pattern = "test[123]";',
|
||||
find: 'test[123]',
|
||||
replace: 'test[456]',
|
||||
},
|
||||
{
|
||||
content: 'const regex = "^start.*end$";',
|
||||
find: '^start.*end$',
|
||||
replace: '^begin.*finish$',
|
||||
},
|
||||
|
||||
// EscapeNormalizedReplacer - test single backslash vs double backslash
|
||||
{
|
||||
content: 'const path = "C:\\Users";',
|
||||
find: 'const path = "C:\\Users";',
|
||||
replace: 'const path = "D:\\Users";',
|
||||
},
|
||||
{
|
||||
content: 'console.log("Line1\\nLine2");',
|
||||
find: 'console.log("Line1\\nLine2");',
|
||||
replace: 'console.log("First\\nSecond");',
|
||||
},
|
||||
|
||||
// BlockAnchorReplacer - test edge case with exact newline boundaries
|
||||
{
|
||||
content: ["function test() {", " return true;", "}"].join("\n"),
|
||||
find: ["function test() {", " // middle", "}"].join("\n"),
|
||||
replace: ["function test() {", " return false;", "}"].join("\n"),
|
||||
},
|
||||
|
||||
// ContextAwareReplacer - test with trailing newline in find string
|
||||
{
|
||||
content: [
|
||||
"class Test {",
|
||||
" method1() {",
|
||||
" return 1;",
|
||||
" }",
|
||||
"}",
|
||||
].join("\n"),
|
||||
find: [
|
||||
"class Test {",
|
||||
" // different content",
|
||||
"}",
|
||||
"", // trailing empty line
|
||||
].join("\n"),
|
||||
replace: ["class Test {", " method2() { return 2; }", "}"].join("\n"),
|
||||
},
|
||||
|
||||
// Test validation for empty strings with same oldString and newString
|
||||
{
|
||||
content: "",
|
||||
find: "",
|
||||
replace: "",
|
||||
fail: true,
|
||||
},
|
||||
|
||||
// Test multiple occurrences with replaceAll=false (should fail)
|
||||
{
|
||||
content: ["const a = 1;", "const b = 1;", "const c = 1;"].join("\n"),
|
||||
find: "= 1",
|
||||
replace: "= 2",
|
||||
all: false,
|
||||
fail: true,
|
||||
},
|
||||
|
||||
// Test whitespace normalization with multiple spaces and tabs mixed
|
||||
{
|
||||
content: "if\t \t( \tcondition\t )\t{",
|
||||
find: "if ( condition ) {",
|
||||
replace: "if (newCondition) {",
|
||||
},
|
||||
|
||||
// Test escape sequences in template literals
|
||||
{
|
||||
content: "const msg = `Hello\\tWorld`;",
|
||||
find: "const msg = `Hello\\tWorld`;",
|
||||
replace: "const msg = `Hi\\tWorld`;",
|
||||
},
|
||||
]
|
||||
|
||||
describe("EditTool Replacers", () => {
|
||||
test.each(testCases)("case %#", (testCase) => {
|
||||
if (testCase.fail) {
|
||||
expect(() => {
|
||||
replace(testCase.content, testCase.find, testCase.replace, testCase.all)
|
||||
}).toThrow()
|
||||
} else {
|
||||
const result = replace(
|
||||
testCase.content,
|
||||
testCase.find,
|
||||
testCase.replace,
|
||||
testCase.all,
|
||||
)
|
||||
expect(result).toContain(testCase.replace)
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1 @@
|
||||
opencode-test
|
||||
@@ -0,0 +1,26 @@
|
||||
# TUI Agent Guidelines
|
||||
|
||||
## Build/Test Commands
|
||||
|
||||
- **Build**: `go build ./cmd/opencode` (builds main binary)
|
||||
- **Test**: `go test ./...` (runs all tests)
|
||||
- **Single test**: `go test ./internal/theme -run TestLoadThemesFromJSON` (specific test)
|
||||
- **Generate client**: `go generate ./pkg/client/` (after server endpoint changes)
|
||||
- **Release build**: Uses `.goreleaser.yml` configuration
|
||||
|
||||
## Code Style
|
||||
|
||||
- **Language**: Go 1.24+ with standard formatting (`gofmt`)
|
||||
- **Imports**: Group standard, third-party, local packages with blank lines
|
||||
- **Naming**: Go conventions - PascalCase exports, camelCase private, ALL_CAPS constants
|
||||
- **Error handling**: Return errors explicitly, use `fmt.Errorf` for wrapping
|
||||
- **Structs**: Define clear interfaces, embed when appropriate
|
||||
- **Testing**: Use table-driven tests, `t.TempDir()` for file operations
|
||||
|
||||
## Architecture
|
||||
|
||||
- **TUI Framework**: Bubble Tea v2 with Lipgloss v2 for styling
|
||||
- **Client**: Generated OpenAPI client communicates with TypeScript server
|
||||
- **Components**: Reusable UI components in `internal/components/`
|
||||
- **Themes**: JSON-based theming system with override hierarchy
|
||||
- **State**: Centralized app state with message passing
|
||||
@@ -66,6 +66,7 @@ func main() {
|
||||
|
||||
program := tea.NewProgram(
|
||||
tui.NewModel(app_),
|
||||
// tea.WithColorProfile(colorprofile.ANSI),
|
||||
tea.WithAltScreen(),
|
||||
tea.WithKeyboardEnhancements(),
|
||||
tea.WithMouseCellMotion(),
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"github.com/sst/opencode/internal/commands"
|
||||
"github.com/sst/opencode/internal/components/toast"
|
||||
"github.com/sst/opencode/internal/config"
|
||||
"github.com/sst/opencode/internal/styles"
|
||||
"github.com/sst/opencode/internal/theme"
|
||||
"github.com/sst/opencode/internal/util"
|
||||
"github.com/sst/opencode/pkg/client"
|
||||
@@ -103,6 +104,12 @@ func New(
|
||||
}
|
||||
|
||||
if appState.Theme != "" {
|
||||
if appState.Theme == "system" && styles.Terminal != nil {
|
||||
theme.UpdateSystemTheme(
|
||||
styles.Terminal.Background,
|
||||
styles.Terminal.BackgroundIsDark,
|
||||
)
|
||||
}
|
||||
theme.SetTheme(appState.Theme)
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"github.com/sst/opencode/internal/app"
|
||||
"github.com/sst/opencode/internal/commands"
|
||||
"github.com/sst/opencode/internal/components/dialog"
|
||||
"github.com/sst/opencode/internal/styles"
|
||||
"github.com/sst/opencode/internal/theme"
|
||||
)
|
||||
|
||||
@@ -37,7 +38,7 @@ func (c *CommandCompletionProvider) GetEmptyMessage() string {
|
||||
|
||||
func getCommandCompletionItem(cmd commands.Command, space int, t theme.Theme) dialog.CompletionItemI {
|
||||
spacer := strings.Repeat(" ", space)
|
||||
title := " /" + cmd.Trigger + lipgloss.NewStyle().Foreground(t.TextMuted()).Render(spacer+cmd.Description)
|
||||
title := " /" + cmd.Trigger + styles.NewStyle().Foreground(t.TextMuted()).Render(spacer+cmd.Description)
|
||||
value := string(cmd.Name)
|
||||
return dialog.NewCompletionItem(dialog.CompletionItem{
|
||||
Title: title,
|
||||
|
||||
@@ -26,6 +26,9 @@ type EditorComponent interface {
|
||||
Content() string
|
||||
Lines() int
|
||||
Value() string
|
||||
Focused() bool
|
||||
Focus() (tea.Model, tea.Cmd)
|
||||
Blur()
|
||||
Submit() (tea.Model, tea.Cmd)
|
||||
Clear() (tea.Model, tea.Cmd)
|
||||
Paste() (tea.Model, tea.Cmd)
|
||||
@@ -48,7 +51,7 @@ type editorComponent struct {
|
||||
}
|
||||
|
||||
func (m *editorComponent) Init() tea.Cmd {
|
||||
return tea.Batch(textarea.Blink, m.spinner.Tick, tea.EnableReportFocus)
|
||||
return tea.Batch(m.textarea.Focus(), m.spinner.Tick, tea.EnableReportFocus)
|
||||
}
|
||||
|
||||
func (m *editorComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
@@ -69,7 +72,7 @@ func (m *editorComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
case dialog.ThemeSelectedMsg:
|
||||
m.textarea = createTextArea(&m.textarea)
|
||||
m.spinner = createSpinner()
|
||||
return m, tea.Batch(m.spinner.Tick, textarea.Blink)
|
||||
return m, tea.Batch(m.spinner.Tick, m.textarea.Focus())
|
||||
case dialog.CompletionSelectedMsg:
|
||||
if msg.IsCommand {
|
||||
commandName := strings.TrimPrefix(msg.CompletionValue, "/")
|
||||
@@ -80,8 +83,15 @@ func (m *editorComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
return m, tea.Batch(cmds...)
|
||||
} else {
|
||||
existingValue := m.textarea.Value()
|
||||
modifiedValue := strings.Replace(existingValue, msg.SearchString, msg.CompletionValue, 1)
|
||||
m.textarea.SetValue(modifiedValue + " ")
|
||||
|
||||
// Replace the current token (after last space)
|
||||
lastSpaceIndex := strings.LastIndex(existingValue, " ")
|
||||
if lastSpaceIndex == -1 {
|
||||
m.textarea.SetValue(msg.CompletionValue + " ")
|
||||
} else {
|
||||
modifiedValue := existingValue[:lastSpaceIndex+1] + msg.CompletionValue
|
||||
m.textarea.SetValue(modifiedValue + " ")
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
}
|
||||
@@ -97,12 +107,11 @@ func (m *editorComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
|
||||
func (m *editorComponent) Content() string {
|
||||
t := theme.CurrentTheme()
|
||||
base := styles.BaseStyle().Background(t.Background()).Render
|
||||
muted := styles.Muted().Background(t.Background()).Render
|
||||
promptStyle := lipgloss.NewStyle().
|
||||
base := styles.NewStyle().Foreground(t.Text()).Background(t.Background()).Render
|
||||
muted := styles.NewStyle().Foreground(t.TextMuted()).Background(t.Background()).Render
|
||||
promptStyle := styles.NewStyle().Foreground(t.Primary()).
|
||||
Padding(0, 0, 0, 1).
|
||||
Bold(true).
|
||||
Foreground(t.Primary())
|
||||
Bold(true)
|
||||
prompt := promptStyle.Render(">")
|
||||
|
||||
textarea := lipgloss.JoinHorizontal(
|
||||
@@ -110,11 +119,16 @@ func (m *editorComponent) Content() string {
|
||||
prompt,
|
||||
m.textarea.View(),
|
||||
)
|
||||
textarea = styles.BaseStyle().
|
||||
textarea = styles.NewStyle().
|
||||
Background(t.BackgroundElement()).
|
||||
Width(m.width).
|
||||
PaddingTop(1).
|
||||
PaddingBottom(1).
|
||||
Background(t.BackgroundElement()).
|
||||
BorderStyle(lipgloss.ThickBorder()).
|
||||
BorderForeground(t.Border()).
|
||||
BorderBackground(t.Background()).
|
||||
BorderLeft(true).
|
||||
BorderRight(true).
|
||||
Render(textarea)
|
||||
|
||||
hint := base(m.getSubmitKeyText()) + muted(" send ")
|
||||
@@ -133,10 +147,10 @@ func (m *editorComponent) Content() string {
|
||||
}
|
||||
|
||||
space := m.width - 2 - lipgloss.Width(model) - lipgloss.Width(hint)
|
||||
spacer := lipgloss.NewStyle().Background(t.Background()).Width(space).Render("")
|
||||
spacer := styles.NewStyle().Background(t.Background()).Width(space).Render("")
|
||||
|
||||
info := hint + spacer + model
|
||||
info = styles.Padded().Background(t.Background()).Render(info)
|
||||
info = styles.NewStyle().Background(t.Background()).Padding(0, 1).Render(info)
|
||||
|
||||
content := strings.Join([]string{"", textarea, info}, "\n")
|
||||
return content
|
||||
@@ -149,6 +163,18 @@ func (m *editorComponent) View() string {
|
||||
return m.Content()
|
||||
}
|
||||
|
||||
func (m *editorComponent) Focused() bool {
|
||||
return m.textarea.Focused()
|
||||
}
|
||||
|
||||
func (m *editorComponent) Focus() (tea.Model, tea.Cmd) {
|
||||
return m, m.textarea.Focus()
|
||||
}
|
||||
|
||||
func (m *editorComponent) Blur() {
|
||||
m.textarea.Blur()
|
||||
}
|
||||
|
||||
func (m *editorComponent) GetSize() (width, height int) {
|
||||
return m.width, m.height
|
||||
}
|
||||
@@ -156,8 +182,6 @@ func (m *editorComponent) GetSize() (width, height int) {
|
||||
func (m *editorComponent) SetSize(width, height int) tea.Cmd {
|
||||
m.width = width
|
||||
m.height = height
|
||||
m.textarea.SetWidth(width - 5) // account for the prompt and padding right
|
||||
// m.textarea.SetHeight(height - 4)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -290,38 +314,42 @@ func createTextArea(existing *textarea.Model) textarea.Model {
|
||||
|
||||
ta := textarea.New()
|
||||
|
||||
ta.Styles.Blurred.Base = lipgloss.NewStyle().Background(bgColor).Foreground(textColor)
|
||||
ta.Styles.Blurred.CursorLine = lipgloss.NewStyle().Background(bgColor)
|
||||
ta.Styles.Blurred.Placeholder = lipgloss.NewStyle().Background(bgColor).Foreground(textMutedColor)
|
||||
ta.Styles.Blurred.Text = lipgloss.NewStyle().Background(bgColor).Foreground(textColor)
|
||||
ta.Styles.Focused.Base = lipgloss.NewStyle().Background(bgColor).Foreground(textColor)
|
||||
ta.Styles.Focused.CursorLine = lipgloss.NewStyle().Background(bgColor)
|
||||
ta.Styles.Focused.Placeholder = lipgloss.NewStyle().Background(bgColor).Foreground(textMutedColor)
|
||||
ta.Styles.Focused.Text = lipgloss.NewStyle().Background(bgColor).Foreground(textColor)
|
||||
ta.Styles.Blurred.Base = styles.NewStyle().Foreground(textColor).Background(bgColor).Lipgloss()
|
||||
ta.Styles.Blurred.CursorLine = styles.NewStyle().Background(bgColor).Lipgloss()
|
||||
ta.Styles.Blurred.Placeholder = styles.NewStyle().Foreground(textMutedColor).Background(bgColor).Lipgloss()
|
||||
ta.Styles.Blurred.Text = styles.NewStyle().Foreground(textColor).Background(bgColor).Lipgloss()
|
||||
ta.Styles.Focused.Base = styles.NewStyle().Foreground(textColor).Background(bgColor).Lipgloss()
|
||||
ta.Styles.Focused.CursorLine = styles.NewStyle().Background(bgColor).Lipgloss()
|
||||
ta.Styles.Focused.Placeholder = styles.NewStyle().Foreground(textMutedColor).Background(bgColor).Lipgloss()
|
||||
ta.Styles.Focused.Text = styles.NewStyle().Foreground(textColor).Background(bgColor).Lipgloss()
|
||||
ta.Styles.Cursor.Color = t.Primary()
|
||||
|
||||
ta.Prompt = " "
|
||||
ta.ShowLineNumbers = false
|
||||
ta.CharLimit = -1
|
||||
ta.SetWidth(layout.Current.Container.Width - 6)
|
||||
|
||||
if existing != nil {
|
||||
ta.SetValue(existing.Value())
|
||||
ta.SetWidth(existing.Width())
|
||||
// ta.SetWidth(existing.Width())
|
||||
ta.SetHeight(existing.Height())
|
||||
}
|
||||
|
||||
ta.Focus()
|
||||
// ta.Focus()
|
||||
return ta
|
||||
}
|
||||
|
||||
func createSpinner() spinner.Model {
|
||||
t := theme.CurrentTheme()
|
||||
return spinner.New(
|
||||
spinner.WithSpinner(spinner.Ellipsis),
|
||||
spinner.WithStyle(
|
||||
styles.
|
||||
Muted().
|
||||
Background(theme.CurrentTheme().Background()).
|
||||
Width(3)),
|
||||
styles.NewStyle().
|
||||
Background(t.Background()).
|
||||
Foreground(t.TextMuted()).
|
||||
Width(3).
|
||||
Lipgloss(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -129,15 +129,13 @@ func renderContentBlock(content string, options ...renderingOption) string {
|
||||
option(renderer)
|
||||
}
|
||||
|
||||
style := styles.BaseStyle().
|
||||
style := styles.NewStyle().Foreground(t.TextMuted()).Background(t.BackgroundPanel()).
|
||||
// MarginTop(renderer.marginTop).
|
||||
// MarginBottom(renderer.marginBottom).
|
||||
PaddingTop(renderer.paddingTop).
|
||||
PaddingBottom(renderer.paddingBottom).
|
||||
PaddingLeft(renderer.paddingLeft).
|
||||
PaddingRight(renderer.paddingRight).
|
||||
Background(t.BackgroundPanel()).
|
||||
Foreground(t.TextMuted()).
|
||||
BorderStyle(lipgloss.ThickBorder())
|
||||
|
||||
align := lipgloss.Left
|
||||
@@ -179,13 +177,13 @@ func renderContentBlock(content string, options ...renderingOption) string {
|
||||
layout.Current.Container.Width,
|
||||
align,
|
||||
content,
|
||||
lipgloss.WithWhitespaceStyle(lipgloss.NewStyle().Background(t.Background())),
|
||||
styles.WhitespaceStyle(t.Background()),
|
||||
)
|
||||
content = lipgloss.PlaceHorizontal(
|
||||
layout.Current.Viewport.Width,
|
||||
lipgloss.Center,
|
||||
content,
|
||||
lipgloss.WithWhitespaceStyle(lipgloss.NewStyle().Background(t.Background())),
|
||||
styles.WhitespaceStyle(t.Background()),
|
||||
)
|
||||
if renderer.marginTop > 0 {
|
||||
for range renderer.marginTop {
|
||||
@@ -226,7 +224,7 @@ func renderText(message client.MessageInfo, text string, author string) string {
|
||||
textWidth := max(lipgloss.Width(text), lipgloss.Width(info))
|
||||
markdownWidth := min(textWidth, width-padding-4) // -4 for the border and padding
|
||||
if message.Role == client.Assistant {
|
||||
markdownWidth = width - padding - 4 - 2
|
||||
markdownWidth = width - padding - 4 - 3
|
||||
}
|
||||
if message.Role == client.User {
|
||||
text = strings.ReplaceAll(text, "<", "\\<")
|
||||
@@ -275,9 +273,10 @@ func renderToolInvocation(
|
||||
}
|
||||
|
||||
t := theme.CurrentTheme()
|
||||
style := styles.Muted().
|
||||
Width(outerWidth).
|
||||
style := styles.NewStyle().
|
||||
Foreground(t.TextMuted()).
|
||||
Background(t.BackgroundPanel()).
|
||||
Width(outerWidth).
|
||||
PaddingTop(paddingTop).
|
||||
PaddingBottom(paddingBottom).
|
||||
PaddingLeft(2).
|
||||
@@ -293,7 +292,9 @@ func renderToolInvocation(
|
||||
if !showDetails {
|
||||
title = "∟ " + title
|
||||
padding := calculatePadding()
|
||||
style := lipgloss.NewStyle().Width(outerWidth - padding - 4).Background(t.BackgroundPanel())
|
||||
style := styles.NewStyle().
|
||||
Background(t.BackgroundPanel()).
|
||||
Width(outerWidth - padding - 4 - 3)
|
||||
return renderContentBlock(style.Render(title),
|
||||
WithAlign(lipgloss.Left),
|
||||
WithBorderColor(t.Accent()),
|
||||
@@ -334,9 +335,9 @@ func renderToolInvocation(
|
||||
if e, ok := metadata.Get("error"); ok && e.(bool) == true {
|
||||
if m, ok := metadata.Get("message"); ok {
|
||||
style = style.BorderLeftForeground(t.Error())
|
||||
error = styles.BaseStyle().
|
||||
Background(t.BackgroundPanel()).
|
||||
error = styles.NewStyle().
|
||||
Foreground(t.Error()).
|
||||
Background(t.BackgroundPanel()).
|
||||
Render(m.(string))
|
||||
error = renderContentBlock(
|
||||
error,
|
||||
@@ -374,7 +375,7 @@ func renderToolInvocation(
|
||||
formattedDiff, _ = diff.FormatDiff(filename, patch, diff.WithTotalWidth(diffWidth))
|
||||
}
|
||||
formattedDiff = strings.TrimSpace(formattedDiff)
|
||||
formattedDiff = lipgloss.NewStyle().
|
||||
formattedDiff = styles.NewStyle().
|
||||
BorderStyle(lipgloss.ThickBorder()).
|
||||
BorderBackground(t.Background()).
|
||||
BorderForeground(t.BackgroundPanel()).
|
||||
@@ -394,8 +395,13 @@ func renderToolInvocation(
|
||||
lipgloss.Center,
|
||||
lipgloss.Top,
|
||||
body,
|
||||
lipgloss.WithWhitespaceStyle(lipgloss.NewStyle().Background(t.Background())),
|
||||
styles.WhitespaceStyle(t.Background()),
|
||||
)
|
||||
|
||||
// Add diagnostics at the bottom if they exist
|
||||
if diagnostics := renderDiagnostics(metadata, filename); diagnostics != "" {
|
||||
body += "\n" + renderContentBlock(diagnostics, WithFullWidth(), WithBorderColor(t.Error()))
|
||||
}
|
||||
}
|
||||
}
|
||||
case "write":
|
||||
@@ -403,6 +409,11 @@ func renderToolInvocation(
|
||||
title = fmt.Sprintf("WRITE %s", relative(filename))
|
||||
if content, ok := toolArgsMap["content"].(string); ok {
|
||||
body = renderFile(filename, content)
|
||||
|
||||
// Add diagnostics at the bottom if they exist
|
||||
if diagnostics := renderDiagnostics(metadata, filename); diagnostics != "" {
|
||||
body += "\n" + renderContentBlock(diagnostics, WithFullWidth(), WithBorderColor(t.Error()))
|
||||
}
|
||||
}
|
||||
}
|
||||
case "bash":
|
||||
@@ -506,7 +517,7 @@ func renderToolInvocation(
|
||||
if !showDetails {
|
||||
title = "∟ " + title
|
||||
padding := calculatePadding()
|
||||
style := lipgloss.NewStyle().Width(outerWidth - padding - 4).Background(t.BackgroundPanel())
|
||||
style := styles.NewStyle().Background(t.BackgroundPanel()).Width(outerWidth - padding - 4 - 3)
|
||||
paddingBottom := 0
|
||||
if isLast {
|
||||
paddingBottom = 1
|
||||
@@ -530,7 +541,7 @@ func renderToolInvocation(
|
||||
layout.Current.Viewport.Width,
|
||||
lipgloss.Center,
|
||||
content,
|
||||
lipgloss.WithWhitespaceStyle(lipgloss.NewStyle().Background(t.Background())),
|
||||
styles.WhitespaceStyle(t.Background()),
|
||||
)
|
||||
if showDetails && body != "" && error == "" {
|
||||
content += "\n" + body
|
||||
@@ -684,3 +695,81 @@ func extension(path string) string {
|
||||
}
|
||||
return ext
|
||||
}
|
||||
|
||||
// Diagnostic represents an LSP diagnostic
|
||||
type Diagnostic struct {
|
||||
Range struct {
|
||||
Start struct {
|
||||
Line int `json:"line"`
|
||||
Character int `json:"character"`
|
||||
} `json:"start"`
|
||||
} `json:"range"`
|
||||
Severity int `json:"severity"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
// renderDiagnostics formats LSP diagnostics for display in the TUI
|
||||
func renderDiagnostics(metadata client.MessageMetadata_Tool_AdditionalProperties, filePath string) string {
|
||||
diagnosticsData, ok := metadata.Get("diagnostics")
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
|
||||
// diagnosticsData should be a map[string][]Diagnostic
|
||||
diagnosticsMap, ok := diagnosticsData.(map[string]interface{})
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
|
||||
fileDiagnostics, ok := diagnosticsMap[filePath]
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
|
||||
diagnosticsList, ok := fileDiagnostics.([]interface{})
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
|
||||
var errorDiagnostics []string
|
||||
for _, diagInterface := range diagnosticsList {
|
||||
diagMap, ok := diagInterface.(map[string]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
// Parse the diagnostic
|
||||
var diag Diagnostic
|
||||
diagBytes, err := json.Marshal(diagMap)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if err := json.Unmarshal(diagBytes, &diag); err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Only show error diagnostics (severity === 1)
|
||||
if diag.Severity != 1 {
|
||||
continue
|
||||
}
|
||||
|
||||
line := diag.Range.Start.Line + 1 // 1-based
|
||||
column := diag.Range.Start.Character + 1 // 1-based
|
||||
errorDiagnostics = append(errorDiagnostics, fmt.Sprintf("Error [%d:%d] %s", line, column, diag.Message))
|
||||
}
|
||||
|
||||
if len(errorDiagnostics) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
t := theme.CurrentTheme()
|
||||
var result strings.Builder
|
||||
for _, diagnostic := range errorDiagnostics {
|
||||
if result.Len() > 0 {
|
||||
result.WriteString("\n")
|
||||
}
|
||||
result.WriteString(styles.NewStyle().Foreground(t.Error()).Render(diagnostic))
|
||||
}
|
||||
|
||||
return result.String()
|
||||
}
|
||||
|
||||
@@ -245,7 +245,7 @@ func (m *messagesComponent) renderView() {
|
||||
m.width,
|
||||
lipgloss.Center,
|
||||
block,
|
||||
lipgloss.WithWhitespaceStyle(lipgloss.NewStyle().Background(t.Background())),
|
||||
styles.WhitespaceStyle(t.Background()),
|
||||
))
|
||||
}
|
||||
|
||||
@@ -260,8 +260,8 @@ func (m *messagesComponent) header() string {
|
||||
|
||||
t := theme.CurrentTheme()
|
||||
width := layout.Current.Container.Width
|
||||
base := styles.BaseStyle().Background(t.Background()).Render
|
||||
muted := styles.Muted().Background(t.Background()).Render
|
||||
base := styles.NewStyle().Foreground(t.Text()).Background(t.Background()).Render
|
||||
muted := styles.NewStyle().Foreground(t.TextMuted()).Background(t.Background()).Render
|
||||
headerLines := []string{}
|
||||
headerLines = append(headerLines, toMarkdown("# "+m.app.Session.Title, width-6, t.Background()))
|
||||
if m.app.Session.Share != nil && m.app.Session.Share.Url != "" {
|
||||
@@ -271,11 +271,11 @@ func (m *messagesComponent) header() string {
|
||||
}
|
||||
header := strings.Join(headerLines, "\n")
|
||||
|
||||
header = styles.BaseStyle().
|
||||
header = styles.NewStyle().
|
||||
Background(t.Background()).
|
||||
Width(width).
|
||||
PaddingLeft(2).
|
||||
PaddingRight(2).
|
||||
Background(t.Background()).
|
||||
BorderLeft(true).
|
||||
BorderRight(true).
|
||||
BorderBackground(t.Background()).
|
||||
@@ -306,7 +306,7 @@ func (m *messagesComponent) View() string {
|
||||
m.width,
|
||||
lipgloss.Center,
|
||||
m.header(),
|
||||
lipgloss.WithWhitespaceStyle(lipgloss.NewStyle().Background(t.Background())),
|
||||
styles.WhitespaceStyle(t.Background()),
|
||||
),
|
||||
m.viewport.View(),
|
||||
)
|
||||
@@ -314,9 +314,9 @@ func (m *messagesComponent) View() string {
|
||||
|
||||
func (m *messagesComponent) home() string {
|
||||
t := theme.CurrentTheme()
|
||||
baseStyle := styles.BaseStyle().Background(t.Background())
|
||||
baseStyle := styles.NewStyle().Background(t.Background())
|
||||
base := baseStyle.Render
|
||||
muted := styles.Muted().Background(t.Background()).Render
|
||||
muted := styles.NewStyle().Foreground(t.TextMuted()).Background(t.Background()).Render
|
||||
|
||||
open := `
|
||||
█▀▀█ █▀▀█ █▀▀ █▀▀▄
|
||||
@@ -335,9 +335,9 @@ func (m *messagesComponent) home() string {
|
||||
// cwd := app.Info.Path.Cwd
|
||||
// config := app.Info.Path.Config
|
||||
|
||||
versionStyle := lipgloss.NewStyle().
|
||||
Background(t.Background()).
|
||||
versionStyle := styles.NewStyle().
|
||||
Foreground(t.TextMuted()).
|
||||
Background(t.Background()).
|
||||
Width(lipgloss.Width(logo)).
|
||||
Align(lipgloss.Right)
|
||||
version := versionStyle.Render(m.app.Version)
|
||||
@@ -347,14 +347,14 @@ func (m *messagesComponent) home() string {
|
||||
m.width,
|
||||
lipgloss.Center,
|
||||
logoAndVersion,
|
||||
lipgloss.WithWhitespaceStyle(lipgloss.NewStyle().Background(t.Background())),
|
||||
styles.WhitespaceStyle(t.Background()),
|
||||
)
|
||||
m.commands.SetBackgroundColor(t.Background())
|
||||
commands := lipgloss.PlaceHorizontal(
|
||||
m.width,
|
||||
lipgloss.Center,
|
||||
m.commands.View(),
|
||||
lipgloss.WithWhitespaceStyle(lipgloss.NewStyle().Background(t.Background())),
|
||||
styles.WhitespaceStyle(t.Background()),
|
||||
)
|
||||
|
||||
lines := []string{}
|
||||
@@ -372,7 +372,7 @@ func (m *messagesComponent) home() string {
|
||||
lipgloss.Center,
|
||||
lipgloss.Center,
|
||||
baseStyle.Render(strings.Join(lines, "\n")),
|
||||
lipgloss.WithWhitespaceStyle(lipgloss.NewStyle().Background(t.Background())),
|
||||
styles.WhitespaceStyle(t.Background()),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -60,15 +60,9 @@ func (c *commandsComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
func (c *commandsComponent) View() string {
|
||||
t := theme.CurrentTheme()
|
||||
|
||||
triggerStyle := lipgloss.NewStyle().
|
||||
Foreground(t.Primary()).
|
||||
Bold(true)
|
||||
|
||||
descriptionStyle := lipgloss.NewStyle().
|
||||
Foreground(t.Text())
|
||||
|
||||
keybindStyle := lipgloss.NewStyle().
|
||||
Foreground(t.TextMuted())
|
||||
triggerStyle := styles.NewStyle().Foreground(t.Primary()).Bold(true)
|
||||
descriptionStyle := styles.NewStyle().Foreground(t.Text())
|
||||
keybindStyle := styles.NewStyle().Foreground(t.TextMuted())
|
||||
|
||||
if c.background != nil {
|
||||
triggerStyle = triggerStyle.Background(*c.background)
|
||||
@@ -99,10 +93,11 @@ func (c *commandsComponent) View() string {
|
||||
}
|
||||
|
||||
if len(commandsToShow) == 0 {
|
||||
muted := styles.NewStyle().Foreground(theme.CurrentTheme().TextMuted())
|
||||
if c.showAll {
|
||||
return styles.Muted().Render("No commands available")
|
||||
return muted.Render("No commands available")
|
||||
}
|
||||
return styles.Muted().Render("No commands with triggers available")
|
||||
return muted.Render("No commands with triggers available")
|
||||
}
|
||||
|
||||
// Calculate column widths
|
||||
@@ -188,7 +183,7 @@ func (c *commandsComponent) View() string {
|
||||
// Remove trailing newline
|
||||
result := strings.TrimSuffix(output.String(), "\n")
|
||||
if c.background != nil {
|
||||
result = lipgloss.NewStyle().Background(c.background).Width(maxWidth).Render(result)
|
||||
result = styles.NewStyle().Background(*c.background).Width(maxWidth).Render(result)
|
||||
}
|
||||
|
||||
return result
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"github.com/charmbracelet/bubbles/v2/key"
|
||||
"github.com/charmbracelet/bubbles/v2/textarea"
|
||||
tea "github.com/charmbracelet/bubbletea/v2"
|
||||
"github.com/charmbracelet/lipgloss/v2"
|
||||
"github.com/sst/opencode/internal/app"
|
||||
"github.com/sst/opencode/internal/components/list"
|
||||
"github.com/sst/opencode/internal/styles"
|
||||
@@ -26,7 +27,7 @@ type CompletionItemI interface {
|
||||
|
||||
func (ci *CompletionItem) Render(selected bool, width int) string {
|
||||
t := theme.CurrentTheme()
|
||||
baseStyle := styles.BaseStyle()
|
||||
baseStyle := styles.NewStyle().Foreground(t.Text())
|
||||
|
||||
itemStyle := baseStyle.
|
||||
Background(t.BackgroundElement()).
|
||||
@@ -34,8 +35,7 @@ func (ci *CompletionItem) Render(selected bool, width int) string {
|
||||
Padding(0, 1)
|
||||
|
||||
if selected {
|
||||
itemStyle = itemStyle.
|
||||
Foreground(t.Primary())
|
||||
itemStyle = itemStyle.Foreground(t.Primary())
|
||||
}
|
||||
|
||||
title := itemStyle.Render(
|
||||
@@ -185,7 +185,7 @@ func (c *completionDialogComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
|
||||
func (c *completionDialogComponent) View() string {
|
||||
t := theme.CurrentTheme()
|
||||
baseStyle := styles.BaseStyle()
|
||||
baseStyle := styles.NewStyle().Foreground(t.Text())
|
||||
|
||||
maxWidth := 40
|
||||
completions := c.list.GetItems()
|
||||
@@ -199,8 +199,14 @@ func (c *completionDialogComponent) View() string {
|
||||
|
||||
c.list.SetMaxWidth(maxWidth)
|
||||
|
||||
return baseStyle.Padding(0, 0).
|
||||
return baseStyle.
|
||||
Padding(0, 0).
|
||||
Background(t.BackgroundElement()).
|
||||
BorderStyle(lipgloss.ThickBorder()).
|
||||
BorderLeft(true).
|
||||
BorderRight(true).
|
||||
BorderForeground(t.Border()).
|
||||
BorderBackground(t.Background()).
|
||||
Width(c.width).
|
||||
Render(c.list.View())
|
||||
}
|
||||
|
||||
@@ -94,7 +94,7 @@ func (m InitDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
// View implements tea.Model.
|
||||
func (m InitDialogCmp) View() string {
|
||||
t := theme.CurrentTheme()
|
||||
baseStyle := styles.BaseStyle()
|
||||
baseStyle := styles.NewStyle().Foreground(t.Text())
|
||||
|
||||
// Calculate width needed for content
|
||||
maxWidth := 60 // Width for explanation text
|
||||
|
||||
@@ -158,7 +158,7 @@ func (m *modelDialog) getScrollIndicators(maxWidth int) string {
|
||||
}
|
||||
|
||||
t := theme.CurrentTheme()
|
||||
return styles.BaseStyle().
|
||||
return styles.NewStyle().
|
||||
Foreground(t.TextMuted()).
|
||||
Width(maxWidth).
|
||||
Align(lipgloss.Right).
|
||||
|
||||
@@ -145,7 +145,7 @@ func (p *permissionDialogComponent) selectCurrentOption() tea.Cmd {
|
||||
|
||||
func (p *permissionDialogComponent) renderButtons() string {
|
||||
t := theme.CurrentTheme()
|
||||
baseStyle := styles.BaseStyle()
|
||||
baseStyle := styles.NewStyle().Foreground(t.Text())
|
||||
|
||||
allowStyle := baseStyle
|
||||
allowSessionStyle := baseStyle
|
||||
@@ -355,8 +355,7 @@ func (p *permissionDialogComponent) renderDefaultContent() string {
|
||||
|
||||
func (p *permissionDialogComponent) styleViewport() string {
|
||||
t := theme.CurrentTheme()
|
||||
contentStyle := lipgloss.NewStyle().
|
||||
Background(t.Background())
|
||||
contentStyle := styles.NewStyle().Background(t.Background())
|
||||
|
||||
return contentStyle.Render(p.contentViewPort.View())
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ import (
|
||||
"slices"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea/v2"
|
||||
"github.com/charmbracelet/lipgloss/v2"
|
||||
"github.com/muesli/reflow/truncate"
|
||||
"github.com/sst/opencode/internal/app"
|
||||
"github.com/sst/opencode/internal/components/list"
|
||||
@@ -33,7 +32,7 @@ type sessionItem struct {
|
||||
|
||||
func (s sessionItem) Render(selected bool, width int) string {
|
||||
t := theme.CurrentTheme()
|
||||
baseStyle := styles.BaseStyle()
|
||||
baseStyle := styles.NewStyle()
|
||||
|
||||
var text string
|
||||
if s.isDeleteConfirming {
|
||||
@@ -44,20 +43,20 @@ func (s sessionItem) Render(selected bool, width int) string {
|
||||
|
||||
truncatedStr := truncate.StringWithTail(text, uint(width-1), "...")
|
||||
|
||||
var itemStyle lipgloss.Style
|
||||
var itemStyle styles.Style
|
||||
if selected {
|
||||
if s.isDeleteConfirming {
|
||||
// Red background for delete confirmation
|
||||
itemStyle = baseStyle.
|
||||
Background(t.Error()).
|
||||
Foreground(t.Background()).
|
||||
Foreground(t.BackgroundElement()).
|
||||
Width(width).
|
||||
PaddingLeft(1)
|
||||
} else {
|
||||
// Normal selection
|
||||
itemStyle = baseStyle.
|
||||
Background(t.Primary()).
|
||||
Foreground(t.Background()).
|
||||
Foreground(t.BackgroundElement()).
|
||||
Width(width).
|
||||
PaddingLeft(1)
|
||||
}
|
||||
@@ -151,9 +150,9 @@ func (s *sessionDialog) Render(background string) string {
|
||||
listView := s.list.View()
|
||||
|
||||
t := theme.CurrentTheme()
|
||||
helpStyle := styles.BaseStyle().PaddingLeft(1).PaddingTop(1)
|
||||
helpText := styles.BaseStyle().Foreground(t.Text()).Render("x/del")
|
||||
helpText = helpText + styles.BaseStyle().Background(t.BackgroundElement()).Foreground(t.TextMuted()).Render(" delete session")
|
||||
helpStyle := styles.NewStyle().PaddingLeft(1).PaddingTop(1)
|
||||
helpText := styles.NewStyle().Foreground(t.Text()).Render("x/del")
|
||||
helpText = helpText + styles.NewStyle().Background(t.BackgroundElement()).Foreground(t.TextMuted()).Render(" delete session")
|
||||
helpText = helpStyle.Render(helpText)
|
||||
|
||||
content := strings.Join([]string{listView, helpText}, "\n")
|
||||
|
||||
@@ -103,7 +103,7 @@ func NewThemeDialog() ThemeDialog {
|
||||
|
||||
// Set the initial selection to the current theme
|
||||
list.SetSelectedIndex(selectedIdx)
|
||||
|
||||
|
||||
// Set the max width for the list to match the modal width
|
||||
list.SetMaxWidth(36) // 40 (modal max width) - 4 (modal padding)
|
||||
|
||||
|
||||
@@ -441,84 +441,84 @@ func SyntaxHighlight(w io.Writer, source, fileName, formatter string, bg color.C
|
||||
<entry type="TextWhitespace" style="%s"/>
|
||||
</style>
|
||||
`,
|
||||
getColor(t.BackgroundPanel()), // Background
|
||||
getColor(t.Text()), // Text
|
||||
getColor(t.Text()), // Other
|
||||
getColor(t.Error()), // Error
|
||||
getChromaColor(t.BackgroundPanel()), // Background
|
||||
getChromaColor(t.Text()), // Text
|
||||
getChromaColor(t.Text()), // Other
|
||||
getChromaColor(t.Error()), // Error
|
||||
|
||||
getColor(t.SyntaxKeyword()), // Keyword
|
||||
getColor(t.SyntaxKeyword()), // KeywordConstant
|
||||
getColor(t.SyntaxKeyword()), // KeywordDeclaration
|
||||
getColor(t.SyntaxKeyword()), // KeywordNamespace
|
||||
getColor(t.SyntaxKeyword()), // KeywordPseudo
|
||||
getColor(t.SyntaxKeyword()), // KeywordReserved
|
||||
getColor(t.SyntaxType()), // KeywordType
|
||||
getChromaColor(t.SyntaxKeyword()), // Keyword
|
||||
getChromaColor(t.SyntaxKeyword()), // KeywordConstant
|
||||
getChromaColor(t.SyntaxKeyword()), // KeywordDeclaration
|
||||
getChromaColor(t.SyntaxKeyword()), // KeywordNamespace
|
||||
getChromaColor(t.SyntaxKeyword()), // KeywordPseudo
|
||||
getChromaColor(t.SyntaxKeyword()), // KeywordReserved
|
||||
getChromaColor(t.SyntaxType()), // KeywordType
|
||||
|
||||
getColor(t.Text()), // Name
|
||||
getColor(t.SyntaxVariable()), // NameAttribute
|
||||
getColor(t.SyntaxType()), // NameBuiltin
|
||||
getColor(t.SyntaxVariable()), // NameBuiltinPseudo
|
||||
getColor(t.SyntaxType()), // NameClass
|
||||
getColor(t.SyntaxVariable()), // NameConstant
|
||||
getColor(t.SyntaxFunction()), // NameDecorator
|
||||
getColor(t.SyntaxVariable()), // NameEntity
|
||||
getColor(t.SyntaxType()), // NameException
|
||||
getColor(t.SyntaxFunction()), // NameFunction
|
||||
getColor(t.Text()), // NameLabel
|
||||
getColor(t.SyntaxType()), // NameNamespace
|
||||
getColor(t.SyntaxVariable()), // NameOther
|
||||
getColor(t.SyntaxKeyword()), // NameTag
|
||||
getColor(t.SyntaxVariable()), // NameVariable
|
||||
getColor(t.SyntaxVariable()), // NameVariableClass
|
||||
getColor(t.SyntaxVariable()), // NameVariableGlobal
|
||||
getColor(t.SyntaxVariable()), // NameVariableInstance
|
||||
getChromaColor(t.Text()), // Name
|
||||
getChromaColor(t.SyntaxVariable()), // NameAttribute
|
||||
getChromaColor(t.SyntaxType()), // NameBuiltin
|
||||
getChromaColor(t.SyntaxVariable()), // NameBuiltinPseudo
|
||||
getChromaColor(t.SyntaxType()), // NameClass
|
||||
getChromaColor(t.SyntaxVariable()), // NameConstant
|
||||
getChromaColor(t.SyntaxFunction()), // NameDecorator
|
||||
getChromaColor(t.SyntaxVariable()), // NameEntity
|
||||
getChromaColor(t.SyntaxType()), // NameException
|
||||
getChromaColor(t.SyntaxFunction()), // NameFunction
|
||||
getChromaColor(t.Text()), // NameLabel
|
||||
getChromaColor(t.SyntaxType()), // NameNamespace
|
||||
getChromaColor(t.SyntaxVariable()), // NameOther
|
||||
getChromaColor(t.SyntaxKeyword()), // NameTag
|
||||
getChromaColor(t.SyntaxVariable()), // NameVariable
|
||||
getChromaColor(t.SyntaxVariable()), // NameVariableClass
|
||||
getChromaColor(t.SyntaxVariable()), // NameVariableGlobal
|
||||
getChromaColor(t.SyntaxVariable()), // NameVariableInstance
|
||||
|
||||
getColor(t.SyntaxString()), // Literal
|
||||
getColor(t.SyntaxString()), // LiteralDate
|
||||
getColor(t.SyntaxString()), // LiteralString
|
||||
getColor(t.SyntaxString()), // LiteralStringBacktick
|
||||
getColor(t.SyntaxString()), // LiteralStringChar
|
||||
getColor(t.SyntaxString()), // LiteralStringDoc
|
||||
getColor(t.SyntaxString()), // LiteralStringDouble
|
||||
getColor(t.SyntaxString()), // LiteralStringEscape
|
||||
getColor(t.SyntaxString()), // LiteralStringHeredoc
|
||||
getColor(t.SyntaxString()), // LiteralStringInterpol
|
||||
getColor(t.SyntaxString()), // LiteralStringOther
|
||||
getColor(t.SyntaxString()), // LiteralStringRegex
|
||||
getColor(t.SyntaxString()), // LiteralStringSingle
|
||||
getColor(t.SyntaxString()), // LiteralStringSymbol
|
||||
getChromaColor(t.SyntaxString()), // Literal
|
||||
getChromaColor(t.SyntaxString()), // LiteralDate
|
||||
getChromaColor(t.SyntaxString()), // LiteralString
|
||||
getChromaColor(t.SyntaxString()), // LiteralStringBacktick
|
||||
getChromaColor(t.SyntaxString()), // LiteralStringChar
|
||||
getChromaColor(t.SyntaxString()), // LiteralStringDoc
|
||||
getChromaColor(t.SyntaxString()), // LiteralStringDouble
|
||||
getChromaColor(t.SyntaxString()), // LiteralStringEscape
|
||||
getChromaColor(t.SyntaxString()), // LiteralStringHeredoc
|
||||
getChromaColor(t.SyntaxString()), // LiteralStringInterpol
|
||||
getChromaColor(t.SyntaxString()), // LiteralStringOther
|
||||
getChromaColor(t.SyntaxString()), // LiteralStringRegex
|
||||
getChromaColor(t.SyntaxString()), // LiteralStringSingle
|
||||
getChromaColor(t.SyntaxString()), // LiteralStringSymbol
|
||||
|
||||
getColor(t.SyntaxNumber()), // LiteralNumber
|
||||
getColor(t.SyntaxNumber()), // LiteralNumberBin
|
||||
getColor(t.SyntaxNumber()), // LiteralNumberFloat
|
||||
getColor(t.SyntaxNumber()), // LiteralNumberHex
|
||||
getColor(t.SyntaxNumber()), // LiteralNumberInteger
|
||||
getColor(t.SyntaxNumber()), // LiteralNumberIntegerLong
|
||||
getColor(t.SyntaxNumber()), // LiteralNumberOct
|
||||
getChromaColor(t.SyntaxNumber()), // LiteralNumber
|
||||
getChromaColor(t.SyntaxNumber()), // LiteralNumberBin
|
||||
getChromaColor(t.SyntaxNumber()), // LiteralNumberFloat
|
||||
getChromaColor(t.SyntaxNumber()), // LiteralNumberHex
|
||||
getChromaColor(t.SyntaxNumber()), // LiteralNumberInteger
|
||||
getChromaColor(t.SyntaxNumber()), // LiteralNumberIntegerLong
|
||||
getChromaColor(t.SyntaxNumber()), // LiteralNumberOct
|
||||
|
||||
getColor(t.SyntaxOperator()), // Operator
|
||||
getColor(t.SyntaxKeyword()), // OperatorWord
|
||||
getColor(t.SyntaxPunctuation()), // Punctuation
|
||||
getChromaColor(t.SyntaxOperator()), // Operator
|
||||
getChromaColor(t.SyntaxKeyword()), // OperatorWord
|
||||
getChromaColor(t.SyntaxPunctuation()), // Punctuation
|
||||
|
||||
getColor(t.SyntaxComment()), // Comment
|
||||
getColor(t.SyntaxComment()), // CommentHashbang
|
||||
getColor(t.SyntaxComment()), // CommentMultiline
|
||||
getColor(t.SyntaxComment()), // CommentSingle
|
||||
getColor(t.SyntaxComment()), // CommentSpecial
|
||||
getColor(t.SyntaxKeyword()), // CommentPreproc
|
||||
getChromaColor(t.SyntaxComment()), // Comment
|
||||
getChromaColor(t.SyntaxComment()), // CommentHashbang
|
||||
getChromaColor(t.SyntaxComment()), // CommentMultiline
|
||||
getChromaColor(t.SyntaxComment()), // CommentSingle
|
||||
getChromaColor(t.SyntaxComment()), // CommentSpecial
|
||||
getChromaColor(t.SyntaxKeyword()), // CommentPreproc
|
||||
|
||||
getColor(t.Text()), // Generic
|
||||
getColor(t.Error()), // GenericDeleted
|
||||
getColor(t.Text()), // GenericEmph
|
||||
getColor(t.Error()), // GenericError
|
||||
getColor(t.Text()), // GenericHeading
|
||||
getColor(t.Success()), // GenericInserted
|
||||
getColor(t.TextMuted()), // GenericOutput
|
||||
getColor(t.Text()), // GenericPrompt
|
||||
getColor(t.Text()), // GenericStrong
|
||||
getColor(t.Text()), // GenericSubheading
|
||||
getColor(t.Error()), // GenericTraceback
|
||||
getColor(t.Text()), // TextWhitespace
|
||||
getChromaColor(t.Text()), // Generic
|
||||
getChromaColor(t.Error()), // GenericDeleted
|
||||
getChromaColor(t.Text()), // GenericEmph
|
||||
getChromaColor(t.Error()), // GenericError
|
||||
getChromaColor(t.Text()), // GenericHeading
|
||||
getChromaColor(t.Success()), // GenericInserted
|
||||
getChromaColor(t.TextMuted()), // GenericOutput
|
||||
getChromaColor(t.Text()), // GenericPrompt
|
||||
getChromaColor(t.Text()), // GenericStrong
|
||||
getChromaColor(t.Text()), // GenericSubheading
|
||||
getChromaColor(t.Error()), // GenericTraceback
|
||||
getChromaColor(t.Text()), // TextWhitespace
|
||||
)
|
||||
|
||||
r := strings.NewReader(syntaxThemeXml)
|
||||
@@ -527,6 +527,9 @@ func SyntaxHighlight(w io.Writer, source, fileName, formatter string, bg color.C
|
||||
// Modify the style to use the provided background
|
||||
s, err := style.Builder().Transform(
|
||||
func(t chroma.StyleEntry) chroma.StyleEntry {
|
||||
if _, ok := bg.(lipgloss.NoColor); ok {
|
||||
return t
|
||||
}
|
||||
r, g, b, _ := bg.RGBA()
|
||||
t.Background = chroma.NewColour(uint8(r>>8), uint8(g>>8), uint8(b>>8))
|
||||
return t
|
||||
@@ -546,10 +549,18 @@ func SyntaxHighlight(w io.Writer, source, fileName, formatter string, bg color.C
|
||||
}
|
||||
|
||||
// getColor returns the appropriate hex color string based on terminal background
|
||||
func getColor(adaptiveColor compat.AdaptiveColor) string {
|
||||
func getColor(adaptiveColor compat.AdaptiveColor) *string {
|
||||
return stylesi.AdaptiveColorToString(adaptiveColor)
|
||||
}
|
||||
|
||||
func getChromaColor(adaptiveColor compat.AdaptiveColor) string {
|
||||
color := stylesi.AdaptiveColorToString(adaptiveColor)
|
||||
if color == nil {
|
||||
return ""
|
||||
}
|
||||
return *color
|
||||
}
|
||||
|
||||
// highlightLine applies syntax highlighting to a single line
|
||||
func highlightLine(fileName string, line string, bg color.Color) string {
|
||||
var buf bytes.Buffer
|
||||
@@ -561,11 +572,11 @@ func highlightLine(fileName string, line string, bg color.Color) string {
|
||||
}
|
||||
|
||||
// createStyles generates the lipgloss styles needed for rendering diffs
|
||||
func createStyles(t theme.Theme) (removedLineStyle, addedLineStyle, contextLineStyle, lineNumberStyle lipgloss.Style) {
|
||||
removedLineStyle = lipgloss.NewStyle().Background(t.DiffRemovedBg())
|
||||
addedLineStyle = lipgloss.NewStyle().Background(t.DiffAddedBg())
|
||||
contextLineStyle = lipgloss.NewStyle().Background(t.DiffContextBg())
|
||||
lineNumberStyle = lipgloss.NewStyle().Background(t.DiffLineNumber()).Foreground(t.TextMuted())
|
||||
func createStyles(t theme.Theme) (removedLineStyle, addedLineStyle, contextLineStyle, lineNumberStyle stylesi.Style) {
|
||||
removedLineStyle = stylesi.NewStyle().Background(t.DiffRemovedBg())
|
||||
addedLineStyle = stylesi.NewStyle().Background(t.DiffAddedBg())
|
||||
contextLineStyle = stylesi.NewStyle().Background(t.DiffContextBg())
|
||||
lineNumberStyle = stylesi.NewStyle().Foreground(t.TextMuted()).Background(t.DiffLineNumber())
|
||||
return
|
||||
}
|
||||
|
||||
@@ -613,9 +624,17 @@ func applyHighlighting(content string, segments []Segment, segmentType LineType,
|
||||
currentPos := 0
|
||||
|
||||
// Get the appropriate color based on terminal background
|
||||
bgColor := lipgloss.Color(getColor(highlightBg))
|
||||
fgColor := lipgloss.Color(getColor(theme.CurrentTheme().BackgroundPanel()))
|
||||
bg := getColor(highlightBg)
|
||||
fg := getColor(theme.CurrentTheme().BackgroundPanel())
|
||||
var bgColor color.Color
|
||||
var fgColor color.Color
|
||||
|
||||
if bg != nil {
|
||||
bgColor = lipgloss.Color(*bg)
|
||||
}
|
||||
if fg != nil {
|
||||
fgColor = lipgloss.Color(*fg)
|
||||
}
|
||||
for i := 0; i < len(content); {
|
||||
// Check if we're at an ANSI sequence
|
||||
isAnsi := false
|
||||
@@ -651,12 +670,20 @@ func applyHighlighting(content string, segments []Segment, segmentType LineType,
|
||||
currentStyle := ansiSequences[currentPos]
|
||||
|
||||
// Apply foreground and background highlight
|
||||
sb.WriteString("\x1b[38;2;")
|
||||
r, g, b, _ := fgColor.RGBA()
|
||||
sb.WriteString(fmt.Sprintf("%d;%d;%dm", r>>8, g>>8, b>>8))
|
||||
sb.WriteString("\x1b[48;2;")
|
||||
r, g, b, _ = bgColor.RGBA()
|
||||
sb.WriteString(fmt.Sprintf("%d;%d;%dm", r>>8, g>>8, b>>8))
|
||||
if fgColor != nil {
|
||||
sb.WriteString("\x1b[38;2;")
|
||||
r, g, b, _ := fgColor.RGBA()
|
||||
sb.WriteString(fmt.Sprintf("%d;%d;%dm", r>>8, g>>8, b>>8))
|
||||
} else {
|
||||
sb.WriteString("\x1b[49m")
|
||||
}
|
||||
if bgColor != nil {
|
||||
sb.WriteString("\x1b[48;2;")
|
||||
r, g, b, _ := bgColor.RGBA()
|
||||
sb.WriteString(fmt.Sprintf("%d;%d;%dm", r>>8, g>>8, b>>8))
|
||||
} else {
|
||||
sb.WriteString("\x1b[39m")
|
||||
}
|
||||
sb.WriteString(char)
|
||||
|
||||
// Full reset of all attributes to ensure clean state
|
||||
@@ -677,16 +704,16 @@ func applyHighlighting(content string, segments []Segment, segmentType LineType,
|
||||
}
|
||||
|
||||
// renderLinePrefix renders the line number and marker prefix for a diff line
|
||||
func renderLinePrefix(dl DiffLine, lineNum string, marker string, lineNumberStyle lipgloss.Style, t theme.Theme) string {
|
||||
func renderLinePrefix(dl DiffLine, lineNum string, marker string, lineNumberStyle stylesi.Style, t theme.Theme) string {
|
||||
// Style the marker based on line type
|
||||
var styledMarker string
|
||||
switch dl.Kind {
|
||||
case LineRemoved:
|
||||
styledMarker = lipgloss.NewStyle().Background(t.DiffRemovedBg()).Foreground(t.DiffRemoved()).Render(marker)
|
||||
styledMarker = stylesi.NewStyle().Foreground(t.DiffRemoved()).Background(t.DiffRemovedBg()).Render(marker)
|
||||
case LineAdded:
|
||||
styledMarker = lipgloss.NewStyle().Background(t.DiffAddedBg()).Foreground(t.DiffAdded()).Render(marker)
|
||||
styledMarker = stylesi.NewStyle().Foreground(t.DiffAdded()).Background(t.DiffAddedBg()).Render(marker)
|
||||
case LineContext:
|
||||
styledMarker = lipgloss.NewStyle().Background(t.DiffContextBg()).Foreground(t.TextMuted()).Render(marker)
|
||||
styledMarker = stylesi.NewStyle().Foreground(t.TextMuted()).Background(t.DiffContextBg()).Render(marker)
|
||||
default:
|
||||
styledMarker = marker
|
||||
}
|
||||
@@ -695,7 +722,7 @@ func renderLinePrefix(dl DiffLine, lineNum string, marker string, lineNumberStyl
|
||||
}
|
||||
|
||||
// renderLineContent renders the content of a diff line with syntax and intra-line highlighting
|
||||
func renderLineContent(fileName string, dl DiffLine, bgStyle lipgloss.Style, highlightColor compat.AdaptiveColor, width int, t theme.Theme) string {
|
||||
func renderLineContent(fileName string, dl DiffLine, bgStyle stylesi.Style, highlightColor compat.AdaptiveColor, width int) string {
|
||||
// Apply syntax highlighting
|
||||
content := highlightLine(fileName, dl.Content, bgStyle.GetBackground())
|
||||
|
||||
@@ -714,7 +741,9 @@ func renderLineContent(fileName string, dl DiffLine, bgStyle lipgloss.Style, hig
|
||||
ansi.Truncate(
|
||||
content,
|
||||
width,
|
||||
lipgloss.NewStyle().Background(bgStyle.GetBackground()).Foreground(t.TextMuted()).Render("..."),
|
||||
"...",
|
||||
// stylesi.NewStyleWithColors(t.TextMuted(), bgStyle.GetBackground()).Render("..."),
|
||||
// stylesi.WithForeground(stylesi.NewStyle().Background(bgStyle.GetBackground()), t.TextMuted()).Render("..."),
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -725,7 +754,7 @@ func renderUnifiedLine(fileName string, dl DiffLine, width int, t theme.Theme) s
|
||||
|
||||
// Determine line style and marker based on line type
|
||||
var marker string
|
||||
var bgStyle lipgloss.Style
|
||||
var bgStyle stylesi.Style
|
||||
var lineNum string
|
||||
var highlightColor compat.AdaptiveColor
|
||||
|
||||
@@ -733,8 +762,8 @@ func renderUnifiedLine(fileName string, dl DiffLine, width int, t theme.Theme) s
|
||||
case LineRemoved:
|
||||
marker = "-"
|
||||
bgStyle = removedLineStyle
|
||||
lineNumberStyle = lineNumberStyle.Foreground(t.DiffRemoved()).Background(t.DiffRemovedLineNumberBg())
|
||||
highlightColor = t.DiffHighlightRemoved()
|
||||
lineNumberStyle = lineNumberStyle.Background(t.DiffRemovedLineNumberBg()).Foreground(t.DiffRemoved())
|
||||
highlightColor = t.DiffHighlightRemoved() // TODO: handle "none"
|
||||
if dl.OldLineNo > 0 {
|
||||
lineNum = fmt.Sprintf("%6d ", dl.OldLineNo)
|
||||
} else {
|
||||
@@ -743,8 +772,8 @@ func renderUnifiedLine(fileName string, dl DiffLine, width int, t theme.Theme) s
|
||||
case LineAdded:
|
||||
marker = "+"
|
||||
bgStyle = addedLineStyle
|
||||
lineNumberStyle = lineNumberStyle.Foreground(t.DiffAdded()).Background(t.DiffAddedLineNumberBg())
|
||||
highlightColor = t.DiffHighlightAdded()
|
||||
lineNumberStyle = lineNumberStyle.Background(t.DiffAddedLineNumberBg()).Foreground(t.DiffAdded())
|
||||
highlightColor = t.DiffHighlightAdded() // TODO: handle "none"
|
||||
if dl.NewLineNo > 0 {
|
||||
lineNum = fmt.Sprintf(" %7d", dl.NewLineNo)
|
||||
} else {
|
||||
@@ -766,7 +795,7 @@ func renderUnifiedLine(fileName string, dl DiffLine, width int, t theme.Theme) s
|
||||
// Render the content
|
||||
prefixWidth := ansi.StringWidth(prefix)
|
||||
contentWidth := width - prefixWidth
|
||||
content := renderLineContent(fileName, dl, bgStyle, highlightColor, contentWidth, t)
|
||||
content := renderLineContent(fileName, dl, bgStyle, highlightColor, contentWidth)
|
||||
|
||||
return prefix + content
|
||||
}
|
||||
@@ -780,7 +809,7 @@ func renderDiffColumnLine(
|
||||
t theme.Theme,
|
||||
) string {
|
||||
if dl == nil {
|
||||
contextLineStyle := lipgloss.NewStyle().Background(t.DiffContextBg())
|
||||
contextLineStyle := stylesi.NewStyle().Background(t.DiffContextBg())
|
||||
return contextLineStyle.Width(colWidth).Render("")
|
||||
}
|
||||
|
||||
@@ -788,7 +817,7 @@ func renderDiffColumnLine(
|
||||
|
||||
// Determine line style based on line type and column
|
||||
var marker string
|
||||
var bgStyle lipgloss.Style
|
||||
var bgStyle stylesi.Style
|
||||
var lineNum string
|
||||
var highlightColor compat.AdaptiveColor
|
||||
|
||||
@@ -798,8 +827,8 @@ func renderDiffColumnLine(
|
||||
case LineRemoved:
|
||||
marker = "-"
|
||||
bgStyle = removedLineStyle
|
||||
lineNumberStyle = lineNumberStyle.Foreground(t.DiffRemoved()).Background(t.DiffRemovedLineNumberBg())
|
||||
highlightColor = t.DiffHighlightRemoved()
|
||||
lineNumberStyle = lineNumberStyle.Background(t.DiffRemovedLineNumberBg()).Foreground(t.DiffRemoved())
|
||||
highlightColor = t.DiffHighlightRemoved() // TODO: handle "none"
|
||||
case LineAdded:
|
||||
marker = "?"
|
||||
bgStyle = contextLineStyle
|
||||
@@ -818,7 +847,7 @@ func renderDiffColumnLine(
|
||||
case LineAdded:
|
||||
marker = "+"
|
||||
bgStyle = addedLineStyle
|
||||
lineNumberStyle = lineNumberStyle.Foreground(t.DiffAdded()).Background(t.DiffAddedLineNumberBg())
|
||||
lineNumberStyle = lineNumberStyle.Background(t.DiffAddedLineNumberBg()).Foreground(t.DiffAdded())
|
||||
highlightColor = t.DiffHighlightAdded()
|
||||
case LineRemoved:
|
||||
marker = "?"
|
||||
@@ -849,7 +878,7 @@ func renderDiffColumnLine(
|
||||
// Render the content
|
||||
prefixWidth := ansi.StringWidth(prefix)
|
||||
contentWidth := colWidth - prefixWidth
|
||||
content := renderLineContent(fileName, *dl, bgStyle, highlightColor, contentWidth, t)
|
||||
content := renderLineContent(fileName, *dl, bgStyle, highlightColor, contentWidth)
|
||||
|
||||
return prefix + content
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ import (
|
||||
|
||||
"github.com/charmbracelet/bubbles/v2/key"
|
||||
tea "github.com/charmbracelet/bubbletea/v2"
|
||||
"github.com/charmbracelet/lipgloss/v2"
|
||||
"github.com/muesli/reflow/truncate"
|
||||
"github.com/sst/opencode/internal/styles"
|
||||
"github.com/sst/opencode/internal/theme"
|
||||
@@ -174,19 +173,20 @@ type StringItem string
|
||||
|
||||
func (s StringItem) Render(selected bool, width int) string {
|
||||
t := theme.CurrentTheme()
|
||||
baseStyle := styles.BaseStyle()
|
||||
baseStyle := styles.NewStyle()
|
||||
|
||||
truncatedStr := truncate.StringWithTail(string(s), uint(width-1), "...")
|
||||
|
||||
var itemStyle lipgloss.Style
|
||||
var itemStyle styles.Style
|
||||
if selected {
|
||||
itemStyle = baseStyle.
|
||||
Background(t.Primary()).
|
||||
Foreground(t.Background()).
|
||||
Foreground(t.BackgroundElement()).
|
||||
Width(width).
|
||||
PaddingLeft(1)
|
||||
} else {
|
||||
itemStyle = baseStyle.
|
||||
Foreground(t.TextMuted()).
|
||||
PaddingLeft(1)
|
||||
}
|
||||
|
||||
|
||||
@@ -90,12 +90,8 @@ func (m *Modal) Render(contentView string, background string) string {
|
||||
|
||||
innerWidth := outerWidth - 4
|
||||
|
||||
// Base style for the modal
|
||||
baseStyle := styles.BaseStyle().
|
||||
Background(t.BackgroundElement()).
|
||||
Foreground(t.TextMuted())
|
||||
baseStyle := styles.NewStyle().Foreground(t.TextMuted()).Background(t.BackgroundElement())
|
||||
|
||||
// Add title if provided
|
||||
var finalContent string
|
||||
if m.title != "" {
|
||||
titleStyle := baseStyle.
|
||||
|
||||
@@ -3,7 +3,7 @@ package qr
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/charmbracelet/lipgloss/v2"
|
||||
"github.com/sst/opencode/internal/styles"
|
||||
"github.com/sst/opencode/internal/theme"
|
||||
"rsc.io/qr"
|
||||
)
|
||||
@@ -23,9 +23,7 @@ func Generate(text string) (string, int, error) {
|
||||
}
|
||||
|
||||
// Create lipgloss style for QR code with theme colors
|
||||
qrStyle := lipgloss.NewStyle().
|
||||
Foreground(t.Text()).
|
||||
Background(t.Background())
|
||||
qrStyle := styles.NewStyleWithColors(t.Text(), t.Background())
|
||||
|
||||
var result strings.Builder
|
||||
|
||||
|
||||
@@ -36,14 +36,15 @@ func (m statusComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
|
||||
func (m statusComponent) logo() string {
|
||||
t := theme.CurrentTheme()
|
||||
base := lipgloss.NewStyle().Background(t.BackgroundElement()).Foreground(t.TextMuted()).Render
|
||||
emphasis := lipgloss.NewStyle().Bold(true).Background(t.BackgroundElement()).Foreground(t.Text()).Render
|
||||
base := styles.NewStyle().Foreground(t.TextMuted()).Background(t.BackgroundElement()).Render
|
||||
emphasis := styles.NewStyle().Foreground(t.Text()).Background(t.BackgroundElement()).Bold(true).Render
|
||||
|
||||
open := base("open")
|
||||
code := emphasis("code ")
|
||||
version := base(m.app.Version)
|
||||
return styles.Padded().
|
||||
return styles.NewStyle().
|
||||
Background(t.BackgroundElement()).
|
||||
Padding(0, 1).
|
||||
Render(open + code + version)
|
||||
}
|
||||
|
||||
@@ -77,7 +78,7 @@ func formatTokensAndCost(tokens float32, contextWindow float32, cost float32) st
|
||||
func (m statusComponent) View() string {
|
||||
t := theme.CurrentTheme()
|
||||
if m.app.Session.Id == "" {
|
||||
return styles.BaseStyle().
|
||||
return styles.NewStyle().
|
||||
Background(t.Background()).
|
||||
Width(m.width).
|
||||
Height(2).
|
||||
@@ -86,9 +87,10 @@ func (m statusComponent) View() string {
|
||||
|
||||
logo := m.logo()
|
||||
|
||||
cwd := styles.Padded().
|
||||
cwd := styles.NewStyle().
|
||||
Foreground(t.TextMuted()).
|
||||
Background(t.BackgroundPanel()).
|
||||
Padding(0, 1).
|
||||
Render(m.app.Info.Path.Cwd)
|
||||
|
||||
sessionInfo := ""
|
||||
@@ -111,9 +113,10 @@ func (m statusComponent) View() string {
|
||||
}
|
||||
}
|
||||
|
||||
sessionInfo = styles.Padded().
|
||||
Background(t.BackgroundElement()).
|
||||
sessionInfo = styles.NewStyle().
|
||||
Foreground(t.TextMuted()).
|
||||
Background(t.BackgroundElement()).
|
||||
Padding(0, 1).
|
||||
Render(formatTokensAndCost(tokens, contextWindow, cost))
|
||||
}
|
||||
|
||||
@@ -123,11 +126,11 @@ func (m statusComponent) View() string {
|
||||
0,
|
||||
m.width-lipgloss.Width(logo)-lipgloss.Width(cwd)-lipgloss.Width(sessionInfo),
|
||||
)
|
||||
spacer := lipgloss.NewStyle().Background(t.BackgroundPanel()).Width(space).Render("")
|
||||
spacer := styles.NewStyle().Background(t.BackgroundPanel()).Width(space).Render("")
|
||||
|
||||
status := logo + cwd + spacer + sessionInfo
|
||||
|
||||
blank := styles.BaseStyle().Background(t.Background()).Width(m.width).Render("")
|
||||
blank := styles.NewStyle().Background(t.Background()).Width(m.width).Render("")
|
||||
return blank + "\n" + status
|
||||
}
|
||||
|
||||
|
||||
@@ -90,9 +90,9 @@ func (tm *ToastManager) Update(msg tea.Msg) (*ToastManager, tea.Cmd) {
|
||||
func (tm *ToastManager) renderSingleToast(toast Toast) string {
|
||||
t := theme.CurrentTheme()
|
||||
|
||||
baseStyle := styles.BaseStyle().
|
||||
Background(t.BackgroundElement()).
|
||||
baseStyle := styles.NewStyle().
|
||||
Foreground(t.Text()).
|
||||
Background(t.BackgroundElement()).
|
||||
Padding(1, 2)
|
||||
|
||||
maxWidth := max(40, layout.Current.Viewport.Width/3)
|
||||
@@ -101,15 +101,14 @@ func (tm *ToastManager) renderSingleToast(toast Toast) string {
|
||||
// Build content with wrapping
|
||||
var content strings.Builder
|
||||
if toast.Title != nil {
|
||||
titleStyle := lipgloss.NewStyle().
|
||||
Foreground(toast.Color).
|
||||
titleStyle := styles.NewStyle().Foreground(toast.Color).
|
||||
Bold(true)
|
||||
content.WriteString(titleStyle.Render(*toast.Title))
|
||||
content.WriteString("\n")
|
||||
}
|
||||
|
||||
// Wrap message text
|
||||
messageStyle := lipgloss.NewStyle()
|
||||
messageStyle := styles.NewStyle()
|
||||
contentWidth := lipgloss.Width(toast.Message)
|
||||
if contentWidth > contentMaxWidth {
|
||||
messageStyle = messageStyle.Width(contentMaxWidth)
|
||||
|
||||
@@ -3,6 +3,7 @@ package layout
|
||||
import (
|
||||
tea "github.com/charmbracelet/bubbletea/v2"
|
||||
"github.com/charmbracelet/lipgloss/v2"
|
||||
"github.com/sst/opencode/internal/styles"
|
||||
"github.com/sst/opencode/internal/theme"
|
||||
)
|
||||
|
||||
@@ -57,7 +58,7 @@ func (c *container) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
|
||||
func (c *container) View() string {
|
||||
t := theme.CurrentTheme()
|
||||
style := lipgloss.NewStyle()
|
||||
style := styles.NewStyle().Background(t.Background())
|
||||
width := c.width
|
||||
height := c.height
|
||||
|
||||
@@ -66,8 +67,6 @@ func (c *container) View() string {
|
||||
width = c.maxWidth
|
||||
}
|
||||
|
||||
style = style.Background(t.Background())
|
||||
|
||||
// Apply border if any side is enabled
|
||||
if c.borderTop || c.borderRight || c.borderBottom || c.borderLeft {
|
||||
// Adjust width and height for borders
|
||||
|
||||
@@ -3,6 +3,7 @@ package layout
|
||||
import (
|
||||
tea "github.com/charmbracelet/bubbletea/v2"
|
||||
"github.com/charmbracelet/lipgloss/v2"
|
||||
"github.com/sst/opencode/internal/styles"
|
||||
"github.com/sst/opencode/internal/theme"
|
||||
)
|
||||
|
||||
@@ -66,7 +67,7 @@ func (f *flexLayout) View() string {
|
||||
alignment,
|
||||
child.View(),
|
||||
// TODO: make configurable WithBackgroundStyle
|
||||
lipgloss.WithWhitespaceStyle(lipgloss.NewStyle().Background(t.Background())),
|
||||
lipgloss.WithWhitespaceStyle(styles.NewStyle().Background(t.Background()).Lipgloss()),
|
||||
)
|
||||
views = append(views, view)
|
||||
} else {
|
||||
@@ -78,7 +79,7 @@ func (f *flexLayout) View() string {
|
||||
alignment,
|
||||
child.View(),
|
||||
// TODO: make configurable WithBackgroundStyle
|
||||
lipgloss.WithWhitespaceStyle(lipgloss.NewStyle().Background(t.Background())),
|
||||
lipgloss.WithWhitespaceStyle(styles.NewStyle().Background(t.Background()).Lipgloss()),
|
||||
)
|
||||
views = append(views, view)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
package styles
|
||||
|
||||
import "image/color"
|
||||
|
||||
type TerminalInfo struct {
|
||||
Background color.Color
|
||||
BackgroundIsDark bool
|
||||
}
|
||||
|
||||
@@ -8,6 +11,7 @@ var Terminal *TerminalInfo
|
||||
|
||||
func init() {
|
||||
Terminal = &TerminalInfo{
|
||||
Background: color.Black,
|
||||
BackgroundIsDark: true,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package styles
|
||||
import (
|
||||
"github.com/charmbracelet/glamour"
|
||||
"github.com/charmbracelet/glamour/ansi"
|
||||
"github.com/charmbracelet/lipgloss/v2"
|
||||
"github.com/charmbracelet/lipgloss/v2/compat"
|
||||
"github.com/lucasb-eyer/go-colorful"
|
||||
"github.com/sst/opencode/internal/theme"
|
||||
@@ -29,7 +30,7 @@ func GetMarkdownRenderer(width int, backgroundColor compat.AdaptiveColor) *glamo
|
||||
// using adaptive colors from the provided theme.
|
||||
func generateMarkdownStyleConfig(backgroundColor compat.AdaptiveColor) ansi.StyleConfig {
|
||||
t := theme.CurrentTheme()
|
||||
background := stringPtr(AdaptiveColorToString(backgroundColor))
|
||||
background := AdaptiveColorToString(backgroundColor)
|
||||
|
||||
return ansi.StyleConfig{
|
||||
Document: ansi.StyleBlock{
|
||||
@@ -37,12 +38,12 @@ func generateMarkdownStyleConfig(backgroundColor compat.AdaptiveColor) ansi.Styl
|
||||
BlockPrefix: "",
|
||||
BlockSuffix: "",
|
||||
BackgroundColor: background,
|
||||
Color: stringPtr(AdaptiveColorToString(t.MarkdownText())),
|
||||
Color: AdaptiveColorToString(t.MarkdownText()),
|
||||
},
|
||||
},
|
||||
BlockQuote: ansi.StyleBlock{
|
||||
StylePrimitive: ansi.StylePrimitive{
|
||||
Color: stringPtr(AdaptiveColorToString(t.MarkdownBlockQuote())),
|
||||
Color: AdaptiveColorToString(t.MarkdownBlockQuote()),
|
||||
Italic: boolPtr(true),
|
||||
Prefix: "┃ ",
|
||||
},
|
||||
@@ -54,108 +55,108 @@ func generateMarkdownStyleConfig(backgroundColor compat.AdaptiveColor) ansi.Styl
|
||||
StyleBlock: ansi.StyleBlock{
|
||||
IndentToken: stringPtr(" "),
|
||||
StylePrimitive: ansi.StylePrimitive{
|
||||
Color: stringPtr(AdaptiveColorToString(t.MarkdownText())),
|
||||
Color: AdaptiveColorToString(t.MarkdownText()),
|
||||
},
|
||||
},
|
||||
},
|
||||
Heading: ansi.StyleBlock{
|
||||
StylePrimitive: ansi.StylePrimitive{
|
||||
BlockSuffix: "\n",
|
||||
Color: stringPtr(AdaptiveColorToString(t.MarkdownHeading())),
|
||||
Color: AdaptiveColorToString(t.MarkdownHeading()),
|
||||
Bold: boolPtr(true),
|
||||
},
|
||||
},
|
||||
H1: ansi.StyleBlock{
|
||||
StylePrimitive: ansi.StylePrimitive{
|
||||
Prefix: "# ",
|
||||
Color: stringPtr(AdaptiveColorToString(t.MarkdownHeading())),
|
||||
Color: AdaptiveColorToString(t.MarkdownHeading()),
|
||||
Bold: boolPtr(true),
|
||||
},
|
||||
},
|
||||
H2: ansi.StyleBlock{
|
||||
StylePrimitive: ansi.StylePrimitive{
|
||||
Prefix: "## ",
|
||||
Color: stringPtr(AdaptiveColorToString(t.MarkdownHeading())),
|
||||
Color: AdaptiveColorToString(t.MarkdownHeading()),
|
||||
Bold: boolPtr(true),
|
||||
},
|
||||
},
|
||||
H3: ansi.StyleBlock{
|
||||
StylePrimitive: ansi.StylePrimitive{
|
||||
Prefix: "### ",
|
||||
Color: stringPtr(AdaptiveColorToString(t.MarkdownHeading())),
|
||||
Color: AdaptiveColorToString(t.MarkdownHeading()),
|
||||
Bold: boolPtr(true),
|
||||
},
|
||||
},
|
||||
H4: ansi.StyleBlock{
|
||||
StylePrimitive: ansi.StylePrimitive{
|
||||
Prefix: "#### ",
|
||||
Color: stringPtr(AdaptiveColorToString(t.MarkdownHeading())),
|
||||
Color: AdaptiveColorToString(t.MarkdownHeading()),
|
||||
Bold: boolPtr(true),
|
||||
},
|
||||
},
|
||||
H5: ansi.StyleBlock{
|
||||
StylePrimitive: ansi.StylePrimitive{
|
||||
Prefix: "##### ",
|
||||
Color: stringPtr(AdaptiveColorToString(t.MarkdownHeading())),
|
||||
Color: AdaptiveColorToString(t.MarkdownHeading()),
|
||||
Bold: boolPtr(true),
|
||||
},
|
||||
},
|
||||
H6: ansi.StyleBlock{
|
||||
StylePrimitive: ansi.StylePrimitive{
|
||||
Prefix: "###### ",
|
||||
Color: stringPtr(AdaptiveColorToString(t.MarkdownHeading())),
|
||||
Color: AdaptiveColorToString(t.MarkdownHeading()),
|
||||
Bold: boolPtr(true),
|
||||
},
|
||||
},
|
||||
Strikethrough: ansi.StylePrimitive{
|
||||
CrossedOut: boolPtr(true),
|
||||
Color: stringPtr(AdaptiveColorToString(t.TextMuted())),
|
||||
Color: AdaptiveColorToString(t.TextMuted()),
|
||||
},
|
||||
Emph: ansi.StylePrimitive{
|
||||
Color: stringPtr(AdaptiveColorToString(t.MarkdownEmph())),
|
||||
Color: AdaptiveColorToString(t.MarkdownEmph()),
|
||||
Italic: boolPtr(true),
|
||||
},
|
||||
Strong: ansi.StylePrimitive{
|
||||
Bold: boolPtr(true),
|
||||
Color: stringPtr(AdaptiveColorToString(t.MarkdownStrong())),
|
||||
Color: AdaptiveColorToString(t.MarkdownStrong()),
|
||||
},
|
||||
HorizontalRule: ansi.StylePrimitive{
|
||||
Color: stringPtr(AdaptiveColorToString(t.MarkdownHorizontalRule())),
|
||||
Color: AdaptiveColorToString(t.MarkdownHorizontalRule()),
|
||||
Format: "\n─────────────────────────────────────────\n",
|
||||
},
|
||||
Item: ansi.StylePrimitive{
|
||||
BlockPrefix: "• ",
|
||||
Color: stringPtr(AdaptiveColorToString(t.MarkdownListItem())),
|
||||
Color: AdaptiveColorToString(t.MarkdownListItem()),
|
||||
},
|
||||
Enumeration: ansi.StylePrimitive{
|
||||
BlockPrefix: ". ",
|
||||
Color: stringPtr(AdaptiveColorToString(t.MarkdownListEnumeration())),
|
||||
Color: AdaptiveColorToString(t.MarkdownListEnumeration()),
|
||||
},
|
||||
Task: ansi.StyleTask{
|
||||
Ticked: "[✓] ",
|
||||
Unticked: "[ ] ",
|
||||
},
|
||||
Link: ansi.StylePrimitive{
|
||||
Color: stringPtr(AdaptiveColorToString(t.MarkdownLink())),
|
||||
Color: AdaptiveColorToString(t.MarkdownLink()),
|
||||
Underline: boolPtr(true),
|
||||
},
|
||||
LinkText: ansi.StylePrimitive{
|
||||
Color: stringPtr(AdaptiveColorToString(t.MarkdownLinkText())),
|
||||
Color: AdaptiveColorToString(t.MarkdownLinkText()),
|
||||
Bold: boolPtr(true),
|
||||
},
|
||||
Image: ansi.StylePrimitive{
|
||||
Color: stringPtr(AdaptiveColorToString(t.MarkdownImage())),
|
||||
Color: AdaptiveColorToString(t.MarkdownImage()),
|
||||
Underline: boolPtr(true),
|
||||
Format: "🖼 {{.text}}",
|
||||
},
|
||||
ImageText: ansi.StylePrimitive{
|
||||
Color: stringPtr(AdaptiveColorToString(t.MarkdownImageText())),
|
||||
Color: AdaptiveColorToString(t.MarkdownImageText()),
|
||||
Format: "{{.text}}",
|
||||
},
|
||||
Code: ansi.StyleBlock{
|
||||
StylePrimitive: ansi.StylePrimitive{
|
||||
BackgroundColor: background,
|
||||
Color: stringPtr(AdaptiveColorToString(t.MarkdownCode())),
|
||||
Color: AdaptiveColorToString(t.MarkdownCode()),
|
||||
Prefix: "",
|
||||
Suffix: "",
|
||||
},
|
||||
@@ -165,7 +166,7 @@ func generateMarkdownStyleConfig(backgroundColor compat.AdaptiveColor) ansi.Styl
|
||||
StylePrimitive: ansi.StylePrimitive{
|
||||
BackgroundColor: background,
|
||||
Prefix: " ",
|
||||
Color: stringPtr(AdaptiveColorToString(t.MarkdownCodeBlock())),
|
||||
Color: AdaptiveColorToString(t.MarkdownCodeBlock()),
|
||||
},
|
||||
},
|
||||
Chroma: &ansi.Chroma{
|
||||
@@ -174,109 +175,109 @@ func generateMarkdownStyleConfig(backgroundColor compat.AdaptiveColor) ansi.Styl
|
||||
},
|
||||
Text: ansi.StylePrimitive{
|
||||
BackgroundColor: background,
|
||||
Color: stringPtr(AdaptiveColorToString(t.MarkdownText())),
|
||||
Color: AdaptiveColorToString(t.MarkdownText()),
|
||||
},
|
||||
Error: ansi.StylePrimitive{
|
||||
BackgroundColor: background,
|
||||
Color: stringPtr(AdaptiveColorToString(t.Error())),
|
||||
Color: AdaptiveColorToString(t.Error()),
|
||||
},
|
||||
Comment: ansi.StylePrimitive{
|
||||
BackgroundColor: background,
|
||||
Color: stringPtr(AdaptiveColorToString(t.SyntaxComment())),
|
||||
Color: AdaptiveColorToString(t.SyntaxComment()),
|
||||
},
|
||||
CommentPreproc: ansi.StylePrimitive{
|
||||
BackgroundColor: background,
|
||||
Color: stringPtr(AdaptiveColorToString(t.SyntaxKeyword())),
|
||||
Color: AdaptiveColorToString(t.SyntaxKeyword()),
|
||||
},
|
||||
Keyword: ansi.StylePrimitive{
|
||||
BackgroundColor: background,
|
||||
Color: stringPtr(AdaptiveColorToString(t.SyntaxKeyword())),
|
||||
Color: AdaptiveColorToString(t.SyntaxKeyword()),
|
||||
},
|
||||
KeywordReserved: ansi.StylePrimitive{
|
||||
BackgroundColor: background,
|
||||
Color: stringPtr(AdaptiveColorToString(t.SyntaxKeyword())),
|
||||
Color: AdaptiveColorToString(t.SyntaxKeyword()),
|
||||
},
|
||||
KeywordNamespace: ansi.StylePrimitive{
|
||||
BackgroundColor: background,
|
||||
Color: stringPtr(AdaptiveColorToString(t.SyntaxKeyword())),
|
||||
Color: AdaptiveColorToString(t.SyntaxKeyword()),
|
||||
},
|
||||
KeywordType: ansi.StylePrimitive{
|
||||
BackgroundColor: background,
|
||||
Color: stringPtr(AdaptiveColorToString(t.SyntaxType())),
|
||||
Color: AdaptiveColorToString(t.SyntaxType()),
|
||||
},
|
||||
Operator: ansi.StylePrimitive{
|
||||
BackgroundColor: background,
|
||||
Color: stringPtr(AdaptiveColorToString(t.SyntaxOperator())),
|
||||
Color: AdaptiveColorToString(t.SyntaxOperator()),
|
||||
},
|
||||
Punctuation: ansi.StylePrimitive{
|
||||
BackgroundColor: background,
|
||||
Color: stringPtr(AdaptiveColorToString(t.SyntaxPunctuation())),
|
||||
Color: AdaptiveColorToString(t.SyntaxPunctuation()),
|
||||
},
|
||||
Name: ansi.StylePrimitive{
|
||||
BackgroundColor: background,
|
||||
Color: stringPtr(AdaptiveColorToString(t.SyntaxVariable())),
|
||||
Color: AdaptiveColorToString(t.SyntaxVariable()),
|
||||
},
|
||||
NameBuiltin: ansi.StylePrimitive{
|
||||
BackgroundColor: background,
|
||||
Color: stringPtr(AdaptiveColorToString(t.SyntaxVariable())),
|
||||
Color: AdaptiveColorToString(t.SyntaxVariable()),
|
||||
},
|
||||
NameTag: ansi.StylePrimitive{
|
||||
BackgroundColor: background,
|
||||
Color: stringPtr(AdaptiveColorToString(t.SyntaxKeyword())),
|
||||
Color: AdaptiveColorToString(t.SyntaxKeyword()),
|
||||
},
|
||||
NameAttribute: ansi.StylePrimitive{
|
||||
BackgroundColor: background,
|
||||
Color: stringPtr(AdaptiveColorToString(t.SyntaxFunction())),
|
||||
Color: AdaptiveColorToString(t.SyntaxFunction()),
|
||||
},
|
||||
NameClass: ansi.StylePrimitive{
|
||||
BackgroundColor: background,
|
||||
Color: stringPtr(AdaptiveColorToString(t.SyntaxType())),
|
||||
Color: AdaptiveColorToString(t.SyntaxType()),
|
||||
},
|
||||
NameConstant: ansi.StylePrimitive{
|
||||
BackgroundColor: background,
|
||||
Color: stringPtr(AdaptiveColorToString(t.SyntaxVariable())),
|
||||
Color: AdaptiveColorToString(t.SyntaxVariable()),
|
||||
},
|
||||
NameDecorator: ansi.StylePrimitive{
|
||||
BackgroundColor: background,
|
||||
Color: stringPtr(AdaptiveColorToString(t.SyntaxFunction())),
|
||||
Color: AdaptiveColorToString(t.SyntaxFunction()),
|
||||
},
|
||||
NameFunction: ansi.StylePrimitive{
|
||||
BackgroundColor: background,
|
||||
Color: stringPtr(AdaptiveColorToString(t.SyntaxFunction())),
|
||||
Color: AdaptiveColorToString(t.SyntaxFunction()),
|
||||
},
|
||||
LiteralNumber: ansi.StylePrimitive{
|
||||
BackgroundColor: background,
|
||||
Color: stringPtr(AdaptiveColorToString(t.SyntaxNumber())),
|
||||
Color: AdaptiveColorToString(t.SyntaxNumber()),
|
||||
},
|
||||
LiteralString: ansi.StylePrimitive{
|
||||
BackgroundColor: background,
|
||||
Color: stringPtr(AdaptiveColorToString(t.SyntaxString())),
|
||||
Color: AdaptiveColorToString(t.SyntaxString()),
|
||||
},
|
||||
LiteralStringEscape: ansi.StylePrimitive{
|
||||
BackgroundColor: background,
|
||||
Color: stringPtr(AdaptiveColorToString(t.SyntaxKeyword())),
|
||||
Color: AdaptiveColorToString(t.SyntaxKeyword()),
|
||||
},
|
||||
GenericDeleted: ansi.StylePrimitive{
|
||||
BackgroundColor: background,
|
||||
Color: stringPtr(AdaptiveColorToString(t.DiffRemoved())),
|
||||
Color: AdaptiveColorToString(t.DiffRemoved()),
|
||||
},
|
||||
GenericEmph: ansi.StylePrimitive{
|
||||
BackgroundColor: background,
|
||||
Color: stringPtr(AdaptiveColorToString(t.MarkdownEmph())),
|
||||
Color: AdaptiveColorToString(t.MarkdownEmph()),
|
||||
Italic: boolPtr(true),
|
||||
},
|
||||
GenericInserted: ansi.StylePrimitive{
|
||||
BackgroundColor: background,
|
||||
Color: stringPtr(AdaptiveColorToString(t.DiffAdded())),
|
||||
Color: AdaptiveColorToString(t.DiffAdded()),
|
||||
},
|
||||
GenericStrong: ansi.StylePrimitive{
|
||||
BackgroundColor: background,
|
||||
Color: stringPtr(AdaptiveColorToString(t.MarkdownStrong())),
|
||||
Color: AdaptiveColorToString(t.MarkdownStrong()),
|
||||
Bold: boolPtr(true),
|
||||
},
|
||||
GenericSubheading: ansi.StylePrimitive{
|
||||
BackgroundColor: background,
|
||||
Color: stringPtr(AdaptiveColorToString(t.MarkdownHeading())),
|
||||
Color: AdaptiveColorToString(t.MarkdownHeading()),
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -293,14 +294,14 @@ func generateMarkdownStyleConfig(backgroundColor compat.AdaptiveColor) ansi.Styl
|
||||
},
|
||||
DefinitionDescription: ansi.StylePrimitive{
|
||||
BlockPrefix: "\n ❯ ",
|
||||
Color: stringPtr(AdaptiveColorToString(t.MarkdownLinkText())),
|
||||
Color: AdaptiveColorToString(t.MarkdownLinkText()),
|
||||
},
|
||||
Text: ansi.StylePrimitive{
|
||||
Color: stringPtr(AdaptiveColorToString(t.MarkdownText())),
|
||||
Color: AdaptiveColorToString(t.MarkdownText()),
|
||||
},
|
||||
Paragraph: ansi.StyleBlock{
|
||||
StylePrimitive: ansi.StylePrimitive{
|
||||
Color: stringPtr(AdaptiveColorToString(t.MarkdownText())),
|
||||
Color: AdaptiveColorToString(t.MarkdownText()),
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -308,11 +309,17 @@ func generateMarkdownStyleConfig(backgroundColor compat.AdaptiveColor) ansi.Styl
|
||||
|
||||
// AdaptiveColorToString converts a compat.AdaptiveColor to the appropriate
|
||||
// hex color string based on the current terminal background
|
||||
func AdaptiveColorToString(color compat.AdaptiveColor) string {
|
||||
func AdaptiveColorToString(color compat.AdaptiveColor) *string {
|
||||
if Terminal.BackgroundIsDark {
|
||||
if _, ok := color.Dark.(lipgloss.NoColor); ok {
|
||||
return nil
|
||||
}
|
||||
c1, _ := colorful.MakeColor(color.Dark)
|
||||
return c1.Hex()
|
||||
return stringPtr(c1.Hex())
|
||||
}
|
||||
if _, ok := color.Light.(lipgloss.NoColor); ok {
|
||||
return nil
|
||||
}
|
||||
c1, _ := colorful.MakeColor(color.Light)
|
||||
return c1.Hex()
|
||||
return stringPtr(c1.Hex())
|
||||
}
|
||||
|
||||
@@ -3,155 +3,8 @@ package styles
|
||||
import (
|
||||
"github.com/charmbracelet/lipgloss/v2"
|
||||
"github.com/charmbracelet/lipgloss/v2/compat"
|
||||
"github.com/sst/opencode/internal/theme"
|
||||
)
|
||||
|
||||
// BaseStyle returns the base style with background and foreground colors
|
||||
func BaseStyle() lipgloss.Style {
|
||||
t := theme.CurrentTheme()
|
||||
return lipgloss.NewStyle().Foreground(t.Text())
|
||||
}
|
||||
|
||||
func Panel() lipgloss.Style {
|
||||
t := theme.CurrentTheme()
|
||||
return lipgloss.NewStyle().
|
||||
Background(t.BackgroundPanel()).
|
||||
Border(lipgloss.NormalBorder(), true, false, true, false).
|
||||
BorderForeground(t.BorderSubtle()).
|
||||
Foreground(t.Text())
|
||||
}
|
||||
|
||||
// Regular returns a basic unstyled lipgloss.Style
|
||||
func Regular() lipgloss.Style {
|
||||
return lipgloss.NewStyle()
|
||||
}
|
||||
|
||||
func Muted() lipgloss.Style {
|
||||
t := theme.CurrentTheme()
|
||||
return lipgloss.NewStyle().Foreground(t.TextMuted())
|
||||
}
|
||||
|
||||
// Bold returns a bold style
|
||||
func Bold() lipgloss.Style {
|
||||
return BaseStyle().Bold(true)
|
||||
}
|
||||
|
||||
// Padded returns a style with horizontal padding
|
||||
func Padded() lipgloss.Style {
|
||||
return BaseStyle().Padding(0, 1)
|
||||
}
|
||||
|
||||
// Border returns a style with a normal border
|
||||
func Border() lipgloss.Style {
|
||||
t := theme.CurrentTheme()
|
||||
return Regular().
|
||||
Border(lipgloss.NormalBorder()).
|
||||
BorderForeground(t.Border())
|
||||
}
|
||||
|
||||
// ThickBorder returns a style with a thick border
|
||||
func ThickBorder() lipgloss.Style {
|
||||
t := theme.CurrentTheme()
|
||||
return Regular().
|
||||
Border(lipgloss.ThickBorder()).
|
||||
BorderForeground(t.Border())
|
||||
}
|
||||
|
||||
// DoubleBorder returns a style with a double border
|
||||
func DoubleBorder() lipgloss.Style {
|
||||
t := theme.CurrentTheme()
|
||||
return Regular().
|
||||
Border(lipgloss.DoubleBorder()).
|
||||
BorderForeground(t.Border())
|
||||
}
|
||||
|
||||
// FocusedBorder returns a style with a border using the focused border color
|
||||
func FocusedBorder() lipgloss.Style {
|
||||
t := theme.CurrentTheme()
|
||||
return Regular().
|
||||
Border(lipgloss.NormalBorder()).
|
||||
BorderForeground(t.BorderActive())
|
||||
}
|
||||
|
||||
// DimBorder returns a style with a border using the dim border color
|
||||
func DimBorder() lipgloss.Style {
|
||||
t := theme.CurrentTheme()
|
||||
return Regular().
|
||||
Border(lipgloss.NormalBorder()).
|
||||
BorderForeground(t.BorderSubtle())
|
||||
}
|
||||
|
||||
// PrimaryColor returns the primary color from the current theme
|
||||
func PrimaryColor() compat.AdaptiveColor {
|
||||
return theme.CurrentTheme().Primary()
|
||||
}
|
||||
|
||||
// SecondaryColor returns the secondary color from the current theme
|
||||
func SecondaryColor() compat.AdaptiveColor {
|
||||
return theme.CurrentTheme().Secondary()
|
||||
}
|
||||
|
||||
// AccentColor returns the accent color from the current theme
|
||||
func AccentColor() compat.AdaptiveColor {
|
||||
return theme.CurrentTheme().Accent()
|
||||
}
|
||||
|
||||
// ErrorColor returns the error color from the current theme
|
||||
func ErrorColor() compat.AdaptiveColor {
|
||||
return theme.CurrentTheme().Error()
|
||||
}
|
||||
|
||||
// WarningColor returns the warning color from the current theme
|
||||
func WarningColor() compat.AdaptiveColor {
|
||||
return theme.CurrentTheme().Warning()
|
||||
}
|
||||
|
||||
// SuccessColor returns the success color from the current theme
|
||||
func SuccessColor() compat.AdaptiveColor {
|
||||
return theme.CurrentTheme().Success()
|
||||
}
|
||||
|
||||
// InfoColor returns the info color from the current theme
|
||||
func InfoColor() compat.AdaptiveColor {
|
||||
return theme.CurrentTheme().Info()
|
||||
}
|
||||
|
||||
// TextColor returns the text color from the current theme
|
||||
func TextColor() compat.AdaptiveColor {
|
||||
return theme.CurrentTheme().Text()
|
||||
}
|
||||
|
||||
// TextMutedColor returns the muted text color from the current theme
|
||||
func TextMutedColor() compat.AdaptiveColor {
|
||||
return theme.CurrentTheme().TextMuted()
|
||||
}
|
||||
|
||||
// BackgroundColor returns the background color from the current theme
|
||||
func BackgroundColor() compat.AdaptiveColor {
|
||||
return theme.CurrentTheme().Background()
|
||||
}
|
||||
|
||||
// BackgroundPanelColor returns the subtle background color from the current theme
|
||||
func BackgroundPanelColor() compat.AdaptiveColor {
|
||||
return theme.CurrentTheme().BackgroundPanel()
|
||||
}
|
||||
|
||||
// BackgroundElementColor returns the darker background color from the current theme
|
||||
func BackgroundElementColor() compat.AdaptiveColor {
|
||||
return theme.CurrentTheme().BackgroundElement()
|
||||
}
|
||||
|
||||
// BorderColor returns the border color from the current theme
|
||||
func BorderColor() compat.AdaptiveColor {
|
||||
return theme.CurrentTheme().Border()
|
||||
}
|
||||
|
||||
// BorderActiveColor returns the active border color from the current theme
|
||||
func BorderActiveColor() compat.AdaptiveColor {
|
||||
return theme.CurrentTheme().BorderActive()
|
||||
}
|
||||
|
||||
// BorderSubtleColor returns the subtle border color from the current theme
|
||||
func BorderSubtleColor() compat.AdaptiveColor {
|
||||
return theme.CurrentTheme().BorderSubtle()
|
||||
func WhitespaceStyle(bg compat.AdaptiveColor) lipgloss.WhitespaceOption {
|
||||
return lipgloss.WithWhitespaceStyle(NewStyle().Background(bg).Lipgloss())
|
||||
}
|
||||
|
||||
@@ -0,0 +1,295 @@
|
||||
package styles
|
||||
|
||||
import (
|
||||
"image/color"
|
||||
|
||||
"github.com/charmbracelet/lipgloss/v2"
|
||||
"github.com/charmbracelet/lipgloss/v2/compat"
|
||||
)
|
||||
|
||||
// IsNoColor checks if a color is the special NoColor type
|
||||
func IsNoColor(c color.Color) bool {
|
||||
_, ok := c.(lipgloss.NoColor)
|
||||
return ok
|
||||
}
|
||||
|
||||
// Style wraps lipgloss.Style to provide a fluent API for handling "none" colors
|
||||
type Style struct {
|
||||
lipgloss.Style
|
||||
}
|
||||
|
||||
// NewStyle creates a new Style with proper handling of "none" colors
|
||||
func NewStyle() Style {
|
||||
return Style{lipgloss.NewStyle()}
|
||||
}
|
||||
|
||||
func (s Style) Lipgloss() lipgloss.Style {
|
||||
return s.Style
|
||||
}
|
||||
|
||||
// Foreground sets the foreground color, handling "none" appropriately
|
||||
func (s Style) Foreground(c compat.AdaptiveColor) Style {
|
||||
if IsNoColor(c.Dark) && IsNoColor(c.Light) {
|
||||
return Style{s.Style.UnsetForeground()}
|
||||
}
|
||||
return Style{s.Style.Foreground(c)}
|
||||
}
|
||||
|
||||
// Background sets the background color, handling "none" appropriately
|
||||
func (s Style) Background(c compat.AdaptiveColor) Style {
|
||||
if IsNoColor(c.Dark) && IsNoColor(c.Light) {
|
||||
return Style{s.Style.UnsetBackground()}
|
||||
}
|
||||
return Style{s.Style.Background(c)}
|
||||
}
|
||||
|
||||
// BorderForeground sets the border foreground color, handling "none" appropriately
|
||||
func (s Style) BorderForeground(c compat.AdaptiveColor) Style {
|
||||
if IsNoColor(c.Dark) && IsNoColor(c.Light) {
|
||||
return Style{s.Style.UnsetBorderForeground()}
|
||||
}
|
||||
return Style{s.Style.BorderForeground(c)}
|
||||
}
|
||||
|
||||
// BorderBackground sets the border background color, handling "none" appropriately
|
||||
func (s Style) BorderBackground(c compat.AdaptiveColor) Style {
|
||||
if IsNoColor(c.Dark) && IsNoColor(c.Light) {
|
||||
return Style{s.Style.UnsetBorderBackground()}
|
||||
}
|
||||
return Style{s.Style.BorderBackground(c)}
|
||||
}
|
||||
|
||||
// BorderTopForeground sets the border top foreground color, handling "none" appropriately
|
||||
func (s Style) BorderTopForeground(c compat.AdaptiveColor) Style {
|
||||
if IsNoColor(c.Dark) && IsNoColor(c.Light) {
|
||||
return Style{s.Style.UnsetBorderTopForeground()}
|
||||
}
|
||||
return Style{s.Style.BorderTopForeground(c)}
|
||||
}
|
||||
|
||||
// BorderTopBackground sets the border top background color, handling "none" appropriately
|
||||
func (s Style) BorderTopBackground(c compat.AdaptiveColor) Style {
|
||||
if IsNoColor(c.Dark) && IsNoColor(c.Light) {
|
||||
return Style{s.Style.UnsetBorderTopBackground()}
|
||||
}
|
||||
return Style{s.Style.BorderTopBackground(c)}
|
||||
}
|
||||
|
||||
// BorderBottomForeground sets the border bottom foreground color, handling "none" appropriately
|
||||
func (s Style) BorderBottomForeground(c compat.AdaptiveColor) Style {
|
||||
if IsNoColor(c.Dark) && IsNoColor(c.Light) {
|
||||
return Style{s.Style.UnsetBorderBottomForeground()}
|
||||
}
|
||||
return Style{s.Style.BorderBottomForeground(c)}
|
||||
}
|
||||
|
||||
// BorderBottomBackground sets the border bottom background color, handling "none" appropriately
|
||||
func (s Style) BorderBottomBackground(c compat.AdaptiveColor) Style {
|
||||
if IsNoColor(c.Dark) && IsNoColor(c.Light) {
|
||||
return Style{s.Style.UnsetBorderBottomBackground()}
|
||||
}
|
||||
return Style{s.Style.BorderBottomBackground(c)}
|
||||
}
|
||||
|
||||
// BorderLeftForeground sets the border left foreground color, handling "none" appropriately
|
||||
func (s Style) BorderLeftForeground(c compat.AdaptiveColor) Style {
|
||||
if IsNoColor(c.Dark) && IsNoColor(c.Light) {
|
||||
return Style{s.Style.UnsetBorderLeftForeground()}
|
||||
}
|
||||
return Style{s.Style.BorderLeftForeground(c)}
|
||||
}
|
||||
|
||||
// BorderLeftBackground sets the border left background color, handling "none" appropriately
|
||||
func (s Style) BorderLeftBackground(c compat.AdaptiveColor) Style {
|
||||
if IsNoColor(c.Dark) && IsNoColor(c.Light) {
|
||||
return Style{s.Style.UnsetBorderLeftBackground()}
|
||||
}
|
||||
return Style{s.Style.BorderLeftBackground(c)}
|
||||
}
|
||||
|
||||
// BorderRightForeground sets the border right foreground color, handling "none" appropriately
|
||||
func (s Style) BorderRightForeground(c compat.AdaptiveColor) Style {
|
||||
if IsNoColor(c.Dark) && IsNoColor(c.Light) {
|
||||
return Style{s.Style.UnsetBorderRightForeground()}
|
||||
}
|
||||
return Style{s.Style.BorderRightForeground(c)}
|
||||
}
|
||||
|
||||
// BorderRightBackground sets the border right background color, handling "none" appropriately
|
||||
func (s Style) BorderRightBackground(c compat.AdaptiveColor) Style {
|
||||
if IsNoColor(c.Dark) && IsNoColor(c.Light) {
|
||||
return Style{s.Style.UnsetBorderRightBackground()}
|
||||
}
|
||||
return Style{s.Style.BorderRightBackground(c)}
|
||||
}
|
||||
|
||||
// Render applies the style to a string
|
||||
func (s Style) Render(str string) string {
|
||||
return s.Style.Render(str)
|
||||
}
|
||||
|
||||
// Common lipgloss.Style method delegations for seamless usage
|
||||
|
||||
func (s Style) Bold(v bool) Style {
|
||||
return Style{s.Style.Bold(v)}
|
||||
}
|
||||
|
||||
func (s Style) Italic(v bool) Style {
|
||||
return Style{s.Style.Italic(v)}
|
||||
}
|
||||
|
||||
func (s Style) Underline(v bool) Style {
|
||||
return Style{s.Style.Underline(v)}
|
||||
}
|
||||
|
||||
func (s Style) Strikethrough(v bool) Style {
|
||||
return Style{s.Style.Strikethrough(v)}
|
||||
}
|
||||
|
||||
func (s Style) Blink(v bool) Style {
|
||||
return Style{s.Style.Blink(v)}
|
||||
}
|
||||
|
||||
func (s Style) Faint(v bool) Style {
|
||||
return Style{s.Style.Faint(v)}
|
||||
}
|
||||
|
||||
func (s Style) Reverse(v bool) Style {
|
||||
return Style{s.Style.Reverse(v)}
|
||||
}
|
||||
|
||||
func (s Style) Width(i int) Style {
|
||||
return Style{s.Style.Width(i)}
|
||||
}
|
||||
|
||||
func (s Style) Height(i int) Style {
|
||||
return Style{s.Style.Height(i)}
|
||||
}
|
||||
|
||||
func (s Style) Padding(i ...int) Style {
|
||||
return Style{s.Style.Padding(i...)}
|
||||
}
|
||||
|
||||
func (s Style) PaddingTop(i int) Style {
|
||||
return Style{s.Style.PaddingTop(i)}
|
||||
}
|
||||
|
||||
func (s Style) PaddingBottom(i int) Style {
|
||||
return Style{s.Style.PaddingBottom(i)}
|
||||
}
|
||||
|
||||
func (s Style) PaddingLeft(i int) Style {
|
||||
return Style{s.Style.PaddingLeft(i)}
|
||||
}
|
||||
|
||||
func (s Style) PaddingRight(i int) Style {
|
||||
return Style{s.Style.PaddingRight(i)}
|
||||
}
|
||||
|
||||
func (s Style) Margin(i ...int) Style {
|
||||
return Style{s.Style.Margin(i...)}
|
||||
}
|
||||
|
||||
func (s Style) MarginTop(i int) Style {
|
||||
return Style{s.Style.MarginTop(i)}
|
||||
}
|
||||
|
||||
func (s Style) MarginBottom(i int) Style {
|
||||
return Style{s.Style.MarginBottom(i)}
|
||||
}
|
||||
|
||||
func (s Style) MarginLeft(i int) Style {
|
||||
return Style{s.Style.MarginLeft(i)}
|
||||
}
|
||||
|
||||
func (s Style) MarginRight(i int) Style {
|
||||
return Style{s.Style.MarginRight(i)}
|
||||
}
|
||||
|
||||
func (s Style) Border(b lipgloss.Border, sides ...bool) Style {
|
||||
return Style{s.Style.Border(b, sides...)}
|
||||
}
|
||||
|
||||
func (s Style) BorderStyle(b lipgloss.Border) Style {
|
||||
return Style{s.Style.BorderStyle(b)}
|
||||
}
|
||||
|
||||
func (s Style) BorderTop(v bool) Style {
|
||||
return Style{s.Style.BorderTop(v)}
|
||||
}
|
||||
|
||||
func (s Style) BorderBottom(v bool) Style {
|
||||
return Style{s.Style.BorderBottom(v)}
|
||||
}
|
||||
|
||||
func (s Style) BorderLeft(v bool) Style {
|
||||
return Style{s.Style.BorderLeft(v)}
|
||||
}
|
||||
|
||||
func (s Style) BorderRight(v bool) Style {
|
||||
return Style{s.Style.BorderRight(v)}
|
||||
}
|
||||
|
||||
func (s Style) Align(p ...lipgloss.Position) Style {
|
||||
return Style{s.Style.Align(p...)}
|
||||
}
|
||||
|
||||
func (s Style) AlignHorizontal(p lipgloss.Position) Style {
|
||||
return Style{s.Style.AlignHorizontal(p)}
|
||||
}
|
||||
|
||||
func (s Style) AlignVertical(p lipgloss.Position) Style {
|
||||
return Style{s.Style.AlignVertical(p)}
|
||||
}
|
||||
|
||||
func (s Style) Inline(v bool) Style {
|
||||
return Style{s.Style.Inline(v)}
|
||||
}
|
||||
|
||||
func (s Style) MaxWidth(n int) Style {
|
||||
return Style{s.Style.MaxWidth(n)}
|
||||
}
|
||||
|
||||
func (s Style) MaxHeight(n int) Style {
|
||||
return Style{s.Style.MaxHeight(n)}
|
||||
}
|
||||
|
||||
func (s Style) TabWidth(n int) Style {
|
||||
return Style{s.Style.TabWidth(n)}
|
||||
}
|
||||
|
||||
func (s Style) UnsetBold() Style {
|
||||
return Style{s.Style.UnsetBold()}
|
||||
}
|
||||
|
||||
func (s Style) UnsetItalic() Style {
|
||||
return Style{s.Style.UnsetItalic()}
|
||||
}
|
||||
|
||||
func (s Style) UnsetUnderline() Style {
|
||||
return Style{s.Style.UnsetUnderline()}
|
||||
}
|
||||
|
||||
func (s Style) UnsetStrikethrough() Style {
|
||||
return Style{s.Style.UnsetStrikethrough()}
|
||||
}
|
||||
|
||||
func (s Style) UnsetBlink() Style {
|
||||
return Style{s.Style.UnsetBlink()}
|
||||
}
|
||||
|
||||
func (s Style) UnsetFaint() Style {
|
||||
return Style{s.Style.UnsetFaint()}
|
||||
}
|
||||
|
||||
func (s Style) UnsetReverse() Style {
|
||||
return Style{s.Style.UnsetReverse()}
|
||||
}
|
||||
|
||||
func (s Style) Copy() Style {
|
||||
return Style{s.Style}
|
||||
}
|
||||
|
||||
func (s Style) Inherit(i Style) Style {
|
||||
return Style{s.Style.Inherit(i.Style)}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"fmt"
|
||||
"image/color"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
@@ -42,7 +43,7 @@ func LoadThemesFromJSON() error {
|
||||
continue
|
||||
}
|
||||
themeName := strings.TrimSuffix(entry.Name(), ".json")
|
||||
data, err := themesFS.ReadFile(filepath.Join("themes", entry.Name()))
|
||||
data, err := themesFS.ReadFile(path.Join("themes", entry.Name()))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read theme file %s: %w", entry.Name(), err)
|
||||
}
|
||||
@@ -170,7 +171,7 @@ func (r *colorResolver) resolveColor(key string, value any) (any, error) {
|
||||
|
||||
switch v := value.(type) {
|
||||
case string:
|
||||
if strings.HasPrefix(v, "#") {
|
||||
if strings.HasPrefix(v, "#") || v == "none" {
|
||||
return v, nil
|
||||
}
|
||||
return r.resolveReference(v)
|
||||
@@ -204,7 +205,7 @@ func (r *colorResolver) resolveColor(key string, value any) (any, error) {
|
||||
func (r *colorResolver) resolveColorValue(value any) (any, error) {
|
||||
switch v := value.(type) {
|
||||
case string:
|
||||
if strings.HasPrefix(v, "#") {
|
||||
if strings.HasPrefix(v, "#") || v == "none" {
|
||||
return v, nil
|
||||
}
|
||||
return r.resolveReference(v)
|
||||
@@ -239,6 +240,12 @@ func (r *colorResolver) resolveReference(ref string) (any, error) {
|
||||
func parseResolvedColor(value any) (compat.AdaptiveColor, error) {
|
||||
switch v := value.(type) {
|
||||
case string:
|
||||
if v == "none" {
|
||||
return compat.AdaptiveColor{
|
||||
Dark: lipgloss.NoColor{},
|
||||
Light: lipgloss.NoColor{},
|
||||
}, nil
|
||||
}
|
||||
return compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color(v),
|
||||
Light: lipgloss.Color(v),
|
||||
@@ -276,6 +283,9 @@ func parseResolvedColor(value any) (compat.AdaptiveColor, error) {
|
||||
func parseColorValue(value any) (color.Color, error) {
|
||||
switch v := value.(type) {
|
||||
case string:
|
||||
if v == "none" {
|
||||
return lipgloss.NoColor{}, nil
|
||||
}
|
||||
return lipgloss.Color(v), nil
|
||||
case float64:
|
||||
return lipgloss.Color(fmt.Sprintf("%d", int(v))), nil
|
||||
|
||||
@@ -21,7 +21,7 @@ func TestLoadThemesFromJSON(t *testing.T) {
|
||||
}
|
||||
|
||||
// Check for expected themes
|
||||
expectedThemes := []string{"tokyonight", "opencode", "everforest", "ayu", "example"}
|
||||
expectedThemes := []string{"tokyonight", "opencode", "everforest", "ayu"}
|
||||
for _, expected := range expectedThemes {
|
||||
found := slices.Contains(themes, expected)
|
||||
if !found {
|
||||
@@ -43,22 +43,28 @@ func TestLoadThemesFromJSON(t *testing.T) {
|
||||
}
|
||||
|
||||
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")
|
||||
// Load themes first
|
||||
err := LoadThemesFromJSON()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to load themes: %v", err)
|
||||
}
|
||||
|
||||
// Check that brandBlue reference was resolved
|
||||
primary := example.Primary()
|
||||
// Test a theme that uses references (e.g., solarized uses color definitions)
|
||||
solarized := GetTheme("solarized")
|
||||
if solarized == nil {
|
||||
t.Fatal("Failed to get solarized theme")
|
||||
}
|
||||
|
||||
// Check that color references were resolved
|
||||
primary := solarized.Primary()
|
||||
if primary.Dark == nil || primary.Light == nil {
|
||||
t.Error("Primary color (brandBlue reference) not resolved")
|
||||
t.Error("Primary color 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")
|
||||
// Check that all colors are properly resolved
|
||||
text := solarized.Text()
|
||||
if text.Dark == nil || text.Light == nil {
|
||||
t.Error("Text color reference not resolved")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,19 +2,25 @@ package theme
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image/color"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/alecthomas/chroma/v2/styles"
|
||||
"github.com/charmbracelet/lipgloss/v2"
|
||||
"github.com/charmbracelet/lipgloss/v2/compat"
|
||||
"github.com/charmbracelet/x/ansi"
|
||||
)
|
||||
|
||||
// Manager handles theme registration, selection, and retrieval.
|
||||
// It maintains a registry of available themes and tracks the currently active theme.
|
||||
type Manager struct {
|
||||
themes map[string]Theme
|
||||
currentName string
|
||||
mu sync.RWMutex
|
||||
themes map[string]Theme
|
||||
currentName string
|
||||
currentUsesAnsiCache bool // Cache whether current theme uses ANSI colors
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
// Global instance of the theme manager
|
||||
@@ -34,6 +40,7 @@ func RegisterTheme(name string, theme Theme) {
|
||||
// If this is the first theme, make it the default
|
||||
if globalManager.currentName == "" {
|
||||
globalManager.currentName = name
|
||||
globalManager.currentUsesAnsiCache = themeUsesAnsiColors(theme)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,11 +51,13 @@ func SetTheme(name string) error {
|
||||
defer globalManager.mu.Unlock()
|
||||
delete(styles.Registry, "charm")
|
||||
|
||||
if _, exists := globalManager.themes[name]; !exists {
|
||||
theme, exists := globalManager.themes[name]
|
||||
if !exists {
|
||||
return fmt.Errorf("theme '%s' not found", name)
|
||||
}
|
||||
|
||||
globalManager.currentName = name
|
||||
globalManager.currentUsesAnsiCache = themeUsesAnsiColors(theme)
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -84,12 +93,16 @@ 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" {
|
||||
return 1
|
||||
}
|
||||
if a == "system" {
|
||||
return -1
|
||||
} else if b == "system" {
|
||||
return 1
|
||||
}
|
||||
return strings.Compare(a, b)
|
||||
})
|
||||
return names
|
||||
@@ -103,3 +116,114 @@ func GetTheme(name string) Theme {
|
||||
|
||||
return globalManager.themes[name]
|
||||
}
|
||||
|
||||
// UpdateSystemTheme updates the system theme with terminal background info
|
||||
func UpdateSystemTheme(terminalBg color.Color, isDark bool) {
|
||||
globalManager.mu.Lock()
|
||||
defer globalManager.mu.Unlock()
|
||||
|
||||
dynamicTheme := NewSystemTheme(terminalBg, isDark)
|
||||
globalManager.themes["system"] = dynamicTheme
|
||||
if globalManager.currentName == "system" {
|
||||
globalManager.currentUsesAnsiCache = themeUsesAnsiColors(dynamicTheme)
|
||||
}
|
||||
}
|
||||
|
||||
// CurrentThemeUsesAnsiColors returns true if the current theme uses ANSI 0-16 colors
|
||||
func CurrentThemeUsesAnsiColors() bool {
|
||||
// globalManager.mu.RLock()
|
||||
// defer globalManager.mu.RUnlock()
|
||||
|
||||
return globalManager.currentUsesAnsiCache
|
||||
}
|
||||
|
||||
// isAnsiColor checks if a color represents an ANSI 0-16 color
|
||||
func isAnsiColor(c color.Color) bool {
|
||||
if _, ok := c.(lipgloss.NoColor); ok {
|
||||
return false
|
||||
}
|
||||
if _, ok := c.(ansi.BasicColor); ok {
|
||||
return true
|
||||
}
|
||||
|
||||
// For other color types, check if they represent ANSI colors
|
||||
// by examining their string representation
|
||||
if stringer, ok := c.(fmt.Stringer); ok {
|
||||
str := stringer.String()
|
||||
// Check if it's a numeric ANSI color (0-15)
|
||||
if num, err := strconv.Atoi(str); err == nil && num >= 0 && num <= 15 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// adaptiveColorUsesAnsi checks if an AdaptiveColor uses ANSI colors
|
||||
func adaptiveColorUsesAnsi(ac compat.AdaptiveColor) bool {
|
||||
if isAnsiColor(ac.Dark) {
|
||||
return true
|
||||
}
|
||||
if isAnsiColor(ac.Light) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// themeUsesAnsiColors checks if a theme uses any ANSI 0-16 colors
|
||||
func themeUsesAnsiColors(theme Theme) bool {
|
||||
if theme == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return adaptiveColorUsesAnsi(theme.Primary()) ||
|
||||
adaptiveColorUsesAnsi(theme.Secondary()) ||
|
||||
adaptiveColorUsesAnsi(theme.Accent()) ||
|
||||
adaptiveColorUsesAnsi(theme.Error()) ||
|
||||
adaptiveColorUsesAnsi(theme.Warning()) ||
|
||||
adaptiveColorUsesAnsi(theme.Success()) ||
|
||||
adaptiveColorUsesAnsi(theme.Info()) ||
|
||||
adaptiveColorUsesAnsi(theme.Text()) ||
|
||||
adaptiveColorUsesAnsi(theme.TextMuted()) ||
|
||||
adaptiveColorUsesAnsi(theme.Background()) ||
|
||||
adaptiveColorUsesAnsi(theme.BackgroundPanel()) ||
|
||||
adaptiveColorUsesAnsi(theme.BackgroundElement()) ||
|
||||
adaptiveColorUsesAnsi(theme.Border()) ||
|
||||
adaptiveColorUsesAnsi(theme.BorderActive()) ||
|
||||
adaptiveColorUsesAnsi(theme.BorderSubtle()) ||
|
||||
adaptiveColorUsesAnsi(theme.DiffAdded()) ||
|
||||
adaptiveColorUsesAnsi(theme.DiffRemoved()) ||
|
||||
adaptiveColorUsesAnsi(theme.DiffContext()) ||
|
||||
adaptiveColorUsesAnsi(theme.DiffHunkHeader()) ||
|
||||
adaptiveColorUsesAnsi(theme.DiffHighlightAdded()) ||
|
||||
adaptiveColorUsesAnsi(theme.DiffHighlightRemoved()) ||
|
||||
adaptiveColorUsesAnsi(theme.DiffAddedBg()) ||
|
||||
adaptiveColorUsesAnsi(theme.DiffRemovedBg()) ||
|
||||
adaptiveColorUsesAnsi(theme.DiffContextBg()) ||
|
||||
adaptiveColorUsesAnsi(theme.DiffLineNumber()) ||
|
||||
adaptiveColorUsesAnsi(theme.DiffAddedLineNumberBg()) ||
|
||||
adaptiveColorUsesAnsi(theme.DiffRemovedLineNumberBg()) ||
|
||||
adaptiveColorUsesAnsi(theme.MarkdownText()) ||
|
||||
adaptiveColorUsesAnsi(theme.MarkdownHeading()) ||
|
||||
adaptiveColorUsesAnsi(theme.MarkdownLink()) ||
|
||||
adaptiveColorUsesAnsi(theme.MarkdownLinkText()) ||
|
||||
adaptiveColorUsesAnsi(theme.MarkdownCode()) ||
|
||||
adaptiveColorUsesAnsi(theme.MarkdownBlockQuote()) ||
|
||||
adaptiveColorUsesAnsi(theme.MarkdownEmph()) ||
|
||||
adaptiveColorUsesAnsi(theme.MarkdownStrong()) ||
|
||||
adaptiveColorUsesAnsi(theme.MarkdownHorizontalRule()) ||
|
||||
adaptiveColorUsesAnsi(theme.MarkdownListItem()) ||
|
||||
adaptiveColorUsesAnsi(theme.MarkdownListEnumeration()) ||
|
||||
adaptiveColorUsesAnsi(theme.MarkdownImage()) ||
|
||||
adaptiveColorUsesAnsi(theme.MarkdownImageText()) ||
|
||||
adaptiveColorUsesAnsi(theme.MarkdownCodeBlock()) ||
|
||||
adaptiveColorUsesAnsi(theme.SyntaxComment()) ||
|
||||
adaptiveColorUsesAnsi(theme.SyntaxKeyword()) ||
|
||||
adaptiveColorUsesAnsi(theme.SyntaxFunction()) ||
|
||||
adaptiveColorUsesAnsi(theme.SyntaxVariable()) ||
|
||||
adaptiveColorUsesAnsi(theme.SyntaxString()) ||
|
||||
adaptiveColorUsesAnsi(theme.SyntaxNumber()) ||
|
||||
adaptiveColorUsesAnsi(theme.SyntaxType()) ||
|
||||
adaptiveColorUsesAnsi(theme.SyntaxOperator()) ||
|
||||
adaptiveColorUsesAnsi(theme.SyntaxPunctuation())
|
||||
}
|
||||
|
||||
@@ -0,0 +1,299 @@
|
||||
package theme
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image/color"
|
||||
"math"
|
||||
|
||||
"github.com/charmbracelet/lipgloss/v2"
|
||||
"github.com/charmbracelet/lipgloss/v2/compat"
|
||||
)
|
||||
|
||||
// SystemTheme is a dynamic theme that derives its gray scale colors
|
||||
// from the terminal's background color at runtime
|
||||
type SystemTheme struct {
|
||||
BaseTheme
|
||||
terminalBg color.Color
|
||||
terminalBgIsDark bool
|
||||
}
|
||||
|
||||
// NewSystemTheme creates a new instance of the dynamic system theme
|
||||
func NewSystemTheme(terminalBg color.Color, isDark bool) *SystemTheme {
|
||||
theme := &SystemTheme{
|
||||
terminalBg: terminalBg,
|
||||
terminalBgIsDark: isDark,
|
||||
}
|
||||
theme.initializeColors()
|
||||
return theme
|
||||
}
|
||||
|
||||
// initializeColors sets up all theme colors
|
||||
func (t *SystemTheme) initializeColors() {
|
||||
// Generate gray scale based on terminal background
|
||||
grays := t.generateGrayScale()
|
||||
|
||||
// Set ANSI colors for primary colors
|
||||
t.PrimaryColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Cyan,
|
||||
Light: lipgloss.Cyan,
|
||||
}
|
||||
t.SecondaryColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Magenta,
|
||||
Light: lipgloss.Magenta,
|
||||
}
|
||||
t.AccentColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Cyan,
|
||||
Light: lipgloss.Cyan,
|
||||
}
|
||||
|
||||
// Status colors using ANSI
|
||||
t.ErrorColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Red,
|
||||
Light: lipgloss.Red,
|
||||
}
|
||||
t.WarningColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Yellow,
|
||||
Light: lipgloss.Yellow,
|
||||
}
|
||||
t.SuccessColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Green,
|
||||
Light: lipgloss.Green,
|
||||
}
|
||||
t.InfoColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Cyan,
|
||||
Light: lipgloss.Cyan,
|
||||
}
|
||||
|
||||
// Text colors
|
||||
t.TextColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.NoColor{},
|
||||
Light: lipgloss.NoColor{},
|
||||
}
|
||||
// Derive muted text color from terminal foreground
|
||||
t.TextMutedColor = t.generateMutedTextColor()
|
||||
|
||||
// Background colors
|
||||
t.BackgroundColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.NoColor{},
|
||||
Light: lipgloss.NoColor{},
|
||||
}
|
||||
t.BackgroundPanelColor = grays[2]
|
||||
t.BackgroundElementColor = grays[3]
|
||||
|
||||
// Border colors
|
||||
t.BorderSubtleColor = grays[6]
|
||||
t.BorderColor = grays[7]
|
||||
t.BorderActiveColor = grays[8]
|
||||
|
||||
// Diff colors using ANSI colors
|
||||
t.DiffAddedColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color("2"), // green
|
||||
Light: lipgloss.Color("2"),
|
||||
}
|
||||
t.DiffRemovedColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color("1"), // red
|
||||
Light: lipgloss.Color("1"),
|
||||
}
|
||||
t.DiffContextColor = grays[7] // Use gray for context
|
||||
t.DiffHunkHeaderColor = grays[7]
|
||||
t.DiffHighlightAddedColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color("2"), // green
|
||||
Light: lipgloss.Color("2"),
|
||||
}
|
||||
t.DiffHighlightRemovedColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color("1"), // red
|
||||
Light: lipgloss.Color("1"),
|
||||
}
|
||||
// Use subtle gray backgrounds for diff
|
||||
t.DiffAddedBgColor = grays[2]
|
||||
t.DiffRemovedBgColor = grays[2]
|
||||
t.DiffContextBgColor = grays[1]
|
||||
t.DiffLineNumberColor = grays[6]
|
||||
t.DiffAddedLineNumberBgColor = grays[3]
|
||||
t.DiffRemovedLineNumberBgColor = grays[3]
|
||||
|
||||
// Markdown colors using ANSI
|
||||
t.MarkdownTextColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.NoColor{},
|
||||
Light: lipgloss.NoColor{},
|
||||
}
|
||||
t.MarkdownHeadingColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.NoColor{},
|
||||
Light: lipgloss.NoColor{},
|
||||
}
|
||||
t.MarkdownLinkColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color("4"), // blue
|
||||
Light: lipgloss.Color("4"),
|
||||
}
|
||||
t.MarkdownLinkTextColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color("6"), // cyan
|
||||
Light: lipgloss.Color("6"),
|
||||
}
|
||||
t.MarkdownCodeColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color("2"), // green
|
||||
Light: lipgloss.Color("2"),
|
||||
}
|
||||
t.MarkdownBlockQuoteColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color("3"), // yellow
|
||||
Light: lipgloss.Color("3"),
|
||||
}
|
||||
t.MarkdownEmphColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color("3"), // yellow
|
||||
Light: lipgloss.Color("3"),
|
||||
}
|
||||
t.MarkdownStrongColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.NoColor{},
|
||||
Light: lipgloss.NoColor{},
|
||||
}
|
||||
t.MarkdownHorizontalRuleColor = t.BorderColor
|
||||
t.MarkdownListItemColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color("4"), // blue
|
||||
Light: lipgloss.Color("4"),
|
||||
}
|
||||
t.MarkdownListEnumerationColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color("6"), // cyan
|
||||
Light: lipgloss.Color("6"),
|
||||
}
|
||||
t.MarkdownImageColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color("4"), // blue
|
||||
Light: lipgloss.Color("4"),
|
||||
}
|
||||
t.MarkdownImageTextColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color("6"), // cyan
|
||||
Light: lipgloss.Color("6"),
|
||||
}
|
||||
t.MarkdownCodeBlockColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.NoColor{},
|
||||
Light: lipgloss.NoColor{},
|
||||
}
|
||||
|
||||
// Syntax colors
|
||||
t.SyntaxCommentColor = t.TextMutedColor // Use same as muted text
|
||||
t.SyntaxKeywordColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color("5"), // magenta
|
||||
Light: lipgloss.Color("5"),
|
||||
}
|
||||
t.SyntaxFunctionColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color("4"), // blue
|
||||
Light: lipgloss.Color("4"),
|
||||
}
|
||||
t.SyntaxVariableColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.NoColor{},
|
||||
Light: lipgloss.NoColor{},
|
||||
}
|
||||
t.SyntaxStringColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color("2"), // green
|
||||
Light: lipgloss.Color("2"),
|
||||
}
|
||||
t.SyntaxNumberColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color("3"), // yellow
|
||||
Light: lipgloss.Color("3"),
|
||||
}
|
||||
t.SyntaxTypeColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color("6"), // cyan
|
||||
Light: lipgloss.Color("6"),
|
||||
}
|
||||
t.SyntaxOperatorColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color("6"), // cyan
|
||||
Light: lipgloss.Color("6"),
|
||||
}
|
||||
t.SyntaxPunctuationColor = compat.AdaptiveColor{
|
||||
Dark: lipgloss.NoColor{},
|
||||
Light: lipgloss.NoColor{},
|
||||
}
|
||||
}
|
||||
|
||||
// generateGrayScale creates a gray scale based on the terminal background
|
||||
func (t *SystemTheme) generateGrayScale() map[int]compat.AdaptiveColor {
|
||||
grays := make(map[int]compat.AdaptiveColor)
|
||||
|
||||
r, g, b, _ := t.terminalBg.RGBA()
|
||||
bgR := float64(r >> 8)
|
||||
bgG := float64(g >> 8)
|
||||
bgB := float64(b >> 8)
|
||||
|
||||
luminance := 0.299*bgR + 0.587*bgG + 0.114*bgB
|
||||
|
||||
for i := 1; i <= 12; i++ {
|
||||
var stepColor string
|
||||
factor := float64(i) / 12.0
|
||||
|
||||
if t.terminalBgIsDark {
|
||||
if luminance < 10 {
|
||||
grayValue := int(factor * 0.4 * 255)
|
||||
stepColor = fmt.Sprintf("#%02x%02x%02x", grayValue, grayValue, grayValue)
|
||||
} else {
|
||||
newLum := luminance + (255-luminance)*factor*0.4
|
||||
|
||||
ratio := newLum / luminance
|
||||
newR := math.Min(bgR*ratio, 255)
|
||||
newG := math.Min(bgG*ratio, 255)
|
||||
newB := math.Min(bgB*ratio, 255)
|
||||
|
||||
stepColor = fmt.Sprintf("#%02x%02x%02x", int(newR), int(newG), int(newB))
|
||||
}
|
||||
} else {
|
||||
if luminance > 245 {
|
||||
grayValue := int(255 - factor*0.4*255)
|
||||
stepColor = fmt.Sprintf("#%02x%02x%02x", grayValue, grayValue, grayValue)
|
||||
} else {
|
||||
newLum := luminance * (1 - factor*0.4)
|
||||
|
||||
ratio := newLum / luminance
|
||||
newR := math.Max(bgR*ratio, 0)
|
||||
newG := math.Max(bgG*ratio, 0)
|
||||
newB := math.Max(bgB*ratio, 0)
|
||||
|
||||
stepColor = fmt.Sprintf("#%02x%02x%02x", int(newR), int(newG), int(newB))
|
||||
}
|
||||
}
|
||||
|
||||
grays[i] = compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color(stepColor),
|
||||
Light: lipgloss.Color(stepColor),
|
||||
}
|
||||
}
|
||||
|
||||
return grays
|
||||
}
|
||||
|
||||
// generateMutedTextColor creates a muted gray color based on the terminal background
|
||||
func (t *SystemTheme) generateMutedTextColor() compat.AdaptiveColor {
|
||||
bgR, bgG, bgB, _ := t.terminalBg.RGBA()
|
||||
|
||||
bgRf := float64(bgR >> 8)
|
||||
bgGf := float64(bgG >> 8)
|
||||
bgBf := float64(bgB >> 8)
|
||||
|
||||
bgLum := 0.299*bgRf + 0.587*bgGf + 0.114*bgBf
|
||||
|
||||
var grayValue int
|
||||
if t.terminalBgIsDark {
|
||||
if bgLum < 10 {
|
||||
// Very dark/black background
|
||||
// grays[3] would be around #2e (46), so we need much lighter
|
||||
grayValue = 180 // #b4b4b4
|
||||
} else {
|
||||
// Scale up for lighter dark backgrounds
|
||||
// Ensure we're always significantly brighter than BackgroundElement
|
||||
grayValue = min(int(160+(bgLum*0.3)), 200)
|
||||
}
|
||||
} else {
|
||||
if bgLum > 245 {
|
||||
// Very light/white background
|
||||
// grays[3] would be around #f5 (245), so we need much darker
|
||||
grayValue = 75 // #4b4b4b
|
||||
} else {
|
||||
// Scale down for darker light backgrounds
|
||||
// Ensure we're always significantly darker than BackgroundElement
|
||||
grayValue = max(int(100-((255-bgLum)*0.2)), 60)
|
||||
}
|
||||
}
|
||||
|
||||
mutedColor := fmt.Sprintf("#%02x%02x%02x", grayValue, grayValue, grayValue)
|
||||
|
||||
return compat.AdaptiveColor{
|
||||
Dark: lipgloss.Color(mutedColor),
|
||||
Light: lipgloss.Color(mutedColor),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,228 @@
|
||||
{
|
||||
"$schema": "https://opencode.ai/theme.json",
|
||||
"defs": {
|
||||
"background": "#193549",
|
||||
"backgroundAlt": "#122738",
|
||||
"backgroundPanel": "#1f4662",
|
||||
"foreground": "#ffffff",
|
||||
"foregroundMuted": "#adb7c9",
|
||||
"yellow": "#ffc600",
|
||||
"yellowBright": "#ffe14c",
|
||||
"orange": "#ff9d00",
|
||||
"orangeBright": "#ffb454",
|
||||
"mint": "#2affdf",
|
||||
"mintBright": "#7efff5",
|
||||
"blue": "#0088ff",
|
||||
"blueBright": "#5cb7ff",
|
||||
"pink": "#ff628c",
|
||||
"pinkBright": "#ff86a5",
|
||||
"green": "#9eff80",
|
||||
"greenBright": "#b9ff9f",
|
||||
"purple": "#9a5feb",
|
||||
"purpleBright": "#b88cfd",
|
||||
"red": "#ff0088",
|
||||
"redBright": "#ff5fb3"
|
||||
},
|
||||
"theme": {
|
||||
"primary": {
|
||||
"dark": "blue",
|
||||
"light": "#0066cc"
|
||||
},
|
||||
"secondary": {
|
||||
"dark": "purple",
|
||||
"light": "#7c4dff"
|
||||
},
|
||||
"accent": {
|
||||
"dark": "mint",
|
||||
"light": "#00acc1"
|
||||
},
|
||||
"error": {
|
||||
"dark": "red",
|
||||
"light": "#e91e63"
|
||||
},
|
||||
"warning": {
|
||||
"dark": "yellow",
|
||||
"light": "#ff9800"
|
||||
},
|
||||
"success": {
|
||||
"dark": "green",
|
||||
"light": "#4caf50"
|
||||
},
|
||||
"info": {
|
||||
"dark": "orange",
|
||||
"light": "#ff5722"
|
||||
},
|
||||
"text": {
|
||||
"dark": "foreground",
|
||||
"light": "#193549"
|
||||
},
|
||||
"textMuted": {
|
||||
"dark": "foregroundMuted",
|
||||
"light": "#5c6b7d"
|
||||
},
|
||||
"background": {
|
||||
"dark": "#193549",
|
||||
"light": "#ffffff"
|
||||
},
|
||||
"backgroundPanel": {
|
||||
"dark": "#122738",
|
||||
"light": "#f5f7fa"
|
||||
},
|
||||
"backgroundElement": {
|
||||
"dark": "#1f4662",
|
||||
"light": "#e8ecf1"
|
||||
},
|
||||
"border": {
|
||||
"dark": "#1f4662",
|
||||
"light": "#d3dae3"
|
||||
},
|
||||
"borderActive": {
|
||||
"dark": "blue",
|
||||
"light": "#0066cc"
|
||||
},
|
||||
"borderSubtle": {
|
||||
"dark": "#0e1e2e",
|
||||
"light": "#e8ecf1"
|
||||
},
|
||||
"diffAdded": {
|
||||
"dark": "green",
|
||||
"light": "#4caf50"
|
||||
},
|
||||
"diffRemoved": {
|
||||
"dark": "red",
|
||||
"light": "#e91e63"
|
||||
},
|
||||
"diffContext": {
|
||||
"dark": "foregroundMuted",
|
||||
"light": "#5c6b7d"
|
||||
},
|
||||
"diffHunkHeader": {
|
||||
"dark": "mint",
|
||||
"light": "#00acc1"
|
||||
},
|
||||
"diffHighlightAdded": {
|
||||
"dark": "greenBright",
|
||||
"light": "#4caf50"
|
||||
},
|
||||
"diffHighlightRemoved": {
|
||||
"dark": "redBright",
|
||||
"light": "#e91e63"
|
||||
},
|
||||
"diffAddedBg": {
|
||||
"dark": "#1a3a2a",
|
||||
"light": "#e8f5e9"
|
||||
},
|
||||
"diffRemovedBg": {
|
||||
"dark": "#3a1a2a",
|
||||
"light": "#ffebee"
|
||||
},
|
||||
"diffContextBg": {
|
||||
"dark": "#122738",
|
||||
"light": "#f5f7fa"
|
||||
},
|
||||
"diffLineNumber": {
|
||||
"dark": "#2d5a7b",
|
||||
"light": "#b0bec5"
|
||||
},
|
||||
"diffAddedLineNumberBg": {
|
||||
"dark": "#1a3a2a",
|
||||
"light": "#e8f5e9"
|
||||
},
|
||||
"diffRemovedLineNumberBg": {
|
||||
"dark": "#3a1a2a",
|
||||
"light": "#ffebee"
|
||||
},
|
||||
"markdownText": {
|
||||
"dark": "foreground",
|
||||
"light": "#193549"
|
||||
},
|
||||
"markdownHeading": {
|
||||
"dark": "yellow",
|
||||
"light": "#ff9800"
|
||||
},
|
||||
"markdownLink": {
|
||||
"dark": "blue",
|
||||
"light": "#0066cc"
|
||||
},
|
||||
"markdownLinkText": {
|
||||
"dark": "mint",
|
||||
"light": "#00acc1"
|
||||
},
|
||||
"markdownCode": {
|
||||
"dark": "green",
|
||||
"light": "#4caf50"
|
||||
},
|
||||
"markdownBlockQuote": {
|
||||
"dark": "foregroundMuted",
|
||||
"light": "#5c6b7d"
|
||||
},
|
||||
"markdownEmph": {
|
||||
"dark": "orange",
|
||||
"light": "#ff5722"
|
||||
},
|
||||
"markdownStrong": {
|
||||
"dark": "pink",
|
||||
"light": "#e91e63"
|
||||
},
|
||||
"markdownHorizontalRule": {
|
||||
"dark": "#2d5a7b",
|
||||
"light": "#d3dae3"
|
||||
},
|
||||
"markdownListItem": {
|
||||
"dark": "blue",
|
||||
"light": "#0066cc"
|
||||
},
|
||||
"markdownListEnumeration": {
|
||||
"dark": "mint",
|
||||
"light": "#00acc1"
|
||||
},
|
||||
"markdownImage": {
|
||||
"dark": "blue",
|
||||
"light": "#0066cc"
|
||||
},
|
||||
"markdownImageText": {
|
||||
"dark": "mint",
|
||||
"light": "#00acc1"
|
||||
},
|
||||
"markdownCodeBlock": {
|
||||
"dark": "foreground",
|
||||
"light": "#193549"
|
||||
},
|
||||
"syntaxComment": {
|
||||
"dark": "#0088ff",
|
||||
"light": "#5c6b7d"
|
||||
},
|
||||
"syntaxKeyword": {
|
||||
"dark": "orange",
|
||||
"light": "#ff5722"
|
||||
},
|
||||
"syntaxFunction": {
|
||||
"dark": "yellow",
|
||||
"light": "#ff9800"
|
||||
},
|
||||
"syntaxVariable": {
|
||||
"dark": "foreground",
|
||||
"light": "#193549"
|
||||
},
|
||||
"syntaxString": {
|
||||
"dark": "green",
|
||||
"light": "#4caf50"
|
||||
},
|
||||
"syntaxNumber": {
|
||||
"dark": "pink",
|
||||
"light": "#e91e63"
|
||||
},
|
||||
"syntaxType": {
|
||||
"dark": "mint",
|
||||
"light": "#00acc1"
|
||||
},
|
||||
"syntaxOperator": {
|
||||
"dark": "orange",
|
||||
"light": "#ff5722"
|
||||
},
|
||||
"syntaxPunctuation": {
|
||||
"dark": "foreground",
|
||||
"light": "#193549"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,219 @@
|
||||
{
|
||||
"$schema": "https://opencode.ai/theme.json",
|
||||
"defs": {
|
||||
"background": "#282a36",
|
||||
"currentLine": "#44475a",
|
||||
"selection": "#44475a",
|
||||
"foreground": "#f8f8f2",
|
||||
"comment": "#6272a4",
|
||||
"cyan": "#8be9fd",
|
||||
"green": "#50fa7b",
|
||||
"orange": "#ffb86c",
|
||||
"pink": "#ff79c6",
|
||||
"purple": "#bd93f9",
|
||||
"red": "#ff5555",
|
||||
"yellow": "#f1fa8c"
|
||||
},
|
||||
"theme": {
|
||||
"primary": {
|
||||
"dark": "purple",
|
||||
"light": "purple"
|
||||
},
|
||||
"secondary": {
|
||||
"dark": "pink",
|
||||
"light": "pink"
|
||||
},
|
||||
"accent": {
|
||||
"dark": "cyan",
|
||||
"light": "cyan"
|
||||
},
|
||||
"error": {
|
||||
"dark": "red",
|
||||
"light": "red"
|
||||
},
|
||||
"warning": {
|
||||
"dark": "yellow",
|
||||
"light": "yellow"
|
||||
},
|
||||
"success": {
|
||||
"dark": "green",
|
||||
"light": "green"
|
||||
},
|
||||
"info": {
|
||||
"dark": "orange",
|
||||
"light": "orange"
|
||||
},
|
||||
"text": {
|
||||
"dark": "foreground",
|
||||
"light": "#282a36"
|
||||
},
|
||||
"textMuted": {
|
||||
"dark": "comment",
|
||||
"light": "#6272a4"
|
||||
},
|
||||
"background": {
|
||||
"dark": "#282a36",
|
||||
"light": "#f8f8f2"
|
||||
},
|
||||
"backgroundPanel": {
|
||||
"dark": "#21222c",
|
||||
"light": "#e8e8e2"
|
||||
},
|
||||
"backgroundElement": {
|
||||
"dark": "currentLine",
|
||||
"light": "#d8d8d2"
|
||||
},
|
||||
"border": {
|
||||
"dark": "currentLine",
|
||||
"light": "#c8c8c2"
|
||||
},
|
||||
"borderActive": {
|
||||
"dark": "purple",
|
||||
"light": "purple"
|
||||
},
|
||||
"borderSubtle": {
|
||||
"dark": "#191a21",
|
||||
"light": "#e0e0e0"
|
||||
},
|
||||
"diffAdded": {
|
||||
"dark": "green",
|
||||
"light": "green"
|
||||
},
|
||||
"diffRemoved": {
|
||||
"dark": "red",
|
||||
"light": "red"
|
||||
},
|
||||
"diffContext": {
|
||||
"dark": "comment",
|
||||
"light": "#6272a4"
|
||||
},
|
||||
"diffHunkHeader": {
|
||||
"dark": "comment",
|
||||
"light": "#6272a4"
|
||||
},
|
||||
"diffHighlightAdded": {
|
||||
"dark": "green",
|
||||
"light": "green"
|
||||
},
|
||||
"diffHighlightRemoved": {
|
||||
"dark": "red",
|
||||
"light": "red"
|
||||
},
|
||||
"diffAddedBg": {
|
||||
"dark": "#1a3a1a",
|
||||
"light": "#e0ffe0"
|
||||
},
|
||||
"diffRemovedBg": {
|
||||
"dark": "#3a1a1a",
|
||||
"light": "#ffe0e0"
|
||||
},
|
||||
"diffContextBg": {
|
||||
"dark": "#21222c",
|
||||
"light": "#e8e8e2"
|
||||
},
|
||||
"diffLineNumber": {
|
||||
"dark": "currentLine",
|
||||
"light": "#c8c8c2"
|
||||
},
|
||||
"diffAddedLineNumberBg": {
|
||||
"dark": "#1a3a1a",
|
||||
"light": "#e0ffe0"
|
||||
},
|
||||
"diffRemovedLineNumberBg": {
|
||||
"dark": "#3a1a1a",
|
||||
"light": "#ffe0e0"
|
||||
},
|
||||
"markdownText": {
|
||||
"dark": "foreground",
|
||||
"light": "#282a36"
|
||||
},
|
||||
"markdownHeading": {
|
||||
"dark": "purple",
|
||||
"light": "purple"
|
||||
},
|
||||
"markdownLink": {
|
||||
"dark": "cyan",
|
||||
"light": "cyan"
|
||||
},
|
||||
"markdownLinkText": {
|
||||
"dark": "pink",
|
||||
"light": "pink"
|
||||
},
|
||||
"markdownCode": {
|
||||
"dark": "green",
|
||||
"light": "green"
|
||||
},
|
||||
"markdownBlockQuote": {
|
||||
"dark": "comment",
|
||||
"light": "#6272a4"
|
||||
},
|
||||
"markdownEmph": {
|
||||
"dark": "yellow",
|
||||
"light": "yellow"
|
||||
},
|
||||
"markdownStrong": {
|
||||
"dark": "orange",
|
||||
"light": "orange"
|
||||
},
|
||||
"markdownHorizontalRule": {
|
||||
"dark": "comment",
|
||||
"light": "#6272a4"
|
||||
},
|
||||
"markdownListItem": {
|
||||
"dark": "purple",
|
||||
"light": "purple"
|
||||
},
|
||||
"markdownListEnumeration": {
|
||||
"dark": "cyan",
|
||||
"light": "cyan"
|
||||
},
|
||||
"markdownImage": {
|
||||
"dark": "cyan",
|
||||
"light": "cyan"
|
||||
},
|
||||
"markdownImageText": {
|
||||
"dark": "pink",
|
||||
"light": "pink"
|
||||
},
|
||||
"markdownCodeBlock": {
|
||||
"dark": "foreground",
|
||||
"light": "#282a36"
|
||||
},
|
||||
"syntaxComment": {
|
||||
"dark": "comment",
|
||||
"light": "#6272a4"
|
||||
},
|
||||
"syntaxKeyword": {
|
||||
"dark": "pink",
|
||||
"light": "pink"
|
||||
},
|
||||
"syntaxFunction": {
|
||||
"dark": "green",
|
||||
"light": "green"
|
||||
},
|
||||
"syntaxVariable": {
|
||||
"dark": "foreground",
|
||||
"light": "#282a36"
|
||||
},
|
||||
"syntaxString": {
|
||||
"dark": "yellow",
|
||||
"light": "yellow"
|
||||
},
|
||||
"syntaxNumber": {
|
||||
"dark": "purple",
|
||||
"light": "purple"
|
||||
},
|
||||
"syntaxType": {
|
||||
"dark": "cyan",
|
||||
"light": "cyan"
|
||||
},
|
||||
"syntaxOperator": {
|
||||
"dark": "pink",
|
||||
"light": "pink"
|
||||
},
|
||||
"syntaxPunctuation": {
|
||||
"dark": "foreground",
|
||||
"light": "#282a36"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,233 @@
|
||||
{
|
||||
"$schema": "https://opencode.ai/theme.json",
|
||||
"defs": {
|
||||
"darkBg": "#0d1117",
|
||||
"darkBgAlt": "#010409",
|
||||
"darkBgPanel": "#161b22",
|
||||
"darkFg": "#c9d1d9",
|
||||
"darkFgMuted": "#8b949e",
|
||||
"darkBlue": "#58a6ff",
|
||||
"darkGreen": "#3fb950",
|
||||
"darkRed": "#f85149",
|
||||
"darkOrange": "#d29922",
|
||||
"darkPurple": "#bc8cff",
|
||||
"darkPink": "#ff7b72",
|
||||
"darkYellow": "#e3b341",
|
||||
"darkCyan": "#39c5cf",
|
||||
"lightBg": "#ffffff",
|
||||
"lightBgAlt": "#f6f8fa",
|
||||
"lightBgPanel": "#f0f3f6",
|
||||
"lightFg": "#24292f",
|
||||
"lightFgMuted": "#57606a",
|
||||
"lightBlue": "#0969da",
|
||||
"lightGreen": "#1a7f37",
|
||||
"lightRed": "#cf222e",
|
||||
"lightOrange": "#bc4c00",
|
||||
"lightPurple": "#8250df",
|
||||
"lightPink": "#bf3989",
|
||||
"lightYellow": "#9a6700",
|
||||
"lightCyan": "#1b7c83"
|
||||
},
|
||||
"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": "#30363d",
|
||||
"light": "#d0d7de"
|
||||
},
|
||||
"borderActive": {
|
||||
"dark": "darkBlue",
|
||||
"light": "lightBlue"
|
||||
},
|
||||
"borderSubtle": {
|
||||
"dark": "#21262d",
|
||||
"light": "#d8dee4"
|
||||
},
|
||||
"diffAdded": {
|
||||
"dark": "darkGreen",
|
||||
"light": "lightGreen"
|
||||
},
|
||||
"diffRemoved": {
|
||||
"dark": "darkRed",
|
||||
"light": "lightRed"
|
||||
},
|
||||
"diffContext": {
|
||||
"dark": "darkFgMuted",
|
||||
"light": "lightFgMuted"
|
||||
},
|
||||
"diffHunkHeader": {
|
||||
"dark": "darkBlue",
|
||||
"light": "lightBlue"
|
||||
},
|
||||
"diffHighlightAdded": {
|
||||
"dark": "#3fb950",
|
||||
"light": "#1a7f37"
|
||||
},
|
||||
"diffHighlightRemoved": {
|
||||
"dark": "#f85149",
|
||||
"light": "#cf222e"
|
||||
},
|
||||
"diffAddedBg": {
|
||||
"dark": "#033a16",
|
||||
"light": "#dafbe1"
|
||||
},
|
||||
"diffRemovedBg": {
|
||||
"dark": "#67060c",
|
||||
"light": "#ffebe9"
|
||||
},
|
||||
"diffContextBg": {
|
||||
"dark": "darkBgAlt",
|
||||
"light": "lightBgAlt"
|
||||
},
|
||||
"diffLineNumber": {
|
||||
"dark": "#484f58",
|
||||
"light": "#afb8c1"
|
||||
},
|
||||
"diffAddedLineNumberBg": {
|
||||
"dark": "#033a16",
|
||||
"light": "#dafbe1"
|
||||
},
|
||||
"diffRemovedLineNumberBg": {
|
||||
"dark": "#67060c",
|
||||
"light": "#ffebe9"
|
||||
},
|
||||
"markdownText": {
|
||||
"dark": "darkFg",
|
||||
"light": "lightFg"
|
||||
},
|
||||
"markdownHeading": {
|
||||
"dark": "darkBlue",
|
||||
"light": "lightBlue"
|
||||
},
|
||||
"markdownLink": {
|
||||
"dark": "darkBlue",
|
||||
"light": "lightBlue"
|
||||
},
|
||||
"markdownLinkText": {
|
||||
"dark": "darkCyan",
|
||||
"light": "lightCyan"
|
||||
},
|
||||
"markdownCode": {
|
||||
"dark": "darkPink",
|
||||
"light": "lightPink"
|
||||
},
|
||||
"markdownBlockQuote": {
|
||||
"dark": "darkFgMuted",
|
||||
"light": "lightFgMuted"
|
||||
},
|
||||
"markdownEmph": {
|
||||
"dark": "darkYellow",
|
||||
"light": "lightYellow"
|
||||
},
|
||||
"markdownStrong": {
|
||||
"dark": "darkOrange",
|
||||
"light": "lightOrange"
|
||||
},
|
||||
"markdownHorizontalRule": {
|
||||
"dark": "#30363d",
|
||||
"light": "#d0d7de"
|
||||
},
|
||||
"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": "darkPink",
|
||||
"light": "lightRed"
|
||||
},
|
||||
"syntaxFunction": {
|
||||
"dark": "darkPurple",
|
||||
"light": "lightPurple"
|
||||
},
|
||||
"syntaxVariable": {
|
||||
"dark": "darkOrange",
|
||||
"light": "lightOrange"
|
||||
},
|
||||
"syntaxString": {
|
||||
"dark": "darkCyan",
|
||||
"light": "lightBlue"
|
||||
},
|
||||
"syntaxNumber": {
|
||||
"dark": "darkBlue",
|
||||
"light": "lightCyan"
|
||||
},
|
||||
"syntaxType": {
|
||||
"dark": "darkOrange",
|
||||
"light": "lightOrange"
|
||||
},
|
||||
"syntaxOperator": {
|
||||
"dark": "darkPink",
|
||||
"light": "lightRed"
|
||||
},
|
||||
"syntaxPunctuation": {
|
||||
"dark": "darkFg",
|
||||
"light": "lightFg"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,235 @@
|
||||
{
|
||||
"$schema": "https://opencode.ai/theme.json",
|
||||
"defs": {
|
||||
"darkBg": "#263238",
|
||||
"darkBgAlt": "#1e272c",
|
||||
"darkBgPanel": "#37474f",
|
||||
"darkFg": "#eeffff",
|
||||
"darkFgMuted": "#546e7a",
|
||||
"darkRed": "#f07178",
|
||||
"darkPink": "#f78c6c",
|
||||
"darkOrange": "#ffcb6b",
|
||||
"darkYellow": "#ffcb6b",
|
||||
"darkGreen": "#c3e88d",
|
||||
"darkCyan": "#89ddff",
|
||||
"darkBlue": "#82aaff",
|
||||
"darkPurple": "#c792ea",
|
||||
"darkViolet": "#bb80b3",
|
||||
"lightBg": "#fafafa",
|
||||
"lightBgAlt": "#f5f5f5",
|
||||
"lightBgPanel": "#e7e7e8",
|
||||
"lightFg": "#263238",
|
||||
"lightFgMuted": "#90a4ae",
|
||||
"lightRed": "#e53935",
|
||||
"lightPink": "#ec407a",
|
||||
"lightOrange": "#f4511e",
|
||||
"lightYellow": "#ffb300",
|
||||
"lightGreen": "#91b859",
|
||||
"lightCyan": "#39adb5",
|
||||
"lightBlue": "#6182b8",
|
||||
"lightPurple": "#7c4dff",
|
||||
"lightViolet": "#945eb8"
|
||||
},
|
||||
"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": "#37474f",
|
||||
"light": "#e0e0e0"
|
||||
},
|
||||
"borderActive": {
|
||||
"dark": "darkBlue",
|
||||
"light": "lightBlue"
|
||||
},
|
||||
"borderSubtle": {
|
||||
"dark": "#1e272c",
|
||||
"light": "#eeeeee"
|
||||
},
|
||||
"diffAdded": {
|
||||
"dark": "darkGreen",
|
||||
"light": "lightGreen"
|
||||
},
|
||||
"diffRemoved": {
|
||||
"dark": "darkRed",
|
||||
"light": "lightRed"
|
||||
},
|
||||
"diffContext": {
|
||||
"dark": "darkFgMuted",
|
||||
"light": "lightFgMuted"
|
||||
},
|
||||
"diffHunkHeader": {
|
||||
"dark": "darkCyan",
|
||||
"light": "lightCyan"
|
||||
},
|
||||
"diffHighlightAdded": {
|
||||
"dark": "darkGreen",
|
||||
"light": "lightGreen"
|
||||
},
|
||||
"diffHighlightRemoved": {
|
||||
"dark": "darkRed",
|
||||
"light": "lightRed"
|
||||
},
|
||||
"diffAddedBg": {
|
||||
"dark": "#2e3c2b",
|
||||
"light": "#e8f5e9"
|
||||
},
|
||||
"diffRemovedBg": {
|
||||
"dark": "#3c2b2b",
|
||||
"light": "#ffebee"
|
||||
},
|
||||
"diffContextBg": {
|
||||
"dark": "darkBgAlt",
|
||||
"light": "lightBgAlt"
|
||||
},
|
||||
"diffLineNumber": {
|
||||
"dark": "#37474f",
|
||||
"light": "#cfd8dc"
|
||||
},
|
||||
"diffAddedLineNumberBg": {
|
||||
"dark": "#2e3c2b",
|
||||
"light": "#e8f5e9"
|
||||
},
|
||||
"diffRemovedLineNumberBg": {
|
||||
"dark": "#3c2b2b",
|
||||
"light": "#ffebee"
|
||||
},
|
||||
"markdownText": {
|
||||
"dark": "darkFg",
|
||||
"light": "lightFg"
|
||||
},
|
||||
"markdownHeading": {
|
||||
"dark": "darkBlue",
|
||||
"light": "lightBlue"
|
||||
},
|
||||
"markdownLink": {
|
||||
"dark": "darkCyan",
|
||||
"light": "lightCyan"
|
||||
},
|
||||
"markdownLinkText": {
|
||||
"dark": "darkPurple",
|
||||
"light": "lightPurple"
|
||||
},
|
||||
"markdownCode": {
|
||||
"dark": "darkGreen",
|
||||
"light": "lightGreen"
|
||||
},
|
||||
"markdownBlockQuote": {
|
||||
"dark": "darkFgMuted",
|
||||
"light": "lightFgMuted"
|
||||
},
|
||||
"markdownEmph": {
|
||||
"dark": "darkYellow",
|
||||
"light": "lightYellow"
|
||||
},
|
||||
"markdownStrong": {
|
||||
"dark": "darkOrange",
|
||||
"light": "lightOrange"
|
||||
},
|
||||
"markdownHorizontalRule": {
|
||||
"dark": "#37474f",
|
||||
"light": "#e0e0e0"
|
||||
},
|
||||
"markdownListItem": {
|
||||
"dark": "darkBlue",
|
||||
"light": "lightBlue"
|
||||
},
|
||||
"markdownListEnumeration": {
|
||||
"dark": "darkCyan",
|
||||
"light": "lightCyan"
|
||||
},
|
||||
"markdownImage": {
|
||||
"dark": "darkCyan",
|
||||
"light": "lightCyan"
|
||||
},
|
||||
"markdownImageText": {
|
||||
"dark": "darkPurple",
|
||||
"light": "lightPurple"
|
||||
},
|
||||
"markdownCodeBlock": {
|
||||
"dark": "darkFg",
|
||||
"light": "lightFg"
|
||||
},
|
||||
"syntaxComment": {
|
||||
"dark": "darkFgMuted",
|
||||
"light": "lightFgMuted"
|
||||
},
|
||||
"syntaxKeyword": {
|
||||
"dark": "darkPurple",
|
||||
"light": "lightPurple"
|
||||
},
|
||||
"syntaxFunction": {
|
||||
"dark": "darkBlue",
|
||||
"light": "lightBlue"
|
||||
},
|
||||
"syntaxVariable": {
|
||||
"dark": "darkFg",
|
||||
"light": "lightFg"
|
||||
},
|
||||
"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,221 @@
|
||||
{
|
||||
"$schema": "https://opencode.ai/theme.json",
|
||||
"defs": {
|
||||
"background": "#272822",
|
||||
"backgroundAlt": "#1e1f1c",
|
||||
"backgroundPanel": "#3e3d32",
|
||||
"foreground": "#f8f8f2",
|
||||
"comment": "#75715e",
|
||||
"red": "#f92672",
|
||||
"orange": "#fd971f",
|
||||
"lightOrange": "#e69f66",
|
||||
"yellow": "#e6db74",
|
||||
"green": "#a6e22e",
|
||||
"cyan": "#66d9ef",
|
||||
"blue": "#66d9ef",
|
||||
"purple": "#ae81ff",
|
||||
"pink": "#f92672"
|
||||
},
|
||||
"theme": {
|
||||
"primary": {
|
||||
"dark": "cyan",
|
||||
"light": "blue"
|
||||
},
|
||||
"secondary": {
|
||||
"dark": "purple",
|
||||
"light": "purple"
|
||||
},
|
||||
"accent": {
|
||||
"dark": "green",
|
||||
"light": "green"
|
||||
},
|
||||
"error": {
|
||||
"dark": "red",
|
||||
"light": "red"
|
||||
},
|
||||
"warning": {
|
||||
"dark": "yellow",
|
||||
"light": "orange"
|
||||
},
|
||||
"success": {
|
||||
"dark": "green",
|
||||
"light": "green"
|
||||
},
|
||||
"info": {
|
||||
"dark": "orange",
|
||||
"light": "orange"
|
||||
},
|
||||
"text": {
|
||||
"dark": "foreground",
|
||||
"light": "#272822"
|
||||
},
|
||||
"textMuted": {
|
||||
"dark": "comment",
|
||||
"light": "#75715e"
|
||||
},
|
||||
"background": {
|
||||
"dark": "#272822",
|
||||
"light": "#fafafa"
|
||||
},
|
||||
"backgroundPanel": {
|
||||
"dark": "#1e1f1c",
|
||||
"light": "#f0f0f0"
|
||||
},
|
||||
"backgroundElement": {
|
||||
"dark": "#3e3d32",
|
||||
"light": "#e0e0e0"
|
||||
},
|
||||
"border": {
|
||||
"dark": "#3e3d32",
|
||||
"light": "#d0d0d0"
|
||||
},
|
||||
"borderActive": {
|
||||
"dark": "cyan",
|
||||
"light": "blue"
|
||||
},
|
||||
"borderSubtle": {
|
||||
"dark": "#1e1f1c",
|
||||
"light": "#e8e8e8"
|
||||
},
|
||||
"diffAdded": {
|
||||
"dark": "green",
|
||||
"light": "green"
|
||||
},
|
||||
"diffRemoved": {
|
||||
"dark": "red",
|
||||
"light": "red"
|
||||
},
|
||||
"diffContext": {
|
||||
"dark": "comment",
|
||||
"light": "#75715e"
|
||||
},
|
||||
"diffHunkHeader": {
|
||||
"dark": "comment",
|
||||
"light": "#75715e"
|
||||
},
|
||||
"diffHighlightAdded": {
|
||||
"dark": "green",
|
||||
"light": "green"
|
||||
},
|
||||
"diffHighlightRemoved": {
|
||||
"dark": "red",
|
||||
"light": "red"
|
||||
},
|
||||
"diffAddedBg": {
|
||||
"dark": "#1a3a1a",
|
||||
"light": "#e0ffe0"
|
||||
},
|
||||
"diffRemovedBg": {
|
||||
"dark": "#3a1a1a",
|
||||
"light": "#ffe0e0"
|
||||
},
|
||||
"diffContextBg": {
|
||||
"dark": "#1e1f1c",
|
||||
"light": "#f0f0f0"
|
||||
},
|
||||
"diffLineNumber": {
|
||||
"dark": "#3e3d32",
|
||||
"light": "#d0d0d0"
|
||||
},
|
||||
"diffAddedLineNumberBg": {
|
||||
"dark": "#1a3a1a",
|
||||
"light": "#e0ffe0"
|
||||
},
|
||||
"diffRemovedLineNumberBg": {
|
||||
"dark": "#3a1a1a",
|
||||
"light": "#ffe0e0"
|
||||
},
|
||||
"markdownText": {
|
||||
"dark": "foreground",
|
||||
"light": "#272822"
|
||||
},
|
||||
"markdownHeading": {
|
||||
"dark": "pink",
|
||||
"light": "pink"
|
||||
},
|
||||
"markdownLink": {
|
||||
"dark": "cyan",
|
||||
"light": "blue"
|
||||
},
|
||||
"markdownLinkText": {
|
||||
"dark": "purple",
|
||||
"light": "purple"
|
||||
},
|
||||
"markdownCode": {
|
||||
"dark": "green",
|
||||
"light": "green"
|
||||
},
|
||||
"markdownBlockQuote": {
|
||||
"dark": "comment",
|
||||
"light": "#75715e"
|
||||
},
|
||||
"markdownEmph": {
|
||||
"dark": "yellow",
|
||||
"light": "orange"
|
||||
},
|
||||
"markdownStrong": {
|
||||
"dark": "orange",
|
||||
"light": "orange"
|
||||
},
|
||||
"markdownHorizontalRule": {
|
||||
"dark": "comment",
|
||||
"light": "#75715e"
|
||||
},
|
||||
"markdownListItem": {
|
||||
"dark": "cyan",
|
||||
"light": "blue"
|
||||
},
|
||||
"markdownListEnumeration": {
|
||||
"dark": "purple",
|
||||
"light": "purple"
|
||||
},
|
||||
"markdownImage": {
|
||||
"dark": "cyan",
|
||||
"light": "blue"
|
||||
},
|
||||
"markdownImageText": {
|
||||
"dark": "purple",
|
||||
"light": "purple"
|
||||
},
|
||||
"markdownCodeBlock": {
|
||||
"dark": "foreground",
|
||||
"light": "#272822"
|
||||
},
|
||||
"syntaxComment": {
|
||||
"dark": "comment",
|
||||
"light": "#75715e"
|
||||
},
|
||||
"syntaxKeyword": {
|
||||
"dark": "pink",
|
||||
"light": "pink"
|
||||
},
|
||||
"syntaxFunction": {
|
||||
"dark": "green",
|
||||
"light": "green"
|
||||
},
|
||||
"syntaxVariable": {
|
||||
"dark": "foreground",
|
||||
"light": "#272822"
|
||||
},
|
||||
"syntaxString": {
|
||||
"dark": "yellow",
|
||||
"light": "orange"
|
||||
},
|
||||
"syntaxNumber": {
|
||||
"dark": "purple",
|
||||
"light": "purple"
|
||||
},
|
||||
"syntaxType": {
|
||||
"dark": "cyan",
|
||||
"light": "blue"
|
||||
},
|
||||
"syntaxOperator": {
|
||||
"dark": "pink",
|
||||
"light": "pink"
|
||||
},
|
||||
"syntaxPunctuation": {
|
||||
"dark": "foreground",
|
||||
"light": "#272822"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,222 @@
|
||||
{
|
||||
"$schema": "https://opencode.ai/theme.json",
|
||||
"defs": {
|
||||
"background": "#292d3e",
|
||||
"backgroundAlt": "#1e2132",
|
||||
"backgroundPanel": "#32364a",
|
||||
"foreground": "#a6accd",
|
||||
"foregroundBright": "#bfc7d5",
|
||||
"comment": "#676e95",
|
||||
"red": "#f07178",
|
||||
"orange": "#f78c6c",
|
||||
"yellow": "#ffcb6b",
|
||||
"green": "#c3e88d",
|
||||
"cyan": "#89ddff",
|
||||
"blue": "#82aaff",
|
||||
"purple": "#c792ea",
|
||||
"magenta": "#ff5370",
|
||||
"pink": "#f07178"
|
||||
},
|
||||
"theme": {
|
||||
"primary": {
|
||||
"dark": "blue",
|
||||
"light": "#4976eb"
|
||||
},
|
||||
"secondary": {
|
||||
"dark": "purple",
|
||||
"light": "#a854f2"
|
||||
},
|
||||
"accent": {
|
||||
"dark": "cyan",
|
||||
"light": "#00acc1"
|
||||
},
|
||||
"error": {
|
||||
"dark": "red",
|
||||
"light": "#e53935"
|
||||
},
|
||||
"warning": {
|
||||
"dark": "yellow",
|
||||
"light": "#ffb300"
|
||||
},
|
||||
"success": {
|
||||
"dark": "green",
|
||||
"light": "#91b859"
|
||||
},
|
||||
"info": {
|
||||
"dark": "orange",
|
||||
"light": "#f4511e"
|
||||
},
|
||||
"text": {
|
||||
"dark": "foreground",
|
||||
"light": "#292d3e"
|
||||
},
|
||||
"textMuted": {
|
||||
"dark": "comment",
|
||||
"light": "#8796b0"
|
||||
},
|
||||
"background": {
|
||||
"dark": "#292d3e",
|
||||
"light": "#fafafa"
|
||||
},
|
||||
"backgroundPanel": {
|
||||
"dark": "#1e2132",
|
||||
"light": "#f5f5f5"
|
||||
},
|
||||
"backgroundElement": {
|
||||
"dark": "#32364a",
|
||||
"light": "#e7e7e8"
|
||||
},
|
||||
"border": {
|
||||
"dark": "#32364a",
|
||||
"light": "#e0e0e0"
|
||||
},
|
||||
"borderActive": {
|
||||
"dark": "blue",
|
||||
"light": "#4976eb"
|
||||
},
|
||||
"borderSubtle": {
|
||||
"dark": "#1e2132",
|
||||
"light": "#eeeeee"
|
||||
},
|
||||
"diffAdded": {
|
||||
"dark": "green",
|
||||
"light": "#91b859"
|
||||
},
|
||||
"diffRemoved": {
|
||||
"dark": "red",
|
||||
"light": "#e53935"
|
||||
},
|
||||
"diffContext": {
|
||||
"dark": "comment",
|
||||
"light": "#8796b0"
|
||||
},
|
||||
"diffHunkHeader": {
|
||||
"dark": "cyan",
|
||||
"light": "#00acc1"
|
||||
},
|
||||
"diffHighlightAdded": {
|
||||
"dark": "green",
|
||||
"light": "#91b859"
|
||||
},
|
||||
"diffHighlightRemoved": {
|
||||
"dark": "red",
|
||||
"light": "#e53935"
|
||||
},
|
||||
"diffAddedBg": {
|
||||
"dark": "#2e3c2b",
|
||||
"light": "#e8f5e9"
|
||||
},
|
||||
"diffRemovedBg": {
|
||||
"dark": "#3c2b2b",
|
||||
"light": "#ffebee"
|
||||
},
|
||||
"diffContextBg": {
|
||||
"dark": "#1e2132",
|
||||
"light": "#f5f5f5"
|
||||
},
|
||||
"diffLineNumber": {
|
||||
"dark": "#444760",
|
||||
"light": "#cfd8dc"
|
||||
},
|
||||
"diffAddedLineNumberBg": {
|
||||
"dark": "#2e3c2b",
|
||||
"light": "#e8f5e9"
|
||||
},
|
||||
"diffRemovedLineNumberBg": {
|
||||
"dark": "#3c2b2b",
|
||||
"light": "#ffebee"
|
||||
},
|
||||
"markdownText": {
|
||||
"dark": "foreground",
|
||||
"light": "#292d3e"
|
||||
},
|
||||
"markdownHeading": {
|
||||
"dark": "purple",
|
||||
"light": "#a854f2"
|
||||
},
|
||||
"markdownLink": {
|
||||
"dark": "blue",
|
||||
"light": "#4976eb"
|
||||
},
|
||||
"markdownLinkText": {
|
||||
"dark": "cyan",
|
||||
"light": "#00acc1"
|
||||
},
|
||||
"markdownCode": {
|
||||
"dark": "green",
|
||||
"light": "#91b859"
|
||||
},
|
||||
"markdownBlockQuote": {
|
||||
"dark": "comment",
|
||||
"light": "#8796b0"
|
||||
},
|
||||
"markdownEmph": {
|
||||
"dark": "yellow",
|
||||
"light": "#ffb300"
|
||||
},
|
||||
"markdownStrong": {
|
||||
"dark": "orange",
|
||||
"light": "#f4511e"
|
||||
},
|
||||
"markdownHorizontalRule": {
|
||||
"dark": "comment",
|
||||
"light": "#8796b0"
|
||||
},
|
||||
"markdownListItem": {
|
||||
"dark": "blue",
|
||||
"light": "#4976eb"
|
||||
},
|
||||
"markdownListEnumeration": {
|
||||
"dark": "cyan",
|
||||
"light": "#00acc1"
|
||||
},
|
||||
"markdownImage": {
|
||||
"dark": "blue",
|
||||
"light": "#4976eb"
|
||||
},
|
||||
"markdownImageText": {
|
||||
"dark": "cyan",
|
||||
"light": "#00acc1"
|
||||
},
|
||||
"markdownCodeBlock": {
|
||||
"dark": "foreground",
|
||||
"light": "#292d3e"
|
||||
},
|
||||
"syntaxComment": {
|
||||
"dark": "comment",
|
||||
"light": "#8796b0"
|
||||
},
|
||||
"syntaxKeyword": {
|
||||
"dark": "purple",
|
||||
"light": "#a854f2"
|
||||
},
|
||||
"syntaxFunction": {
|
||||
"dark": "blue",
|
||||
"light": "#4976eb"
|
||||
},
|
||||
"syntaxVariable": {
|
||||
"dark": "foreground",
|
||||
"light": "#292d3e"
|
||||
},
|
||||
"syntaxString": {
|
||||
"dark": "green",
|
||||
"light": "#91b859"
|
||||
},
|
||||
"syntaxNumber": {
|
||||
"dark": "orange",
|
||||
"light": "#f4511e"
|
||||
},
|
||||
"syntaxType": {
|
||||
"dark": "yellow",
|
||||
"light": "#ffb300"
|
||||
},
|
||||
"syntaxOperator": {
|
||||
"dark": "cyan",
|
||||
"light": "#00acc1"
|
||||
},
|
||||
"syntaxPunctuation": {
|
||||
"dark": "foreground",
|
||||
"light": "#292d3e"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,234 @@
|
||||
{
|
||||
"$schema": "https://opencode.ai/theme.json",
|
||||
"defs": {
|
||||
"base": "#191724",
|
||||
"surface": "#1f1d2e",
|
||||
"overlay": "#26233a",
|
||||
"muted": "#6e6a86",
|
||||
"subtle": "#908caa",
|
||||
"text": "#e0def4",
|
||||
"love": "#eb6f92",
|
||||
"gold": "#f6c177",
|
||||
"rose": "#ebbcba",
|
||||
"pine": "#31748f",
|
||||
"foam": "#9ccfd8",
|
||||
"iris": "#c4a7e7",
|
||||
"highlightLow": "#21202e",
|
||||
"highlightMed": "#403d52",
|
||||
"highlightHigh": "#524f67",
|
||||
"moonBase": "#232136",
|
||||
"moonSurface": "#2a273f",
|
||||
"moonOverlay": "#393552",
|
||||
"moonMuted": "#6e6a86",
|
||||
"moonSubtle": "#908caa",
|
||||
"moonText": "#e0def4",
|
||||
"dawnBase": "#faf4ed",
|
||||
"dawnSurface": "#fffaf3",
|
||||
"dawnOverlay": "#f2e9e1",
|
||||
"dawnMuted": "#9893a5",
|
||||
"dawnSubtle": "#797593",
|
||||
"dawnText": "#575279"
|
||||
},
|
||||
"theme": {
|
||||
"primary": {
|
||||
"dark": "foam",
|
||||
"light": "pine"
|
||||
},
|
||||
"secondary": {
|
||||
"dark": "iris",
|
||||
"light": "#907aa9"
|
||||
},
|
||||
"accent": {
|
||||
"dark": "rose",
|
||||
"light": "#d7827e"
|
||||
},
|
||||
"error": {
|
||||
"dark": "love",
|
||||
"light": "#b4637a"
|
||||
},
|
||||
"warning": {
|
||||
"dark": "gold",
|
||||
"light": "#ea9d34"
|
||||
},
|
||||
"success": {
|
||||
"dark": "pine",
|
||||
"light": "#286983"
|
||||
},
|
||||
"info": {
|
||||
"dark": "foam",
|
||||
"light": "#56949f"
|
||||
},
|
||||
"text": {
|
||||
"dark": "#e0def4",
|
||||
"light": "#575279"
|
||||
},
|
||||
"textMuted": {
|
||||
"dark": "muted",
|
||||
"light": "dawnMuted"
|
||||
},
|
||||
"background": {
|
||||
"dark": "base",
|
||||
"light": "dawnBase"
|
||||
},
|
||||
"backgroundPanel": {
|
||||
"dark": "surface",
|
||||
"light": "dawnSurface"
|
||||
},
|
||||
"backgroundElement": {
|
||||
"dark": "overlay",
|
||||
"light": "dawnOverlay"
|
||||
},
|
||||
"border": {
|
||||
"dark": "highlightMed",
|
||||
"light": "#dfdad9"
|
||||
},
|
||||
"borderActive": {
|
||||
"dark": "foam",
|
||||
"light": "pine"
|
||||
},
|
||||
"borderSubtle": {
|
||||
"dark": "highlightLow",
|
||||
"light": "#f4ede8"
|
||||
},
|
||||
"diffAdded": {
|
||||
"dark": "pine",
|
||||
"light": "#286983"
|
||||
},
|
||||
"diffRemoved": {
|
||||
"dark": "love",
|
||||
"light": "#b4637a"
|
||||
},
|
||||
"diffContext": {
|
||||
"dark": "muted",
|
||||
"light": "dawnMuted"
|
||||
},
|
||||
"diffHunkHeader": {
|
||||
"dark": "iris",
|
||||
"light": "#907aa9"
|
||||
},
|
||||
"diffHighlightAdded": {
|
||||
"dark": "pine",
|
||||
"light": "#286983"
|
||||
},
|
||||
"diffHighlightRemoved": {
|
||||
"dark": "love",
|
||||
"light": "#b4637a"
|
||||
},
|
||||
"diffAddedBg": {
|
||||
"dark": "#1f2d3a",
|
||||
"light": "#e5f2f3"
|
||||
},
|
||||
"diffRemovedBg": {
|
||||
"dark": "#3a1f2d",
|
||||
"light": "#fce5e8"
|
||||
},
|
||||
"diffContextBg": {
|
||||
"dark": "surface",
|
||||
"light": "dawnSurface"
|
||||
},
|
||||
"diffLineNumber": {
|
||||
"dark": "muted",
|
||||
"light": "dawnMuted"
|
||||
},
|
||||
"diffAddedLineNumberBg": {
|
||||
"dark": "#1f2d3a",
|
||||
"light": "#e5f2f3"
|
||||
},
|
||||
"diffRemovedLineNumberBg": {
|
||||
"dark": "#3a1f2d",
|
||||
"light": "#fce5e8"
|
||||
},
|
||||
"markdownText": {
|
||||
"dark": "#e0def4",
|
||||
"light": "#575279"
|
||||
},
|
||||
"markdownHeading": {
|
||||
"dark": "iris",
|
||||
"light": "#907aa9"
|
||||
},
|
||||
"markdownLink": {
|
||||
"dark": "foam",
|
||||
"light": "pine"
|
||||
},
|
||||
"markdownLinkText": {
|
||||
"dark": "rose",
|
||||
"light": "#d7827e"
|
||||
},
|
||||
"markdownCode": {
|
||||
"dark": "pine",
|
||||
"light": "#286983"
|
||||
},
|
||||
"markdownBlockQuote": {
|
||||
"dark": "muted",
|
||||
"light": "dawnMuted"
|
||||
},
|
||||
"markdownEmph": {
|
||||
"dark": "gold",
|
||||
"light": "#ea9d34"
|
||||
},
|
||||
"markdownStrong": {
|
||||
"dark": "love",
|
||||
"light": "#b4637a"
|
||||
},
|
||||
"markdownHorizontalRule": {
|
||||
"dark": "highlightMed",
|
||||
"light": "#dfdad9"
|
||||
},
|
||||
"markdownListItem": {
|
||||
"dark": "foam",
|
||||
"light": "pine"
|
||||
},
|
||||
"markdownListEnumeration": {
|
||||
"dark": "rose",
|
||||
"light": "#d7827e"
|
||||
},
|
||||
"markdownImage": {
|
||||
"dark": "foam",
|
||||
"light": "pine"
|
||||
},
|
||||
"markdownImageText": {
|
||||
"dark": "rose",
|
||||
"light": "#d7827e"
|
||||
},
|
||||
"markdownCodeBlock": {
|
||||
"dark": "#e0def4",
|
||||
"light": "#575279"
|
||||
},
|
||||
"syntaxComment": {
|
||||
"dark": "muted",
|
||||
"light": "dawnMuted"
|
||||
},
|
||||
"syntaxKeyword": {
|
||||
"dark": "pine",
|
||||
"light": "#286983"
|
||||
},
|
||||
"syntaxFunction": {
|
||||
"dark": "rose",
|
||||
"light": "#d7827e"
|
||||
},
|
||||
"syntaxVariable": {
|
||||
"dark": "#e0def4",
|
||||
"light": "#575279"
|
||||
},
|
||||
"syntaxString": {
|
||||
"dark": "gold",
|
||||
"light": "#ea9d34"
|
||||
},
|
||||
"syntaxNumber": {
|
||||
"dark": "iris",
|
||||
"light": "#907aa9"
|
||||
},
|
||||
"syntaxType": {
|
||||
"dark": "foam",
|
||||
"light": "#56949f"
|
||||
},
|
||||
"syntaxOperator": {
|
||||
"dark": "subtle",
|
||||
"light": "dawnSubtle"
|
||||
},
|
||||
"syntaxPunctuation": {
|
||||
"dark": "subtle",
|
||||
"light": "dawnSubtle"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,223 @@
|
||||
{
|
||||
"$schema": "https://opencode.ai/theme.json",
|
||||
"defs": {
|
||||
"base03": "#002b36",
|
||||
"base02": "#073642",
|
||||
"base01": "#586e75",
|
||||
"base00": "#657b83",
|
||||
"base0": "#839496",
|
||||
"base1": "#93a1a1",
|
||||
"base2": "#eee8d5",
|
||||
"base3": "#fdf6e3",
|
||||
"yellow": "#b58900",
|
||||
"orange": "#cb4b16",
|
||||
"red": "#dc322f",
|
||||
"magenta": "#d33682",
|
||||
"violet": "#6c71c4",
|
||||
"blue": "#268bd2",
|
||||
"cyan": "#2aa198",
|
||||
"green": "#859900"
|
||||
},
|
||||
"theme": {
|
||||
"primary": {
|
||||
"dark": "blue",
|
||||
"light": "blue"
|
||||
},
|
||||
"secondary": {
|
||||
"dark": "violet",
|
||||
"light": "violet"
|
||||
},
|
||||
"accent": {
|
||||
"dark": "cyan",
|
||||
"light": "cyan"
|
||||
},
|
||||
"error": {
|
||||
"dark": "red",
|
||||
"light": "red"
|
||||
},
|
||||
"warning": {
|
||||
"dark": "yellow",
|
||||
"light": "yellow"
|
||||
},
|
||||
"success": {
|
||||
"dark": "green",
|
||||
"light": "green"
|
||||
},
|
||||
"info": {
|
||||
"dark": "orange",
|
||||
"light": "orange"
|
||||
},
|
||||
"text": {
|
||||
"dark": "base0",
|
||||
"light": "base00"
|
||||
},
|
||||
"textMuted": {
|
||||
"dark": "base01",
|
||||
"light": "base1"
|
||||
},
|
||||
"background": {
|
||||
"dark": "base03",
|
||||
"light": "base3"
|
||||
},
|
||||
"backgroundPanel": {
|
||||
"dark": "base02",
|
||||
"light": "base2"
|
||||
},
|
||||
"backgroundElement": {
|
||||
"dark": "#073642",
|
||||
"light": "#eee8d5"
|
||||
},
|
||||
"border": {
|
||||
"dark": "base02",
|
||||
"light": "base2"
|
||||
},
|
||||
"borderActive": {
|
||||
"dark": "base01",
|
||||
"light": "base1"
|
||||
},
|
||||
"borderSubtle": {
|
||||
"dark": "#073642",
|
||||
"light": "#eee8d5"
|
||||
},
|
||||
"diffAdded": {
|
||||
"dark": "green",
|
||||
"light": "green"
|
||||
},
|
||||
"diffRemoved": {
|
||||
"dark": "red",
|
||||
"light": "red"
|
||||
},
|
||||
"diffContext": {
|
||||
"dark": "base01",
|
||||
"light": "base1"
|
||||
},
|
||||
"diffHunkHeader": {
|
||||
"dark": "base01",
|
||||
"light": "base1"
|
||||
},
|
||||
"diffHighlightAdded": {
|
||||
"dark": "green",
|
||||
"light": "green"
|
||||
},
|
||||
"diffHighlightRemoved": {
|
||||
"dark": "red",
|
||||
"light": "red"
|
||||
},
|
||||
"diffAddedBg": {
|
||||
"dark": "#073642",
|
||||
"light": "#eee8d5"
|
||||
},
|
||||
"diffRemovedBg": {
|
||||
"dark": "#073642",
|
||||
"light": "#eee8d5"
|
||||
},
|
||||
"diffContextBg": {
|
||||
"dark": "base02",
|
||||
"light": "base2"
|
||||
},
|
||||
"diffLineNumber": {
|
||||
"dark": "base01",
|
||||
"light": "base1"
|
||||
},
|
||||
"diffAddedLineNumberBg": {
|
||||
"dark": "#073642",
|
||||
"light": "#eee8d5"
|
||||
},
|
||||
"diffRemovedLineNumberBg": {
|
||||
"dark": "#073642",
|
||||
"light": "#eee8d5"
|
||||
},
|
||||
"markdownText": {
|
||||
"dark": "base0",
|
||||
"light": "base00"
|
||||
},
|
||||
"markdownHeading": {
|
||||
"dark": "blue",
|
||||
"light": "blue"
|
||||
},
|
||||
"markdownLink": {
|
||||
"dark": "cyan",
|
||||
"light": "cyan"
|
||||
},
|
||||
"markdownLinkText": {
|
||||
"dark": "violet",
|
||||
"light": "violet"
|
||||
},
|
||||
"markdownCode": {
|
||||
"dark": "green",
|
||||
"light": "green"
|
||||
},
|
||||
"markdownBlockQuote": {
|
||||
"dark": "base01",
|
||||
"light": "base1"
|
||||
},
|
||||
"markdownEmph": {
|
||||
"dark": "yellow",
|
||||
"light": "yellow"
|
||||
},
|
||||
"markdownStrong": {
|
||||
"dark": "orange",
|
||||
"light": "orange"
|
||||
},
|
||||
"markdownHorizontalRule": {
|
||||
"dark": "base01",
|
||||
"light": "base1"
|
||||
},
|
||||
"markdownListItem": {
|
||||
"dark": "blue",
|
||||
"light": "blue"
|
||||
},
|
||||
"markdownListEnumeration": {
|
||||
"dark": "cyan",
|
||||
"light": "cyan"
|
||||
},
|
||||
"markdownImage": {
|
||||
"dark": "cyan",
|
||||
"light": "cyan"
|
||||
},
|
||||
"markdownImageText": {
|
||||
"dark": "violet",
|
||||
"light": "violet"
|
||||
},
|
||||
"markdownCodeBlock": {
|
||||
"dark": "base0",
|
||||
"light": "base00"
|
||||
},
|
||||
"syntaxComment": {
|
||||
"dark": "base01",
|
||||
"light": "base1"
|
||||
},
|
||||
"syntaxKeyword": {
|
||||
"dark": "green",
|
||||
"light": "green"
|
||||
},
|
||||
"syntaxFunction": {
|
||||
"dark": "blue",
|
||||
"light": "blue"
|
||||
},
|
||||
"syntaxVariable": {
|
||||
"dark": "cyan",
|
||||
"light": "cyan"
|
||||
},
|
||||
"syntaxString": {
|
||||
"dark": "cyan",
|
||||
"light": "cyan"
|
||||
},
|
||||
"syntaxNumber": {
|
||||
"dark": "magenta",
|
||||
"light": "magenta"
|
||||
},
|
||||
"syntaxType": {
|
||||
"dark": "yellow",
|
||||
"light": "yellow"
|
||||
},
|
||||
"syntaxOperator": {
|
||||
"dark": "green",
|
||||
"light": "green"
|
||||
},
|
||||
"syntaxPunctuation": {
|
||||
"dark": "base0",
|
||||
"light": "base00"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,226 @@
|
||||
{
|
||||
"$schema": "https://opencode.ai/theme.json",
|
||||
"defs": {
|
||||
"background": "#262335",
|
||||
"backgroundAlt": "#1e1a29",
|
||||
"backgroundPanel": "#2a2139",
|
||||
"foreground": "#ffffff",
|
||||
"foregroundMuted": "#848bbd",
|
||||
"pink": "#ff7edb",
|
||||
"pinkBright": "#ff92df",
|
||||
"cyan": "#36f9f6",
|
||||
"cyanBright": "#72f1f8",
|
||||
"yellow": "#fede5d",
|
||||
"yellowBright": "#fff95d",
|
||||
"orange": "#ff8b39",
|
||||
"orangeBright": "#ff9f43",
|
||||
"purple": "#b084eb",
|
||||
"purpleBright": "#c792ea",
|
||||
"red": "#fe4450",
|
||||
"redBright": "#ff5e5b",
|
||||
"green": "#72f1b8",
|
||||
"greenBright": "#97f1d8"
|
||||
},
|
||||
"theme": {
|
||||
"primary": {
|
||||
"dark": "cyan",
|
||||
"light": "#00bcd4"
|
||||
},
|
||||
"secondary": {
|
||||
"dark": "pink",
|
||||
"light": "#e91e63"
|
||||
},
|
||||
"accent": {
|
||||
"dark": "purple",
|
||||
"light": "#9c27b0"
|
||||
},
|
||||
"error": {
|
||||
"dark": "red",
|
||||
"light": "#f44336"
|
||||
},
|
||||
"warning": {
|
||||
"dark": "yellow",
|
||||
"light": "#ff9800"
|
||||
},
|
||||
"success": {
|
||||
"dark": "green",
|
||||
"light": "#4caf50"
|
||||
},
|
||||
"info": {
|
||||
"dark": "orange",
|
||||
"light": "#ff5722"
|
||||
},
|
||||
"text": {
|
||||
"dark": "foreground",
|
||||
"light": "#262335"
|
||||
},
|
||||
"textMuted": {
|
||||
"dark": "foregroundMuted",
|
||||
"light": "#5c5c8a"
|
||||
},
|
||||
"background": {
|
||||
"dark": "#262335",
|
||||
"light": "#fafafa"
|
||||
},
|
||||
"backgroundPanel": {
|
||||
"dark": "#1e1a29",
|
||||
"light": "#f5f5f5"
|
||||
},
|
||||
"backgroundElement": {
|
||||
"dark": "#2a2139",
|
||||
"light": "#eeeeee"
|
||||
},
|
||||
"border": {
|
||||
"dark": "#495495",
|
||||
"light": "#e0e0e0"
|
||||
},
|
||||
"borderActive": {
|
||||
"dark": "cyan",
|
||||
"light": "#00bcd4"
|
||||
},
|
||||
"borderSubtle": {
|
||||
"dark": "#241b2f",
|
||||
"light": "#f0f0f0"
|
||||
},
|
||||
"diffAdded": {
|
||||
"dark": "green",
|
||||
"light": "#4caf50"
|
||||
},
|
||||
"diffRemoved": {
|
||||
"dark": "red",
|
||||
"light": "#f44336"
|
||||
},
|
||||
"diffContext": {
|
||||
"dark": "foregroundMuted",
|
||||
"light": "#5c5c8a"
|
||||
},
|
||||
"diffHunkHeader": {
|
||||
"dark": "purple",
|
||||
"light": "#9c27b0"
|
||||
},
|
||||
"diffHighlightAdded": {
|
||||
"dark": "greenBright",
|
||||
"light": "#4caf50"
|
||||
},
|
||||
"diffHighlightRemoved": {
|
||||
"dark": "redBright",
|
||||
"light": "#f44336"
|
||||
},
|
||||
"diffAddedBg": {
|
||||
"dark": "#1a3a2a",
|
||||
"light": "#e8f5e9"
|
||||
},
|
||||
"diffRemovedBg": {
|
||||
"dark": "#3a1a2a",
|
||||
"light": "#ffebee"
|
||||
},
|
||||
"diffContextBg": {
|
||||
"dark": "#1e1a29",
|
||||
"light": "#f5f5f5"
|
||||
},
|
||||
"diffLineNumber": {
|
||||
"dark": "#495495",
|
||||
"light": "#b0b0b0"
|
||||
},
|
||||
"diffAddedLineNumberBg": {
|
||||
"dark": "#1a3a2a",
|
||||
"light": "#e8f5e9"
|
||||
},
|
||||
"diffRemovedLineNumberBg": {
|
||||
"dark": "#3a1a2a",
|
||||
"light": "#ffebee"
|
||||
},
|
||||
"markdownText": {
|
||||
"dark": "foreground",
|
||||
"light": "#262335"
|
||||
},
|
||||
"markdownHeading": {
|
||||
"dark": "pink",
|
||||
"light": "#e91e63"
|
||||
},
|
||||
"markdownLink": {
|
||||
"dark": "cyan",
|
||||
"light": "#00bcd4"
|
||||
},
|
||||
"markdownLinkText": {
|
||||
"dark": "purple",
|
||||
"light": "#9c27b0"
|
||||
},
|
||||
"markdownCode": {
|
||||
"dark": "green",
|
||||
"light": "#4caf50"
|
||||
},
|
||||
"markdownBlockQuote": {
|
||||
"dark": "foregroundMuted",
|
||||
"light": "#5c5c8a"
|
||||
},
|
||||
"markdownEmph": {
|
||||
"dark": "yellow",
|
||||
"light": "#ff9800"
|
||||
},
|
||||
"markdownStrong": {
|
||||
"dark": "orange",
|
||||
"light": "#ff5722"
|
||||
},
|
||||
"markdownHorizontalRule": {
|
||||
"dark": "#495495",
|
||||
"light": "#e0e0e0"
|
||||
},
|
||||
"markdownListItem": {
|
||||
"dark": "cyan",
|
||||
"light": "#00bcd4"
|
||||
},
|
||||
"markdownListEnumeration": {
|
||||
"dark": "purple",
|
||||
"light": "#9c27b0"
|
||||
},
|
||||
"markdownImage": {
|
||||
"dark": "cyan",
|
||||
"light": "#00bcd4"
|
||||
},
|
||||
"markdownImageText": {
|
||||
"dark": "purple",
|
||||
"light": "#9c27b0"
|
||||
},
|
||||
"markdownCodeBlock": {
|
||||
"dark": "foreground",
|
||||
"light": "#262335"
|
||||
},
|
||||
"syntaxComment": {
|
||||
"dark": "foregroundMuted",
|
||||
"light": "#5c5c8a"
|
||||
},
|
||||
"syntaxKeyword": {
|
||||
"dark": "pink",
|
||||
"light": "#e91e63"
|
||||
},
|
||||
"syntaxFunction": {
|
||||
"dark": "orange",
|
||||
"light": "#ff5722"
|
||||
},
|
||||
"syntaxVariable": {
|
||||
"dark": "foreground",
|
||||
"light": "#262335"
|
||||
},
|
||||
"syntaxString": {
|
||||
"dark": "yellow",
|
||||
"light": "#ff9800"
|
||||
},
|
||||
"syntaxNumber": {
|
||||
"dark": "purple",
|
||||
"light": "#9c27b0"
|
||||
},
|
||||
"syntaxType": {
|
||||
"dark": "cyan",
|
||||
"light": "#00bcd4"
|
||||
},
|
||||
"syntaxOperator": {
|
||||
"dark": "pink",
|
||||
"light": "#e91e63"
|
||||
},
|
||||
"syntaxPunctuation": {
|
||||
"dark": "foreground",
|
||||
"light": "#262335"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,223 @@
|
||||
{
|
||||
"$schema": "https://opencode.ai/theme.json",
|
||||
"defs": {
|
||||
"bg": "#3f3f3f",
|
||||
"bgAlt": "#4f4f4f",
|
||||
"bgPanel": "#5f5f5f",
|
||||
"fg": "#dcdccc",
|
||||
"fgMuted": "#9f9f9f",
|
||||
"red": "#cc9393",
|
||||
"redBright": "#dca3a3",
|
||||
"green": "#7f9f7f",
|
||||
"greenBright": "#8fb28f",
|
||||
"yellow": "#f0dfaf",
|
||||
"yellowDim": "#e0cf9f",
|
||||
"blue": "#8cd0d3",
|
||||
"blueDim": "#7cb8bb",
|
||||
"magenta": "#dc8cc3",
|
||||
"cyan": "#93e0e3",
|
||||
"orange": "#dfaf8f"
|
||||
},
|
||||
"theme": {
|
||||
"primary": {
|
||||
"dark": "blue",
|
||||
"light": "#5f7f8f"
|
||||
},
|
||||
"secondary": {
|
||||
"dark": "magenta",
|
||||
"light": "#8f5f8f"
|
||||
},
|
||||
"accent": {
|
||||
"dark": "cyan",
|
||||
"light": "#5f8f8f"
|
||||
},
|
||||
"error": {
|
||||
"dark": "red",
|
||||
"light": "#8f5f5f"
|
||||
},
|
||||
"warning": {
|
||||
"dark": "yellow",
|
||||
"light": "#8f8f5f"
|
||||
},
|
||||
"success": {
|
||||
"dark": "green",
|
||||
"light": "#5f8f5f"
|
||||
},
|
||||
"info": {
|
||||
"dark": "orange",
|
||||
"light": "#8f7f5f"
|
||||
},
|
||||
"text": {
|
||||
"dark": "fg",
|
||||
"light": "#3f3f3f"
|
||||
},
|
||||
"textMuted": {
|
||||
"dark": "fgMuted",
|
||||
"light": "#6f6f6f"
|
||||
},
|
||||
"background": {
|
||||
"dark": "bg",
|
||||
"light": "#ffffef"
|
||||
},
|
||||
"backgroundPanel": {
|
||||
"dark": "bgAlt",
|
||||
"light": "#f5f5e5"
|
||||
},
|
||||
"backgroundElement": {
|
||||
"dark": "bgPanel",
|
||||
"light": "#ebebdb"
|
||||
},
|
||||
"border": {
|
||||
"dark": "#5f5f5f",
|
||||
"light": "#d0d0c0"
|
||||
},
|
||||
"borderActive": {
|
||||
"dark": "blue",
|
||||
"light": "#5f7f8f"
|
||||
},
|
||||
"borderSubtle": {
|
||||
"dark": "#4f4f4f",
|
||||
"light": "#e0e0d0"
|
||||
},
|
||||
"diffAdded": {
|
||||
"dark": "green",
|
||||
"light": "#5f8f5f"
|
||||
},
|
||||
"diffRemoved": {
|
||||
"dark": "red",
|
||||
"light": "#8f5f5f"
|
||||
},
|
||||
"diffContext": {
|
||||
"dark": "fgMuted",
|
||||
"light": "#6f6f6f"
|
||||
},
|
||||
"diffHunkHeader": {
|
||||
"dark": "cyan",
|
||||
"light": "#5f8f8f"
|
||||
},
|
||||
"diffHighlightAdded": {
|
||||
"dark": "greenBright",
|
||||
"light": "#5f8f5f"
|
||||
},
|
||||
"diffHighlightRemoved": {
|
||||
"dark": "redBright",
|
||||
"light": "#8f5f5f"
|
||||
},
|
||||
"diffAddedBg": {
|
||||
"dark": "#4f5f4f",
|
||||
"light": "#efffef"
|
||||
},
|
||||
"diffRemovedBg": {
|
||||
"dark": "#5f4f4f",
|
||||
"light": "#ffefef"
|
||||
},
|
||||
"diffContextBg": {
|
||||
"dark": "bgAlt",
|
||||
"light": "#f5f5e5"
|
||||
},
|
||||
"diffLineNumber": {
|
||||
"dark": "#6f6f6f",
|
||||
"light": "#b0b0a0"
|
||||
},
|
||||
"diffAddedLineNumberBg": {
|
||||
"dark": "#4f5f4f",
|
||||
"light": "#efffef"
|
||||
},
|
||||
"diffRemovedLineNumberBg": {
|
||||
"dark": "#5f4f4f",
|
||||
"light": "#ffefef"
|
||||
},
|
||||
"markdownText": {
|
||||
"dark": "fg",
|
||||
"light": "#3f3f3f"
|
||||
},
|
||||
"markdownHeading": {
|
||||
"dark": "yellow",
|
||||
"light": "#8f8f5f"
|
||||
},
|
||||
"markdownLink": {
|
||||
"dark": "blue",
|
||||
"light": "#5f7f8f"
|
||||
},
|
||||
"markdownLinkText": {
|
||||
"dark": "cyan",
|
||||
"light": "#5f8f8f"
|
||||
},
|
||||
"markdownCode": {
|
||||
"dark": "green",
|
||||
"light": "#5f8f5f"
|
||||
},
|
||||
"markdownBlockQuote": {
|
||||
"dark": "fgMuted",
|
||||
"light": "#6f6f6f"
|
||||
},
|
||||
"markdownEmph": {
|
||||
"dark": "yellowDim",
|
||||
"light": "#8f8f5f"
|
||||
},
|
||||
"markdownStrong": {
|
||||
"dark": "orange",
|
||||
"light": "#8f7f5f"
|
||||
},
|
||||
"markdownHorizontalRule": {
|
||||
"dark": "fgMuted",
|
||||
"light": "#6f6f6f"
|
||||
},
|
||||
"markdownListItem": {
|
||||
"dark": "blue",
|
||||
"light": "#5f7f8f"
|
||||
},
|
||||
"markdownListEnumeration": {
|
||||
"dark": "cyan",
|
||||
"light": "#5f8f8f"
|
||||
},
|
||||
"markdownImage": {
|
||||
"dark": "blue",
|
||||
"light": "#5f7f8f"
|
||||
},
|
||||
"markdownImageText": {
|
||||
"dark": "cyan",
|
||||
"light": "#5f8f8f"
|
||||
},
|
||||
"markdownCodeBlock": {
|
||||
"dark": "fg",
|
||||
"light": "#3f3f3f"
|
||||
},
|
||||
"syntaxComment": {
|
||||
"dark": "#7f9f7f",
|
||||
"light": "#5f7f5f"
|
||||
},
|
||||
"syntaxKeyword": {
|
||||
"dark": "yellow",
|
||||
"light": "#8f8f5f"
|
||||
},
|
||||
"syntaxFunction": {
|
||||
"dark": "blue",
|
||||
"light": "#5f7f8f"
|
||||
},
|
||||
"syntaxVariable": {
|
||||
"dark": "fg",
|
||||
"light": "#3f3f3f"
|
||||
},
|
||||
"syntaxString": {
|
||||
"dark": "red",
|
||||
"light": "#8f5f5f"
|
||||
},
|
||||
"syntaxNumber": {
|
||||
"dark": "greenBright",
|
||||
"light": "#5f8f5f"
|
||||
},
|
||||
"syntaxType": {
|
||||
"dark": "cyan",
|
||||
"light": "#5f8f8f"
|
||||
},
|
||||
"syntaxOperator": {
|
||||
"dark": "yellow",
|
||||
"light": "#8f8f5f"
|
||||
},
|
||||
"syntaxPunctuation": {
|
||||
"dark": "fg",
|
||||
"light": "#3f3f3f"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,7 @@ import (
|
||||
"github.com/sst/opencode/internal/components/toast"
|
||||
"github.com/sst/opencode/internal/layout"
|
||||
"github.com/sst/opencode/internal/styles"
|
||||
"github.com/sst/opencode/internal/theme"
|
||||
"github.com/sst/opencode/internal/util"
|
||||
"github.com/sst/opencode/pkg/client"
|
||||
)
|
||||
@@ -230,9 +231,19 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
return a, tea.Batch(cmds...)
|
||||
case tea.BackgroundColorMsg:
|
||||
styles.Terminal = &styles.TerminalInfo{
|
||||
Background: msg.Color,
|
||||
BackgroundIsDark: msg.IsDark(),
|
||||
}
|
||||
slog.Debug("Background color", "isDark", msg.IsDark())
|
||||
slog.Debug("Background color", "color", msg.String(), "isDark", msg.IsDark())
|
||||
return a, func() tea.Msg {
|
||||
theme.UpdateSystemTheme(
|
||||
styles.Terminal.Background,
|
||||
styles.Terminal.BackgroundIsDark,
|
||||
)
|
||||
return dialog.ThemeSelectedMsg{
|
||||
ThemeName: theme.CurrentThemeName(),
|
||||
}
|
||||
}
|
||||
case modal.CloseModalMsg:
|
||||
var cmd tea.Cmd
|
||||
if a.modal != nil {
|
||||
@@ -424,6 +435,9 @@ func (a appModel) View() string {
|
||||
|
||||
appView = a.toastManager.RenderOverlay(appView)
|
||||
|
||||
if theme.CurrentThemeUsesAnsiColors() {
|
||||
appView = util.ConvertRGBToAnsi16Colors(appView)
|
||||
}
|
||||
return appView
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var csiRE *regexp.Regexp
|
||||
|
||||
func init() {
|
||||
csiRE = regexp.MustCompile(`\x1b\[([0-9;]+)m`)
|
||||
}
|
||||
|
||||
var targetFGMap = map[string]string{
|
||||
"0;0;0": "\x1b[30m", // Black
|
||||
"128;0;0": "\x1b[31m", // Red
|
||||
"0;128;0": "\x1b[32m", // Green
|
||||
"128;128;0": "\x1b[33m", // Yellow
|
||||
"0;0;128": "\x1b[34m", // Blue
|
||||
"128;0;128": "\x1b[35m", // Magenta
|
||||
"0;128;128": "\x1b[36m", // Cyan
|
||||
"192;192;192": "\x1b[37m", // White (light grey)
|
||||
"128;128;128": "\x1b[90m", // Bright Black (dark grey)
|
||||
"255;0;0": "\x1b[91m", // Bright Red
|
||||
"0;255;0": "\x1b[92m", // Bright Green
|
||||
"255;255;0": "\x1b[93m", // Bright Yellow
|
||||
"0;0;255": "\x1b[94m", // Bright Blue
|
||||
"255;0;255": "\x1b[95m", // Bright Magenta
|
||||
"0;255;255": "\x1b[96m", // Bright Cyan
|
||||
"255;255;255": "\x1b[97m", // Bright White
|
||||
}
|
||||
|
||||
var targetBGMap = map[string]string{
|
||||
"0;0;0": "\x1b[40m",
|
||||
"128;0;0": "\x1b[41m",
|
||||
"0;128;0": "\x1b[42m",
|
||||
"128;128;0": "\x1b[43m",
|
||||
"0;0;128": "\x1b[44m",
|
||||
"128;0;128": "\x1b[45m",
|
||||
"0;128;128": "\x1b[46m",
|
||||
"192;192;192": "\x1b[47m",
|
||||
"128;128;128": "\x1b[100m",
|
||||
"255;0;0": "\x1b[101m",
|
||||
"0;255;0": "\x1b[102m",
|
||||
"255;255;0": "\x1b[103m",
|
||||
"0;0;255": "\x1b[104m",
|
||||
"255;0;255": "\x1b[105m",
|
||||
"0;255;255": "\x1b[106m",
|
||||
"255;255;255": "\x1b[107m",
|
||||
}
|
||||
|
||||
func ConvertRGBToAnsi16Colors(s string) string {
|
||||
return csiRE.ReplaceAllStringFunc(s, func(seq string) string {
|
||||
params := strings.Split(csiRE.FindStringSubmatch(seq)[1], ";")
|
||||
out := make([]string, 0, len(params))
|
||||
|
||||
for i := 0; i < len(params); {
|
||||
// Detect “38 | 48 ; 2 ; r ; g ; b ( ; alpha? )”
|
||||
if (params[i] == "38" || params[i] == "48") &&
|
||||
i+4 < len(params) &&
|
||||
params[i+1] == "2" {
|
||||
|
||||
key := strings.Join(params[i+2:i+5], ";")
|
||||
var repl string
|
||||
if params[i] == "38" {
|
||||
repl = targetFGMap[key]
|
||||
} else {
|
||||
repl = targetBGMap[key]
|
||||
}
|
||||
|
||||
if repl != "" { // exact RGB hit
|
||||
out = append(out, repl[2:len(repl)-1])
|
||||
i += 5 // skip 38/48;2;r;g;b
|
||||
|
||||
// if i == len(params)-1 && looksLikeByte(params[i]) {
|
||||
// i++ // swallow the alpha byte
|
||||
// }
|
||||
continue
|
||||
}
|
||||
}
|
||||
// Normal token — keep verbatim.
|
||||
out = append(out, params[i])
|
||||
i++
|
||||
}
|
||||
|
||||
return "\x1b[" + strings.Join(out, ";") + "m"
|
||||
})
|
||||
}
|
||||
|
||||
// func looksLikeByte(tok string) bool {
|
||||
// v, err := strconv.Atoi(tok)
|
||||
// return err == nil && v >= 0 && v <= 255
|
||||
// }
|
||||
@@ -8,14 +8,12 @@ import config from "./config.mjs"
|
||||
import { rehypeHeadingIds } from "@astrojs/markdown-remark"
|
||||
import rehypeAutolinkHeadings from "rehype-autolink-headings"
|
||||
|
||||
const url = "https://opencode.ai"
|
||||
const github = "https://github.com/sst/opencode"
|
||||
const headerLinks = [
|
||||
{ name: "Docs", url: "/docs/" },
|
||||
{ name: "GitHub", url: github },
|
||||
]
|
||||
|
||||
// https://astro.build/config
|
||||
export default defineConfig({
|
||||
site: url,
|
||||
output: "server",
|
||||
adapter: cloudflare({
|
||||
imageService: "passthrough",
|
||||
@@ -37,6 +35,29 @@ export default defineConfig({
|
||||
social: [
|
||||
{ icon: "github", label: "GitHub", href: config.github },
|
||||
],
|
||||
head: [
|
||||
{
|
||||
tag: "link",
|
||||
attrs: {
|
||||
rel: "icon",
|
||||
href: "/favicon.svg",
|
||||
},
|
||||
},
|
||||
{
|
||||
tag: "meta",
|
||||
attrs: {
|
||||
property: "og:image",
|
||||
content: `${url}/social-share.png`,
|
||||
},
|
||||
},
|
||||
{
|
||||
tag: "meta",
|
||||
attrs: {
|
||||
property: "twitter:image",
|
||||
content: `${url}/social-share.png`,
|
||||
},
|
||||
},
|
||||
],
|
||||
editLink: {
|
||||
baseUrl: `${github}/edit/master/www/`,
|
||||
},
|
||||
@@ -52,6 +73,7 @@ export default defineConfig({
|
||||
sidebar: [
|
||||
"docs",
|
||||
"docs/cli",
|
||||
"docs/rules",
|
||||
"docs/config",
|
||||
"docs/models",
|
||||
"docs/themes",
|
||||
|
||||
@@ -22,6 +22,11 @@
|
||||
"minimum": 0,
|
||||
"maximum": 255,
|
||||
"description": "ANSI color code (0-255)"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"enum": ["none"],
|
||||
"description": "No color (uses terminal default)"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -110,6 +115,11 @@
|
||||
"maximum": 255,
|
||||
"description": "ANSI color code (0-255, same for dark and light)"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"enum": ["none"],
|
||||
"description": "No color (uses terminal default)"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"pattern": "^[a-zA-Z][a-zA-Z0-9_]*$",
|
||||
@@ -131,6 +141,11 @@
|
||||
"maximum": 255,
|
||||
"description": "ANSI color code for dark mode"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"enum": ["none"],
|
||||
"description": "No color (uses terminal default)"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"pattern": "^[a-zA-Z][a-zA-Z0-9_]*$",
|
||||
@@ -151,6 +166,11 @@
|
||||
"maximum": 255,
|
||||
"description": "ANSI color code for light mode"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"enum": ["none"],
|
||||
"description": "No color (uses terminal default)"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"pattern": "^[a-zA-Z][a-zA-Z0-9_]*$",
|
||||
|
||||
@@ -54,8 +54,8 @@ const DiffView: Component<DiffViewProps> = (props) => {
|
||||
// Pair removals with additions
|
||||
const maxLength = Math.max(removals.length, additions.length)
|
||||
for (let k = 0; k < maxLength; k++) {
|
||||
const hasLeft = !!removals[k]
|
||||
const hasRight = !!additions[k]
|
||||
const hasLeft = k < removals.length
|
||||
const hasRight = k < additions.length
|
||||
|
||||
if (hasLeft && hasRight) {
|
||||
// Replacement - left is removed, right is added
|
||||
@@ -71,8 +71,8 @@ const DiffView: Component<DiffViewProps> = (props) => {
|
||||
right: "",
|
||||
type: "removed"
|
||||
})
|
||||
} else {
|
||||
// Pure addition
|
||||
} else if (hasRight) {
|
||||
// Pure addition - only create if we actually have content
|
||||
diffRows.push({
|
||||
left: "",
|
||||
right: additions[k],
|
||||
@@ -111,27 +111,89 @@ const DiffView: Component<DiffViewProps> = (props) => {
|
||||
return diffRows
|
||||
})
|
||||
|
||||
const mobileRows = createMemo(() => {
|
||||
const mobileBlocks: { type: 'removed' | 'added' | 'unchanged', lines: string[] }[] = []
|
||||
const currentRows = rows()
|
||||
|
||||
let i = 0
|
||||
while (i < currentRows.length) {
|
||||
const removedLines: string[] = []
|
||||
const addedLines: string[] = []
|
||||
|
||||
// Collect consecutive modified/removed/added rows
|
||||
while (i < currentRows.length &&
|
||||
(currentRows[i].type === 'modified' ||
|
||||
currentRows[i].type === 'removed' ||
|
||||
currentRows[i].type === 'added')) {
|
||||
const row = currentRows[i]
|
||||
if (row.left && (row.type === 'removed' || row.type === 'modified')) {
|
||||
removedLines.push(row.left)
|
||||
}
|
||||
if (row.right && (row.type === 'added' || row.type === 'modified')) {
|
||||
addedLines.push(row.right)
|
||||
}
|
||||
i++
|
||||
}
|
||||
|
||||
// Add grouped blocks
|
||||
if (removedLines.length > 0) {
|
||||
mobileBlocks.push({ type: 'removed', lines: removedLines })
|
||||
}
|
||||
if (addedLines.length > 0) {
|
||||
mobileBlocks.push({ type: 'added', lines: addedLines })
|
||||
}
|
||||
|
||||
// Add unchanged rows as-is
|
||||
if (i < currentRows.length && currentRows[i].type === 'unchanged') {
|
||||
mobileBlocks.push({
|
||||
type: 'unchanged',
|
||||
lines: [currentRows[i].left]
|
||||
})
|
||||
i++
|
||||
}
|
||||
}
|
||||
|
||||
return mobileBlocks
|
||||
})
|
||||
|
||||
return (
|
||||
<div class={`${styles.diff} ${props.class ?? ""}`}>
|
||||
<div class={styles.column}>
|
||||
<div class={styles.desktopView}>
|
||||
{rows().map((r) => (
|
||||
<CodeBlock
|
||||
code={r.left}
|
||||
lang={props.lang}
|
||||
data-section="cell"
|
||||
data-diff-type={r.type === "removed" || r.type === "modified" ? "removed" : ""}
|
||||
/>
|
||||
<div class={styles.row}>
|
||||
<div class={styles.beforeColumn}>
|
||||
<CodeBlock
|
||||
code={r.left}
|
||||
lang={props.lang}
|
||||
data-section="cell"
|
||||
data-diff-type={r.type === "removed" || r.type === "modified" ? "removed" : ""}
|
||||
/>
|
||||
</div>
|
||||
<div class={styles.afterColumn}>
|
||||
<CodeBlock
|
||||
code={r.right}
|
||||
lang={props.lang}
|
||||
data-section="cell"
|
||||
data-diff-type={r.type === "added" || r.type === "modified" ? "added" : ""}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div class={styles.column}>
|
||||
{rows().map((r) => (
|
||||
<CodeBlock
|
||||
code={r.right}
|
||||
lang={props.lang}
|
||||
data-section="cell"
|
||||
data-diff-type={r.type === "added" || r.type === "modified" ? "added" : ""}
|
||||
/>
|
||||
<div class={styles.mobileView}>
|
||||
{mobileRows().map((block) => (
|
||||
<div class={styles.mobileBlock}>
|
||||
{block.lines.map((line) => (
|
||||
<CodeBlock
|
||||
code={line}
|
||||
lang={props.lang}
|
||||
data-section="cell"
|
||||
data-diff-type={block.type === 'removed' ? 'removed' :
|
||||
block.type === 'added' ? 'added' : ''}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
@@ -139,3 +201,46 @@ const DiffView: Component<DiffViewProps> = (props) => {
|
||||
}
|
||||
|
||||
export default DiffView
|
||||
|
||||
// const testDiff = `--- combined_before.txt 2025-06-24 16:38:08
|
||||
// +++ combined_after.txt 2025-06-24 16:38:12
|
||||
// @@ -1,21 +1,25 @@
|
||||
// unchanged line
|
||||
// -deleted line
|
||||
// -old content
|
||||
// +added line
|
||||
// +new content
|
||||
//
|
||||
// -removed empty line below
|
||||
// +added empty line above
|
||||
//
|
||||
// - tab indented
|
||||
// -trailing spaces
|
||||
// -very long line that will definitely wrap in most editors and cause potential alignment issues when displayed in a two column diff view
|
||||
// -unicode content: 🚀 ✨ 中文
|
||||
// -mixed content with tabs and spaces
|
||||
// + space indented
|
||||
// +no trailing spaces
|
||||
// +short line
|
||||
// +very long replacement line that will also wrap and test how the diff viewer handles long line additions after short line removals
|
||||
// +different unicode: 🎉 💻 日本語
|
||||
// +normalized content with consistent spacing
|
||||
// +newline to content
|
||||
//
|
||||
// -content to remove
|
||||
// -whitespace only:
|
||||
// -multiple
|
||||
// -consecutive
|
||||
// -deletions
|
||||
// -single deletion
|
||||
// +
|
||||
// +single addition
|
||||
// +first addition
|
||||
// +second addition
|
||||
// +third addition
|
||||
// line before addition
|
||||
// +first added line
|
||||
// +
|
||||
// +third added line
|
||||
// line after addition
|
||||
// final unchanged line`
|
||||
|
||||
@@ -463,6 +463,7 @@ function MarkdownPart(props: MarkdownPartProps) {
|
||||
|
||||
interface TerminalPartProps extends JSX.HTMLAttributes<HTMLDivElement> {
|
||||
command: string
|
||||
error?: string
|
||||
result?: string
|
||||
desc?: string
|
||||
expand?: boolean
|
||||
@@ -470,6 +471,7 @@ interface TerminalPartProps extends JSX.HTMLAttributes<HTMLDivElement> {
|
||||
function TerminalPart(props: TerminalPartProps) {
|
||||
const [local, rest] = splitProps(props, [
|
||||
"command",
|
||||
"error",
|
||||
"result",
|
||||
"desc",
|
||||
"expand",
|
||||
@@ -508,12 +510,25 @@ function TerminalPart(props: TerminalPartProps) {
|
||||
</div>
|
||||
<div data-section="content">
|
||||
<CodeBlock lang="bash" code={local.command} />
|
||||
<CodeBlock
|
||||
lang="console"
|
||||
onRendered={checkOverflow}
|
||||
ref={(el) => (preEl = el)}
|
||||
code={local.result || ""}
|
||||
/>
|
||||
<Switch>
|
||||
<Match when={local.error}>
|
||||
<CodeBlock
|
||||
data-section="error"
|
||||
lang="text"
|
||||
onRendered={checkOverflow}
|
||||
ref={(el) => (preEl = el)}
|
||||
code={local.error || ""}
|
||||
/>
|
||||
</Match>
|
||||
<Match when={local.result}>
|
||||
<CodeBlock
|
||||
lang="console"
|
||||
onRendered={checkOverflow}
|
||||
ref={(el) => (preEl = el)}
|
||||
code={local.result || ""}
|
||||
/>
|
||||
</Match>
|
||||
</Switch>
|
||||
</div>
|
||||
</div>
|
||||
{((!local.expand && overflowed()) || expanded()) && (
|
||||
@@ -1601,8 +1616,10 @@ export default function Share(props: {
|
||||
}
|
||||
>
|
||||
{(_part) => {
|
||||
const command = () => toolData()?.args.command
|
||||
const desc = () => toolData()?.args.description
|
||||
const command = () => toolData()?.metadata?.title
|
||||
const desc = () => toolData()?.metadata?.description
|
||||
const result = () => toolData()?.metadata?.stdout
|
||||
const error = () => toolData()?.metadata?.stderr
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -1617,14 +1634,17 @@ export default function Share(props: {
|
||||
<div></div>
|
||||
</div>
|
||||
<div data-section="content">
|
||||
<div data-part-tool-body>
|
||||
<TerminalPart
|
||||
desc={desc()}
|
||||
data-size="sm"
|
||||
command={command()}
|
||||
result={toolData()?.result}
|
||||
/>
|
||||
</div>
|
||||
{command() && (
|
||||
<div data-part-tool-body>
|
||||
<TerminalPart
|
||||
desc={desc()}
|
||||
data-size="sm"
|
||||
command={command()!}
|
||||
result={result()}
|
||||
error={error()}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<ToolFooter
|
||||
time={toolData()?.duration || 0}
|
||||
/>
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
pre {
|
||||
--shiki-dark-bg: var(--sl-color-bg-surface) !important;
|
||||
background-color: var(--sl-color-bg-surface) !important;
|
||||
|
||||
span {
|
||||
white-space: break-spaces;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,39 +1,65 @@
|
||||
.diff {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border: 1px solid var(--sl-color-divider);
|
||||
background-color: var(--sl-color-bg-surface);
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
|
||||
.column {
|
||||
.desktopView {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.mobileView {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.mobileBlock {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.beforeColumn,
|
||||
.afterColumn {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow-x: visible;
|
||||
min-width: 0;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
border-right: 1px solid var(--sl-color-divider);
|
||||
}
|
||||
.beforeColumn {
|
||||
border-right: 1px solid var(--sl-color-divider);
|
||||
}
|
||||
|
||||
& > [data-section="cell"]:first-child {
|
||||
padding-top: 0.5rem;
|
||||
}
|
||||
& > [data-section="cell"]:last-child {
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
.diff > .row:first-child [data-section="cell"]:first-child {
|
||||
padding-top: 0.5rem;
|
||||
}
|
||||
|
||||
.diff > .row:last-child [data-section="cell"]:last-child {
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
[data-section="cell"] {
|
||||
position: relative;
|
||||
flex: none;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
width: 100%;
|
||||
padding: 0.1875rem 0.5rem 0.1875rem 2.2ch;
|
||||
margin: 0;
|
||||
|
||||
&[data-display-mobile="true"] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
pre {
|
||||
--shiki-dark-bg: var(--sl-color-bg-surface) !important;
|
||||
background-color: var(--sl-color-bg-surface) !important;
|
||||
@@ -83,3 +109,13 @@
|
||||
color: var(--sl-color-green-high);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 40rem) {
|
||||
.desktopView {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.mobileView {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,6 +30,8 @@
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
|
||||
@@ -616,6 +616,13 @@
|
||||
}
|
||||
}
|
||||
|
||||
[data-section="error"] {
|
||||
pre {
|
||||
color: var(--sl-color-red) !important;
|
||||
--shiki-dark: var(--sl-color-red) !important;
|
||||
}
|
||||
}
|
||||
|
||||
&[data-expanded="true"] {
|
||||
pre {
|
||||
display: block;
|
||||
|
||||
@@ -64,6 +64,10 @@ paru -S opencode-bin
|
||||
|
||||
---
|
||||
|
||||
##### Windows
|
||||
|
||||
Right now the automatic installation methods do not work properly on Windows. However you can grab the binary from the [Releases](https://github.com/sst/opencode/releases).
|
||||
|
||||
## Providers
|
||||
|
||||
We recommend signing up for Claude Pro or Max, running `opencode auth login` and selecting Anthropic. It's the most cost-effective way to use opencode.
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
---
|
||||
title: Rules
|
||||
---
|
||||
|
||||
You can provide custom instructions to opencode by creating an `AGENTS.md` file. This is similar to `CLAUDE.md` or Cursor's rules. It contains instructions that will be included in the LLM's context to customize its behavior for your specific project.
|
||||
|
||||
---
|
||||
|
||||
## Initialize
|
||||
|
||||
To create a new `AGENTS.md` file, you can run the `/init` command in opencode.
|
||||
|
||||
:::tip
|
||||
You should commit your project's `AGENTS.md` file to Git.
|
||||
:::
|
||||
|
||||
This will scan your project and all its contents to understand what the project is about and generate an `AGENTS.md` file with it. This helps opencode to navigate the project better.
|
||||
|
||||
If you have an existing `AGENTS.md` file, this will try to add to it.
|
||||
|
||||
---
|
||||
|
||||
## Example
|
||||
|
||||
You can also just create this file manually. Here's an example of some things you can put into an `AGENTS.md` file.
|
||||
|
||||
```markdown title="AGENTS.md"
|
||||
# SST v3 Monorepo Project
|
||||
|
||||
This is an SST v3 monorepo with TypeScript. The project uses bun workspaces for package management.
|
||||
|
||||
## Project Structure
|
||||
- `packages/` - Contains all workspace packages (functions, core, web, etc.)
|
||||
- `infra/` - Infrastructure definitions split by service (storage.ts, api.ts, web.ts)
|
||||
- `sst.config.ts` - Main SST configuration with dynamic imports
|
||||
|
||||
## Code Standards
|
||||
- Use TypeScript with strict mode enabled
|
||||
- Shared code goes in `packages/core/` with proper exports configuration
|
||||
- Functions go in `packages/functions/`
|
||||
- Infrastructure should be split into logical files in `infra/`
|
||||
|
||||
## Monorepo Conventions
|
||||
- Import shared modules using workspace names: `@my-app/core/example`
|
||||
```
|
||||
|
||||
We are adding project-specific instructions here and this will be shared across your team.
|
||||
|
||||
---
|
||||
|
||||
## Types
|
||||
|
||||
opencode also supports reading the `AGENTS.md` file from multiple locations. And this serves different purposes.
|
||||
|
||||
### Project
|
||||
|
||||
The ones we have seen above, where the `AGENTS.md` is placed in the project root, are project-specific rules. These only apply when you are working in this directory or its sub-directories.
|
||||
|
||||
### Global
|
||||
|
||||
You can also have global rules in a `~/.config/opencode/AGENTS.md` file. This gets applied across all opencode sessions.
|
||||
|
||||
Since this isn't committed to Git or shared with your team, we recommend using this to specify any personal rules that the LLM should follow.
|
||||
|
||||
---
|
||||
|
||||
## Precedence
|
||||
|
||||
So when opencode starts, it looks for:
|
||||
|
||||
1. **Local files** by traversing up from the current directory
|
||||
2. **Global file** by checking `~/.config/opencode/AGENTS.md`
|
||||
|
||||
If you have both global and project-specific rules, opencode will combine them together.
|
||||
@@ -2,34 +2,96 @@
|
||||
title: Themes
|
||||
---
|
||||
|
||||
opencode supports a flexible JSON-based theme system that allows users to create and customize themes easily.
|
||||
With opencode you can select from one of several built-in themes, use a theme that adapts to your terminal theme, or define your own custom theme.
|
||||
|
||||
## Theme Loading Hierarchy
|
||||
By default, opencode uses our own `opencode` theme.
|
||||
|
||||
Themes are loaded from multiple directories in the following order (later directories override earlier ones):
|
||||
---
|
||||
|
||||
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`
|
||||
## Built-in themes
|
||||
|
||||
opencode comes with several built-in themes.
|
||||
|
||||
| Name | Description |
|
||||
| --- | --- |
|
||||
| `system` | Adapts to your terminal's background color |
|
||||
| `tokyonight` | Based on the Tokyonight theme |
|
||||
| `everforest` | Based on the Everforest theme |
|
||||
| `ayu` | Based on the Ayu dark theme |
|
||||
| `catppuccin` | Based on the Catppuccin theme |
|
||||
| `gruvbox` | Based on the Gruvbox theme |
|
||||
| `kanagawa` | Based on the Kanagawa theme |
|
||||
| `nord` | Based on the Nord theme |
|
||||
| `matrix` | Hacker-style green on black theme |
|
||||
| `one-dark` | Based on the Atom One Dark theme |
|
||||
|
||||
And more, we are constantly adding new themes.
|
||||
|
||||
---
|
||||
|
||||
## System theme
|
||||
|
||||
The `system` theme is designed to automatically adapt to your terminal's color scheme. Unlike traditional themes that use fixed colors, the _system_ theme:
|
||||
|
||||
- **Generates gray scale**: Creates a custom gray scale based on your terminal's background color, ensuring optimal contrast.
|
||||
- **Uses ANSI colors**: Leverages standard ANSI colors (0-15) for syntax highlighting and UI elements, which respect your terminal's color palette.
|
||||
- **Preserves terminal defaults**: Uses `none` for text and background colors to maintain your terminal's native appearance.
|
||||
|
||||
The system theme is for users who:
|
||||
|
||||
- Want opencode to match their terminal's appearance
|
||||
- Use custom terminal color schemes
|
||||
- Prefer a consistent look across all terminal applications
|
||||
|
||||
---
|
||||
|
||||
## Using a theme
|
||||
|
||||
You can select a theme by bringing up the theme select with the `/theme` command. Or you can specify it in your [config](/docs/config).
|
||||
|
||||
```json title="opencode.json" {3}
|
||||
{
|
||||
"$schema": "https://opencode.ai/config.json",
|
||||
"theme": "tokyonight"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Custom themes
|
||||
|
||||
opencode supports a flexible JSON-based theme system that allows users to create and customize themes easily.
|
||||
|
||||
### Hierarchy
|
||||
|
||||
Themes are loaded from multiple directories in the following order where later directories override earlier ones:
|
||||
|
||||
1. **Built-in themes** - These are embedded in the binary
|
||||
2. **User config directory** - Defined in `~/.config/opencode/themes/*.json` or `$XDG_CONFIG_HOME/opencode/themes/*.json`
|
||||
3. **Project root directory** - Defined in the `<project-root>/.opencode/themes/*.json`
|
||||
4. **Current working directory** - Defined in `./.opencode/themes/*.json`
|
||||
|
||||
If multiple directories contain a theme with the same name, the theme from the directory with higher priority will be used.
|
||||
|
||||
## Creating a Custom Theme
|
||||
### Creating a theme
|
||||
|
||||
To create a custom theme, create a JSON file in one of the theme directories:
|
||||
To create a custom theme, create a JSON file in one of the theme directories.
|
||||
|
||||
For user-wide themes:
|
||||
|
||||
```bash no-frame
|
||||
# For user-wide themes
|
||||
mkdir -p ~/.config/opencode/themes
|
||||
vim ~/.config/opencode/themes/my-theme.json
|
||||
```
|
||||
|
||||
# For project-specific themes
|
||||
And for project-specific themes.
|
||||
|
||||
```bash no-frame
|
||||
mkdir -p .opencode/themes
|
||||
vim .opencode/themes/my-theme.json
|
||||
```
|
||||
|
||||
## Theme JSON Format
|
||||
### JSON format
|
||||
|
||||
Themes use a flexible JSON format with support for:
|
||||
|
||||
@@ -37,10 +99,13 @@ Themes use a flexible JSON format with support for:
|
||||
- **ANSI colors**: `3` (0-255)
|
||||
- **Color references**: `"primary"` or custom definitions
|
||||
- **Dark/light variants**: `{"dark": "#000", "light": "#fff"}`
|
||||
- **No color**: `"none"` - Uses the terminal's default color or transparent
|
||||
|
||||
### Example Theme
|
||||
### Example
|
||||
|
||||
```json no-frame
|
||||
Here's an example of a custom theme:
|
||||
|
||||
```json title="my-theme.json"
|
||||
{
|
||||
"$schema": "https://opencode.ai/theme.json",
|
||||
"defs": {
|
||||
@@ -266,30 +331,13 @@ Themes use a flexible JSON format with support for:
|
||||
}
|
||||
```
|
||||
|
||||
### Color Definitions
|
||||
### Color definitions
|
||||
|
||||
The `defs` section (optional) allows you to define reusable colors that can be referenced in the theme.
|
||||
The `defs` section is optional and it allows you to define reusable colors that can be referenced in the theme.
|
||||
|
||||
## Built-in Themes
|
||||
### Terminal defaults
|
||||
|
||||
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)
|
||||
The special value `\"none\"` can be used for any color to inherit the terminal's default color. This is particularly useful for creating themes that blend seamlessly with your terminal's color scheme:
|
||||
|
||||
## 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}
|
||||
{
|
||||
"$schema": "https://opencode.ai/config.json",
|
||||
"theme": "tokyonight"
|
||||
}
|
||||
```
|
||||
- `"text": "none"` - Uses terminal's default foreground color
|
||||
- `"background": "none"` - Uses terminal's default background color
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
@echo off
|
||||
|
||||
if not exist ".git" (
|
||||
exit /b 0
|
||||
)
|
||||
|
||||
if not exist ".git\hooks" (
|
||||
mkdir ".git\hooks"
|
||||
)
|
||||
|
||||
(
|
||||
echo #!/bin/sh
|
||||
echo bun run typecheck
|
||||
) > ".git\hooks\pre-push"
|
||||
|
||||
echo ✅ Pre-push hook installed
|
||||
Reference in New Issue
Block a user