mirror of
https://github.com/sst/opencode.git
synced 2025-08-04 13:30:52 +00:00
wip: refactoring tui
This commit is contained in:
parent
979bad3e64
commit
95d5e1f231
37 changed files with 1496 additions and 1801 deletions
70
README.md
70
README.md
|
@ -411,76 +411,6 @@ OpenCode's AI assistant has access to various tools to help with coding tasks:
|
||||||
| `fetch` | Fetch data from URLs | `url` (required), `format` (required), `timeout` (optional) |
|
| `fetch` | Fetch data from URLs | `url` (required), `format` (required), `timeout` (optional) |
|
||||||
| `agent` | Run sub-tasks with the AI agent | `prompt` (required) |
|
| `agent` | Run sub-tasks with the AI agent | `prompt` (required) |
|
||||||
|
|
||||||
## Theming
|
|
||||||
|
|
||||||
OpenCode supports multiple themes for customizing the appearance of the terminal interface.
|
|
||||||
|
|
||||||
### Available Themes
|
|
||||||
|
|
||||||
The following predefined themes are available:
|
|
||||||
|
|
||||||
- `opencode` (default)
|
|
||||||
- `catppuccin`
|
|
||||||
- `dracula`
|
|
||||||
- `flexoki`
|
|
||||||
- `gruvbox`
|
|
||||||
- `monokai`
|
|
||||||
- `onedark`
|
|
||||||
- `tokyonight`
|
|
||||||
- `tron`
|
|
||||||
- `custom` (user-defined)
|
|
||||||
|
|
||||||
### Setting a Theme
|
|
||||||
|
|
||||||
You can set a theme in your `.opencode.json` configuration file:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"tui": {
|
|
||||||
"theme": "monokai"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Custom Themes
|
|
||||||
|
|
||||||
You can define your own custom theme by setting the `theme` to `"custom"` and providing color definitions in the `customTheme` map:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"tui": {
|
|
||||||
"theme": "custom",
|
|
||||||
"customTheme": {
|
|
||||||
"primary": "#ffcc00",
|
|
||||||
"secondary": "#00ccff",
|
|
||||||
"accent": { "dark": "#aa00ff", "light": "#ddccff" },
|
|
||||||
"error": "#ff0000"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Color Definition Formats
|
|
||||||
|
|
||||||
Custom theme colors support two formats:
|
|
||||||
|
|
||||||
1. **Simple Hex String**: A single hex color string (e.g., `"#aabbcc"`) that will be used for both light and dark terminal backgrounds.
|
|
||||||
|
|
||||||
2. **Adaptive Object**: An object with `dark` and `light` keys, each holding a hex color string. This allows for adaptive colors based on the terminal's background.
|
|
||||||
|
|
||||||
#### Available Color Keys
|
|
||||||
|
|
||||||
You can define any of the following color keys in your `customTheme`:
|
|
||||||
|
|
||||||
- Base colors: `primary`, `secondary`, `accent`
|
|
||||||
- Status colors: `error`, `warning`, `success`, `info`
|
|
||||||
- Text colors: `text`, `textMuted`, `textEmphasized`
|
|
||||||
- Background colors: `background`, `backgroundSecondary`, `backgroundDarker`
|
|
||||||
- Border colors: `borderNormal`, `borderFocused`, `borderDim`
|
|
||||||
- Diff view colors: `diffAdded`, `diffRemoved`, `diffContext`, etc.
|
|
||||||
|
|
||||||
You don't need to define all colors. Any undefined colors will fall back to the default "opencode" theme colors.
|
|
||||||
|
|
||||||
### Shell Configuration
|
### Shell Configuration
|
||||||
|
|
||||||
OpenCode allows you to configure the shell used by the `bash` tool. By default, it uses:
|
OpenCode allows you to configure the shell used by the `bash` tool. By default, it uses:
|
||||||
|
|
|
@ -5,6 +5,7 @@ import (
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
@ -51,7 +52,11 @@ func main() {
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
app_, err := app.New(ctx, httpClient)
|
version := Version
|
||||||
|
if version != "dev" && !strings.HasPrefix(Version, "v") {
|
||||||
|
version = "v" + Version
|
||||||
|
}
|
||||||
|
app_, err := app.New(ctx, version, httpClient)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,7 +21,6 @@ import (
|
||||||
type App struct {
|
type App struct {
|
||||||
ConfigPath string
|
ConfigPath string
|
||||||
Config *config.Config
|
Config *config.Config
|
||||||
Info *client.AppInfo
|
|
||||||
Client *client.ClientWithResponses
|
Client *client.ClientWithResponses
|
||||||
Provider *client.ProviderInfo
|
Provider *client.ProviderInfo
|
||||||
Model *client.ProviderModel
|
Model *client.ProviderModel
|
||||||
|
@ -34,7 +33,14 @@ type App struct {
|
||||||
completionDialogOpen bool
|
completionDialogOpen bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(ctx context.Context, httpClient *client.ClientWithResponses) (*App, error) {
|
type AppInfo struct {
|
||||||
|
client.AppInfo
|
||||||
|
Version string
|
||||||
|
}
|
||||||
|
|
||||||
|
var Info AppInfo
|
||||||
|
|
||||||
|
func New(ctx context.Context, version string, httpClient *client.ClientWithResponses) (*App, error) {
|
||||||
err := status.InitService()
|
err := status.InitService()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Error("Failed to initialize status service", "error", err)
|
slog.Error("Failed to initialize status service", "error", err)
|
||||||
|
@ -43,6 +49,12 @@ func New(ctx context.Context, httpClient *client.ClientWithResponses) (*App, err
|
||||||
|
|
||||||
appInfoResponse, _ := httpClient.PostAppInfoWithResponse(ctx)
|
appInfoResponse, _ := httpClient.PostAppInfoWithResponse(ctx)
|
||||||
appInfo := appInfoResponse.JSON200
|
appInfo := appInfoResponse.JSON200
|
||||||
|
Info = AppInfo{Version: version}
|
||||||
|
Info.Git = appInfo.Git
|
||||||
|
Info.Path = appInfo.Path
|
||||||
|
Info.Time = appInfo.Time
|
||||||
|
Info.User = appInfo.User
|
||||||
|
|
||||||
providersResponse, err := httpClient.PostProviderListWithResponse(ctx)
|
providersResponse, err := httpClient.PostProviderListWithResponse(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -70,7 +82,7 @@ func New(ctx context.Context, httpClient *client.ClientWithResponses) (*App, err
|
||||||
return nil, fmt.Errorf("no providers found")
|
return nil, fmt.Errorf("no providers found")
|
||||||
}
|
}
|
||||||
|
|
||||||
appConfigPath := filepath.Join(appInfo.Path.Config, "tui.toml")
|
appConfigPath := filepath.Join(Info.Path.Config, "tui.toml")
|
||||||
appConfig, err := config.LoadConfig(appConfigPath)
|
appConfig, err := config.LoadConfig(appConfigPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Info("No TUI config found, using default values", "error", err)
|
slog.Info("No TUI config found, using default values", "error", err)
|
||||||
|
@ -95,7 +107,6 @@ func New(ctx context.Context, httpClient *client.ClientWithResponses) (*App, err
|
||||||
app := &App{
|
app := &App{
|
||||||
ConfigPath: appConfigPath,
|
ConfigPath: appConfigPath,
|
||||||
Config: appConfig,
|
Config: appConfig,
|
||||||
Info: appInfo,
|
|
||||||
Client: httpClient,
|
Client: httpClient,
|
||||||
Provider: currentProvider,
|
Provider: currentProvider,
|
||||||
Model: currentModel,
|
Model: currentModel,
|
||||||
|
|
|
@ -5,8 +5,6 @@ import (
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"fmt"
|
"fmt"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/sst/opencode/pkg/client"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// MessageCache caches rendered messages to avoid re-rendering
|
// MessageCache caches rendered messages to avoid re-rendering
|
||||||
|
@ -23,51 +21,27 @@ func NewMessageCache() *MessageCache {
|
||||||
}
|
}
|
||||||
|
|
||||||
// generateKey creates a unique key for a message based on its content and rendering parameters
|
// generateKey creates a unique key for a message based on its content and rendering parameters
|
||||||
func (c *MessageCache) generateKey(msg client.MessageInfo, width int, showToolMessages bool, appInfo client.AppInfo) string {
|
func (c *MessageCache) GenerateKey(params ...any) string {
|
||||||
// Create a hash of the message content and rendering parameters
|
|
||||||
h := sha256.New()
|
h := sha256.New()
|
||||||
|
for _, param := range params {
|
||||||
// Include message ID and role
|
h.Write(fmt.Appendf(nil, ":%v", param))
|
||||||
h.Write(fmt.Appendf(nil, "%s:%s", msg.Id, msg.Role))
|
|
||||||
|
|
||||||
// Include timestamp
|
|
||||||
h.Write(fmt.Appendf(nil, ":%f", msg.Metadata.Time.Created))
|
|
||||||
|
|
||||||
// Include width and showToolMessages flag
|
|
||||||
h.Write(fmt.Appendf(nil, ":%d:%t", width, showToolMessages))
|
|
||||||
|
|
||||||
// Include app path for relative path calculations
|
|
||||||
h.Write([]byte(appInfo.Path.Root))
|
|
||||||
|
|
||||||
// Include message parts
|
|
||||||
for _, part := range msg.Parts {
|
|
||||||
h.Write(fmt.Appendf(nil, ":%v", part))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Include tool metadata if present
|
|
||||||
for toolID, metadata := range msg.Metadata.Tool {
|
|
||||||
h.Write(fmt.Appendf(nil, ":%s:%v", toolID, metadata))
|
|
||||||
}
|
|
||||||
|
|
||||||
return hex.EncodeToString(h.Sum(nil))
|
return hex.EncodeToString(h.Sum(nil))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get retrieves a cached rendered message
|
// Get retrieves a cached rendered message
|
||||||
func (c *MessageCache) Get(msg client.MessageInfo, width int, showToolMessages bool, appInfo client.AppInfo) (string, bool) {
|
func (c *MessageCache) Get(key string) (string, bool) {
|
||||||
c.mu.RLock()
|
c.mu.RLock()
|
||||||
defer c.mu.RUnlock()
|
defer c.mu.RUnlock()
|
||||||
|
|
||||||
key := c.generateKey(msg, width, showToolMessages, appInfo)
|
|
||||||
content, exists := c.cache[key]
|
content, exists := c.cache[key]
|
||||||
return content, exists
|
return content, exists
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set stores a rendered message in the cache
|
// Set stores a rendered message in the cache
|
||||||
func (c *MessageCache) Set(msg client.MessageInfo, width int, showToolMessages bool, appInfo client.AppInfo, content string) {
|
func (c *MessageCache) Set(key string, content string) {
|
||||||
c.mu.Lock()
|
c.mu.Lock()
|
||||||
defer c.mu.Unlock()
|
defer c.mu.Unlock()
|
||||||
|
|
||||||
key := c.generateKey(msg, width, showToolMessages, appInfo)
|
|
||||||
c.cache[key] = content
|
c.cache[key] = content
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,11 +1,6 @@
|
||||||
package chat
|
package chat
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"sort"
|
|
||||||
|
|
||||||
"github.com/charmbracelet/lipgloss"
|
|
||||||
"github.com/charmbracelet/x/ansi"
|
|
||||||
"github.com/sst/opencode/internal/app"
|
"github.com/sst/opencode/internal/app"
|
||||||
"github.com/sst/opencode/internal/styles"
|
"github.com/sst/opencode/internal/styles"
|
||||||
"github.com/sst/opencode/internal/theme"
|
"github.com/sst/opencode/internal/theme"
|
||||||
|
@ -16,100 +11,6 @@ type SendMsg struct {
|
||||||
Attachments []app.Attachment
|
Attachments []app.Attachment
|
||||||
}
|
}
|
||||||
|
|
||||||
func header(app *app.App, width int) string {
|
|
||||||
return lipgloss.JoinVertical(
|
|
||||||
lipgloss.Top,
|
|
||||||
logo(width),
|
|
||||||
repo(width),
|
|
||||||
"",
|
|
||||||
cwd(app, width),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
func lspsConfigured(width int) string {
|
|
||||||
// cfg := config.Get()
|
|
||||||
title := "LSP Servers"
|
|
||||||
title = ansi.Truncate(title, width, "…")
|
|
||||||
|
|
||||||
t := theme.CurrentTheme()
|
|
||||||
baseStyle := styles.BaseStyle()
|
|
||||||
|
|
||||||
lsps := baseStyle.
|
|
||||||
Width(width).
|
|
||||||
Foreground(t.Primary()).
|
|
||||||
Bold(true).
|
|
||||||
Render(title)
|
|
||||||
|
|
||||||
// Get LSP names and sort them for consistent ordering
|
|
||||||
var lspNames []string
|
|
||||||
// for name := range cfg.LSP {
|
|
||||||
// lspNames = append(lspNames, name)
|
|
||||||
// }
|
|
||||||
sort.Strings(lspNames)
|
|
||||||
|
|
||||||
var lspViews []string
|
|
||||||
// for _, name := range lspNames {
|
|
||||||
// lsp := cfg.LSP[name]
|
|
||||||
// lspName := baseStyle.
|
|
||||||
// Foreground(t.Text()).
|
|
||||||
// Render(fmt.Sprintf("• %s", name))
|
|
||||||
|
|
||||||
// cmd := lsp.Command
|
|
||||||
// cmd = ansi.Truncate(cmd, width-lipgloss.Width(lspName)-3, "…")
|
|
||||||
|
|
||||||
// lspPath := baseStyle.
|
|
||||||
// Foreground(t.TextMuted()).
|
|
||||||
// Render(fmt.Sprintf(" (%s)", cmd))
|
|
||||||
|
|
||||||
// lspViews = append(lspViews,
|
|
||||||
// baseStyle.
|
|
||||||
// Width(width).
|
|
||||||
// Render(
|
|
||||||
// lipgloss.JoinHorizontal(
|
|
||||||
// lipgloss.Left,
|
|
||||||
// lspName,
|
|
||||||
// lspPath,
|
|
||||||
// ),
|
|
||||||
// ),
|
|
||||||
// )
|
|
||||||
// }
|
|
||||||
|
|
||||||
return baseStyle.
|
|
||||||
Width(width).
|
|
||||||
Render(
|
|
||||||
lipgloss.JoinVertical(
|
|
||||||
lipgloss.Left,
|
|
||||||
lsps,
|
|
||||||
lipgloss.JoinVertical(
|
|
||||||
lipgloss.Left,
|
|
||||||
lspViews...,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
func logo(width int) string {
|
|
||||||
logo := fmt.Sprintf("%s %s", styles.OpenCodeIcon, "OpenCode")
|
|
||||||
t := theme.CurrentTheme()
|
|
||||||
baseStyle := styles.BaseStyle()
|
|
||||||
|
|
||||||
versionText := baseStyle.
|
|
||||||
Foreground(t.TextMuted()).
|
|
||||||
Render("v0.0.1") // TODO: get version from server
|
|
||||||
|
|
||||||
return baseStyle.
|
|
||||||
Bold(true).
|
|
||||||
Width(width).
|
|
||||||
Render(
|
|
||||||
lipgloss.JoinHorizontal(
|
|
||||||
lipgloss.Left,
|
|
||||||
logo,
|
|
||||||
" ",
|
|
||||||
versionText,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
func repo(width int) string {
|
func repo(width int) string {
|
||||||
repo := "github.com/sst/opencode"
|
repo := "github.com/sst/opencode"
|
||||||
t := theme.CurrentTheme()
|
t := theme.CurrentTheme()
|
||||||
|
@ -119,13 +20,3 @@ func repo(width int) string {
|
||||||
Width(width).
|
Width(width).
|
||||||
Render(repo)
|
Render(repo)
|
||||||
}
|
}
|
||||||
|
|
||||||
func cwd(app *app.App, width int) string {
|
|
||||||
cwd := fmt.Sprintf("cwd: %s", app.Info.Path.Cwd)
|
|
||||||
t := theme.CurrentTheme()
|
|
||||||
|
|
||||||
return styles.BaseStyle().
|
|
||||||
Foreground(t.TextMuted()).
|
|
||||||
Width(width).
|
|
||||||
Render(cwd)
|
|
||||||
}
|
|
||||||
|
|
|
@ -10,6 +10,7 @@ import (
|
||||||
"unicode"
|
"unicode"
|
||||||
|
|
||||||
"github.com/charmbracelet/bubbles/key"
|
"github.com/charmbracelet/bubbles/key"
|
||||||
|
"github.com/charmbracelet/bubbles/spinner"
|
||||||
"github.com/charmbracelet/bubbles/textarea"
|
"github.com/charmbracelet/bubbles/textarea"
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
"github.com/charmbracelet/lipgloss"
|
"github.com/charmbracelet/lipgloss"
|
||||||
|
@ -23,7 +24,7 @@ import (
|
||||||
"github.com/sst/opencode/internal/util"
|
"github.com/sst/opencode/internal/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
type editorCmp struct {
|
type editorComponent struct {
|
||||||
width int
|
width int
|
||||||
height int
|
height int
|
||||||
app *app.App
|
app *app.App
|
||||||
|
@ -33,6 +34,7 @@ type editorCmp struct {
|
||||||
history []string
|
history []string
|
||||||
historyIndex int
|
historyIndex int
|
||||||
currentMessage string
|
currentMessage string
|
||||||
|
spinner spinner.Model
|
||||||
}
|
}
|
||||||
|
|
||||||
type EditorKeyMaps struct {
|
type EditorKeyMaps struct {
|
||||||
|
@ -96,86 +98,19 @@ const (
|
||||||
maxAttachments = 5
|
maxAttachments = 5
|
||||||
)
|
)
|
||||||
|
|
||||||
func (m *editorCmp) openEditor(value string) tea.Cmd {
|
func (m *editorComponent) Init() tea.Cmd {
|
||||||
editor := os.Getenv("EDITOR")
|
return tea.Batch(textarea.Blink, m.spinner.Tick)
|
||||||
if editor == "" {
|
|
||||||
editor = "nvim"
|
|
||||||
}
|
|
||||||
|
|
||||||
tmpfile, err := os.CreateTemp("", "msg_*.md")
|
|
||||||
tmpfile.WriteString(value)
|
|
||||||
if err != nil {
|
|
||||||
status.Error(err.Error())
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
tmpfile.Close()
|
|
||||||
c := exec.Command(editor, tmpfile.Name()) //nolint:gosec
|
|
||||||
c.Stdin = os.Stdin
|
|
||||||
c.Stdout = os.Stdout
|
|
||||||
c.Stderr = os.Stderr
|
|
||||||
return tea.ExecProcess(c, func(err error) tea.Msg {
|
|
||||||
if err != nil {
|
|
||||||
status.Error(err.Error())
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
content, err := os.ReadFile(tmpfile.Name())
|
|
||||||
if err != nil {
|
|
||||||
status.Error(err.Error())
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if len(content) == 0 {
|
|
||||||
status.Warn("Message is empty")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
os.Remove(tmpfile.Name())
|
|
||||||
attachments := m.attachments
|
|
||||||
m.attachments = nil
|
|
||||||
return SendMsg{
|
|
||||||
Text: string(content),
|
|
||||||
Attachments: attachments,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *editorCmp) Init() tea.Cmd {
|
func (m *editorComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
return textarea.Blink
|
var cmds []tea.Cmd
|
||||||
}
|
|
||||||
|
|
||||||
func (m *editorCmp) send() tea.Cmd {
|
|
||||||
value := m.textarea.Value()
|
|
||||||
m.textarea.Reset()
|
|
||||||
attachments := m.attachments
|
|
||||||
|
|
||||||
// Save to history if not empty and not a duplicate of the last entry
|
|
||||||
if value != "" {
|
|
||||||
if len(m.history) == 0 || m.history[len(m.history)-1] != value {
|
|
||||||
m.history = append(m.history, value)
|
|
||||||
}
|
|
||||||
m.historyIndex = len(m.history)
|
|
||||||
m.currentMessage = ""
|
|
||||||
}
|
|
||||||
|
|
||||||
m.attachments = nil
|
|
||||||
if value == "" {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return tea.Batch(
|
|
||||||
util.CmdHandler(SendMsg{
|
|
||||||
Text: value,
|
|
||||||
Attachments: attachments,
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *editorCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|
||||||
var cmd tea.Cmd
|
var cmd tea.Cmd
|
||||||
switch msg := msg.(type) {
|
switch msg := msg.(type) {
|
||||||
case dialog.ThemeChangedMsg:
|
case dialog.ThemeChangedMsg:
|
||||||
m.textarea = CreateTextArea(&m.textarea)
|
m.textarea = createTextArea(&m.textarea)
|
||||||
case dialog.CompletionSelectedMsg:
|
case dialog.CompletionSelectedMsg:
|
||||||
existingValue := m.textarea.Value()
|
existingValue := m.textarea.Value()
|
||||||
modifiedValue := strings.Replace(existingValue, msg.SearchString, msg.CompletionValue, 1)
|
modifiedValue := strings.Replace(existingValue, msg.SearchString, msg.CompletionValue, 1)
|
||||||
|
|
||||||
m.textarea.SetValue(modifiedValue)
|
m.textarea.SetValue(modifiedValue)
|
||||||
return m, nil
|
return m, nil
|
||||||
case dialog.AttachmentAddedMsg:
|
case dialog.AttachmentAddedMsg:
|
||||||
|
@ -296,47 +231,160 @@ func (m *editorCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
return m, m.send()
|
return m, m.send()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
m.spinner, cmd = m.spinner.Update(msg)
|
||||||
|
cmds = append(cmds, cmd)
|
||||||
|
|
||||||
m.textarea, cmd = m.textarea.Update(msg)
|
m.textarea, cmd = m.textarea.Update(msg)
|
||||||
return m, cmd
|
cmds = append(cmds, cmd)
|
||||||
|
|
||||||
|
return m, tea.Batch(cmds...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *editorCmp) View() string {
|
func (m *editorComponent) View() string {
|
||||||
t := theme.CurrentTheme()
|
t := theme.CurrentTheme()
|
||||||
|
base := styles.BaseStyle().Render
|
||||||
// Style the prompt with theme colors
|
muted := styles.Muted().Render
|
||||||
style := lipgloss.NewStyle().
|
promptStyle := lipgloss.NewStyle().
|
||||||
Padding(0, 0, 0, 1).
|
Padding(0, 0, 0, 1).
|
||||||
Bold(true).
|
Bold(true).
|
||||||
Foreground(t.Primary())
|
Foreground(t.Primary())
|
||||||
|
prompt := promptStyle.Render(">")
|
||||||
|
|
||||||
if len(m.attachments) == 0 {
|
textarea := lipgloss.JoinHorizontal(
|
||||||
return lipgloss.JoinHorizontal(lipgloss.Top, style.Render(">"), m.textarea.View())
|
lipgloss.Top,
|
||||||
|
prompt,
|
||||||
|
m.textarea.View(),
|
||||||
|
)
|
||||||
|
textarea = styles.BaseStyle().
|
||||||
|
Width(m.width-2).
|
||||||
|
Border(lipgloss.NormalBorder(), true, true).
|
||||||
|
BorderForeground(t.Border()).
|
||||||
|
Render(textarea)
|
||||||
|
|
||||||
|
hint := base("enter") + muted(" send ") + base("shift") + muted("+") + base("enter") + muted(" newline")
|
||||||
|
if m.app.IsBusy() {
|
||||||
|
hint = muted("working") + m.spinner.View() + muted(" ") + base("esc") + muted(" interrupt")
|
||||||
}
|
}
|
||||||
m.textarea.SetHeight(m.height - 1)
|
|
||||||
return lipgloss.JoinVertical(lipgloss.Top,
|
model := ""
|
||||||
m.attachmentsContent(),
|
if m.app.Model != nil {
|
||||||
lipgloss.JoinHorizontal(lipgloss.Top, style.Render(">"),
|
model = base(*m.app.Model.Name) + muted(" • /model")
|
||||||
m.textarea.View()),
|
}
|
||||||
|
|
||||||
|
space := m.width - 2 - lipgloss.Width(model) - lipgloss.Width(hint)
|
||||||
|
spacer := lipgloss.NewStyle().Width(space).Render("")
|
||||||
|
|
||||||
|
info := lipgloss.JoinHorizontal(lipgloss.Left, hint, spacer, model)
|
||||||
|
info = styles.Padded().Render(info)
|
||||||
|
|
||||||
|
content := lipgloss.JoinVertical(
|
||||||
|
lipgloss.Top,
|
||||||
|
// m.attachmentsContent(),
|
||||||
|
textarea,
|
||||||
|
info,
|
||||||
|
)
|
||||||
|
|
||||||
|
return styles.ForceReplaceBackgroundWithLipgloss(
|
||||||
|
content,
|
||||||
|
t.Background(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *editorCmp) SetSize(width, height int) tea.Cmd {
|
func (m *editorComponent) SetSize(width, height int) tea.Cmd {
|
||||||
m.width = width
|
m.width = width
|
||||||
m.height = height
|
m.height = height
|
||||||
m.textarea.SetWidth(width - 3) // account for the prompt and padding right
|
m.textarea.SetWidth(width - 5) // account for the prompt and padding right
|
||||||
m.textarea.SetHeight(height)
|
m.textarea.SetHeight(height - 3) // account for info underneath
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *editorCmp) GetSize() (int, int) {
|
func (m *editorComponent) GetSize() (int, int) {
|
||||||
return m.textarea.Width(), m.textarea.Height()
|
return m.width, m.height
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *editorCmp) attachmentsContent() string {
|
func (m *editorComponent) BindingKeys() []key.Binding {
|
||||||
var styledAttachments []string
|
bindings := []key.Binding{}
|
||||||
|
bindings = append(bindings, layout.KeyMapToSlice(editorMaps)...)
|
||||||
|
bindings = append(bindings, layout.KeyMapToSlice(DeleteKeyMaps)...)
|
||||||
|
return bindings
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *editorComponent) openEditor(value string) tea.Cmd {
|
||||||
|
editor := os.Getenv("EDITOR")
|
||||||
|
if editor == "" {
|
||||||
|
editor = "nvim"
|
||||||
|
}
|
||||||
|
|
||||||
|
tmpfile, err := os.CreateTemp("", "msg_*.md")
|
||||||
|
tmpfile.WriteString(value)
|
||||||
|
if err != nil {
|
||||||
|
status.Error(err.Error())
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
tmpfile.Close()
|
||||||
|
c := exec.Command(editor, tmpfile.Name()) //nolint:gosec
|
||||||
|
c.Stdin = os.Stdin
|
||||||
|
c.Stdout = os.Stdout
|
||||||
|
c.Stderr = os.Stderr
|
||||||
|
return tea.ExecProcess(c, func(err error) tea.Msg {
|
||||||
|
if err != nil {
|
||||||
|
status.Error(err.Error())
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
content, err := os.ReadFile(tmpfile.Name())
|
||||||
|
if err != nil {
|
||||||
|
status.Error(err.Error())
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if len(content) == 0 {
|
||||||
|
status.Warn("Message is empty")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
os.Remove(tmpfile.Name())
|
||||||
|
attachments := m.attachments
|
||||||
|
m.attachments = nil
|
||||||
|
return SendMsg{
|
||||||
|
Text: string(content),
|
||||||
|
Attachments: attachments,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *editorComponent) send() tea.Cmd {
|
||||||
|
value := m.textarea.Value()
|
||||||
|
m.textarea.Reset()
|
||||||
|
attachments := m.attachments
|
||||||
|
|
||||||
|
// Save to history if not empty and not a duplicate of the last entry
|
||||||
|
if value != "" {
|
||||||
|
if len(m.history) == 0 || m.history[len(m.history)-1] != value {
|
||||||
|
m.history = append(m.history, value)
|
||||||
|
}
|
||||||
|
m.historyIndex = len(m.history)
|
||||||
|
m.currentMessage = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
m.attachments = nil
|
||||||
|
if value == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return tea.Batch(
|
||||||
|
util.CmdHandler(SendMsg{
|
||||||
|
Text: value,
|
||||||
|
Attachments: attachments,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *editorComponent) attachmentsContent() string {
|
||||||
|
if len(m.attachments) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
t := theme.CurrentTheme()
|
t := theme.CurrentTheme()
|
||||||
|
var styledAttachments []string
|
||||||
attachmentStyles := styles.BaseStyle().
|
attachmentStyles := styles.BaseStyle().
|
||||||
MarginLeft(1).
|
MarginLeft(1).
|
||||||
Background(t.TextMuted()).
|
Background(t.TextMuted()).
|
||||||
|
@ -357,20 +405,15 @@ func (m *editorCmp) attachmentsContent() string {
|
||||||
return content
|
return content
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *editorCmp) BindingKeys() []key.Binding {
|
func createTextArea(existing *textarea.Model) textarea.Model {
|
||||||
bindings := []key.Binding{}
|
|
||||||
bindings = append(bindings, layout.KeyMapToSlice(editorMaps)...)
|
|
||||||
bindings = append(bindings, layout.KeyMapToSlice(DeleteKeyMaps)...)
|
|
||||||
return bindings
|
|
||||||
}
|
|
||||||
|
|
||||||
func CreateTextArea(existing *textarea.Model) textarea.Model {
|
|
||||||
t := theme.CurrentTheme()
|
t := theme.CurrentTheme()
|
||||||
bgColor := t.Background()
|
bgColor := t.Background()
|
||||||
textColor := t.Text()
|
textColor := t.Text()
|
||||||
textMutedColor := t.TextMuted()
|
textMutedColor := t.TextMuted()
|
||||||
|
|
||||||
ta := textarea.New()
|
ta := textarea.New()
|
||||||
|
ta.Placeholder = "It's prompting time..."
|
||||||
|
|
||||||
ta.BlurredStyle.Base = styles.BaseStyle().Background(bgColor).Foreground(textColor)
|
ta.BlurredStyle.Base = styles.BaseStyle().Background(bgColor).Foreground(textColor)
|
||||||
ta.BlurredStyle.CursorLine = styles.BaseStyle().Background(bgColor)
|
ta.BlurredStyle.CursorLine = styles.BaseStyle().Background(bgColor)
|
||||||
ta.BlurredStyle.Placeholder = styles.BaseStyle().Background(bgColor).Foreground(textMutedColor)
|
ta.BlurredStyle.Placeholder = styles.BaseStyle().Background(bgColor).Foreground(textMutedColor)
|
||||||
|
@ -394,13 +437,16 @@ func CreateTextArea(existing *textarea.Model) textarea.Model {
|
||||||
return ta
|
return ta
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewEditorCmp(app *app.App) tea.Model {
|
func NewEditorComponent(app *app.App) tea.Model {
|
||||||
ta := CreateTextArea(nil)
|
s := spinner.New(spinner.WithSpinner(spinner.Ellipsis), spinner.WithStyle(styles.Muted().Width(3)))
|
||||||
return &editorCmp{
|
ta := createTextArea(nil)
|
||||||
|
|
||||||
|
return &editorComponent{
|
||||||
app: app,
|
app: app,
|
||||||
textarea: ta,
|
textarea: ta,
|
||||||
history: []string{},
|
history: []string{},
|
||||||
historyIndex: 0,
|
historyIndex: 0,
|
||||||
currentMessage: "",
|
currentMessage: "",
|
||||||
|
spinner: s,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,13 +2,18 @@ package chat
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
"unicode"
|
||||||
|
|
||||||
"github.com/charmbracelet/lipgloss"
|
"github.com/charmbracelet/lipgloss"
|
||||||
"github.com/charmbracelet/x/ansi"
|
"github.com/charmbracelet/x/ansi"
|
||||||
|
"github.com/sst/opencode/internal/app"
|
||||||
"github.com/sst/opencode/internal/components/diff"
|
"github.com/sst/opencode/internal/components/diff"
|
||||||
|
"github.com/sst/opencode/internal/layout"
|
||||||
"github.com/sst/opencode/internal/styles"
|
"github.com/sst/opencode/internal/styles"
|
||||||
"github.com/sst/opencode/internal/theme"
|
"github.com/sst/opencode/internal/theme"
|
||||||
"github.com/sst/opencode/pkg/client"
|
"github.com/sst/opencode/pkg/client"
|
||||||
|
@ -16,14 +21,12 @@ import (
|
||||||
"golang.org/x/text/language"
|
"golang.org/x/text/language"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
|
||||||
maxResultHeight = 10
|
|
||||||
)
|
|
||||||
|
|
||||||
func toMarkdown(content string, width int) string {
|
func toMarkdown(content string, width int) string {
|
||||||
r := styles.GetMarkdownRenderer(width)
|
r := styles.GetMarkdownRenderer(width)
|
||||||
|
content = strings.ReplaceAll(content, app.Info.Path.Root+"/", "")
|
||||||
rendered, _ := r.Render(content)
|
rendered, _ := r.Render(content)
|
||||||
lines := strings.Split(rendered, "\n")
|
lines := strings.Split(rendered, "\n")
|
||||||
|
|
||||||
if len(lines) > 0 {
|
if len(lines) > 0 {
|
||||||
firstLine := lines[0]
|
firstLine := lines[0]
|
||||||
cleaned := ansi.Strip(firstLine)
|
cleaned := ansi.Strip(firstLine)
|
||||||
|
@ -40,139 +43,204 @@ func toMarkdown(content string, width int) string {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return strings.TrimSuffix(strings.Join(lines, "\n"), "\n")
|
|
||||||
|
content = strings.Join(lines, "\n")
|
||||||
|
return strings.TrimSuffix(content, "\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
func renderUserMessage(user string, msg client.MessageInfo, width int) string {
|
type markdownRenderer struct {
|
||||||
|
align *lipgloss.Position
|
||||||
|
borderColor *lipgloss.AdaptiveColor
|
||||||
|
fullWidth bool
|
||||||
|
paddingTop int
|
||||||
|
paddingBottom int
|
||||||
|
}
|
||||||
|
|
||||||
|
type markdownRenderingOption func(*markdownRenderer)
|
||||||
|
|
||||||
|
func WithFullWidth() markdownRenderingOption {
|
||||||
|
return func(c *markdownRenderer) {
|
||||||
|
c.fullWidth = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithAlign(align lipgloss.Position) markdownRenderingOption {
|
||||||
|
return func(c *markdownRenderer) {
|
||||||
|
c.align = &align
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithBorderColor(color lipgloss.AdaptiveColor) markdownRenderingOption {
|
||||||
|
return func(c *markdownRenderer) {
|
||||||
|
c.borderColor = &color
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithPaddingTop(padding int) markdownRenderingOption {
|
||||||
|
return func(c *markdownRenderer) {
|
||||||
|
c.paddingTop = padding
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithPaddingBottom(padding int) markdownRenderingOption {
|
||||||
|
return func(c *markdownRenderer) {
|
||||||
|
c.paddingBottom = padding
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func renderMarkdown(content string, options ...markdownRenderingOption) string {
|
||||||
t := theme.CurrentTheme()
|
t := theme.CurrentTheme()
|
||||||
|
renderer := &markdownRenderer{
|
||||||
|
fullWidth: false,
|
||||||
|
}
|
||||||
|
for _, option := range options {
|
||||||
|
option(renderer)
|
||||||
|
}
|
||||||
|
|
||||||
style := styles.BaseStyle().
|
style := styles.BaseStyle().
|
||||||
PaddingLeft(1).
|
PaddingTop(1).
|
||||||
BorderLeft(true).
|
PaddingBottom(1).
|
||||||
|
PaddingLeft(2).
|
||||||
|
PaddingRight(2).
|
||||||
|
Background(t.BackgroundSubtle()).
|
||||||
Foreground(t.TextMuted()).
|
Foreground(t.TextMuted()).
|
||||||
BorderForeground(t.Secondary()).
|
|
||||||
BorderStyle(lipgloss.ThickBorder())
|
BorderStyle(lipgloss.ThickBorder())
|
||||||
|
|
||||||
// var styledAttachments []string
|
align := lipgloss.Left
|
||||||
// attachmentStyles := baseStyle.
|
if renderer.align != nil {
|
||||||
// MarginLeft(1).
|
align = *renderer.align
|
||||||
// Background(t.TextMuted()).
|
}
|
||||||
// Foreground(t.Text())
|
|
||||||
// for _, attachment := range msg.BinaryContent() {
|
|
||||||
// file := filepath.Base(attachment.Path)
|
|
||||||
// var filename string
|
|
||||||
// if len(file) > 10 {
|
|
||||||
// filename = fmt.Sprintf(" %s %s...", styles.DocumentIcon, file[0:7])
|
|
||||||
// } else {
|
|
||||||
// filename = fmt.Sprintf(" %s %s", styles.DocumentIcon, file)
|
|
||||||
// }
|
|
||||||
// styledAttachments = append(styledAttachments, attachmentStyles.Render(filename))
|
|
||||||
// }
|
|
||||||
|
|
||||||
timestamp := time.UnixMilli(int64(msg.Metadata.Time.Created)).Local().Format("02 Jan 2006 03:04 PM")
|
borderColor := t.BackgroundSubtle()
|
||||||
|
if renderer.borderColor != nil {
|
||||||
|
borderColor = *renderer.borderColor
|
||||||
|
}
|
||||||
|
|
||||||
|
switch align {
|
||||||
|
case lipgloss.Left:
|
||||||
|
style = style.
|
||||||
|
BorderLeft(true).
|
||||||
|
BorderRight(true).
|
||||||
|
AlignHorizontal(align).
|
||||||
|
BorderLeftForeground(borderColor).
|
||||||
|
BorderLeftBackground(t.Background()).
|
||||||
|
BorderRightForeground(t.BackgroundSubtle()).
|
||||||
|
BorderRightBackground(t.Background())
|
||||||
|
case lipgloss.Right:
|
||||||
|
style = style.
|
||||||
|
BorderRight(true).
|
||||||
|
BorderLeft(true).
|
||||||
|
AlignHorizontal(align).
|
||||||
|
BorderRightForeground(borderColor).
|
||||||
|
BorderRightBackground(t.Background()).
|
||||||
|
BorderLeftForeground(t.BackgroundSubtle()).
|
||||||
|
BorderLeftBackground(t.Background())
|
||||||
|
}
|
||||||
|
|
||||||
|
content = styles.ForceReplaceBackgroundWithLipgloss(content, t.BackgroundSubtle())
|
||||||
|
if renderer.fullWidth {
|
||||||
|
style = style.Width(layout.Current.Container.Width - 2)
|
||||||
|
}
|
||||||
|
content = style.Render(content)
|
||||||
|
if renderer.paddingTop > 0 {
|
||||||
|
content = strings.Repeat("\n", renderer.paddingTop) + content
|
||||||
|
}
|
||||||
|
if renderer.paddingBottom > 0 {
|
||||||
|
content = content + strings.Repeat("\n", renderer.paddingBottom)
|
||||||
|
}
|
||||||
|
content = lipgloss.PlaceHorizontal(
|
||||||
|
layout.Current.Container.Width,
|
||||||
|
align,
|
||||||
|
content,
|
||||||
|
lipgloss.WithWhitespaceBackground(t.Background()),
|
||||||
|
)
|
||||||
|
content = lipgloss.PlaceHorizontal(
|
||||||
|
layout.Current.Viewport.Width,
|
||||||
|
lipgloss.Center,
|
||||||
|
content,
|
||||||
|
lipgloss.WithWhitespaceBackground(t.Background()),
|
||||||
|
)
|
||||||
|
return content
|
||||||
|
}
|
||||||
|
|
||||||
|
func renderText(message client.MessageInfo, text string, author string) string {
|
||||||
|
t := theme.CurrentTheme()
|
||||||
|
width := layout.Current.Container.Width
|
||||||
|
padding := 0
|
||||||
|
switch layout.Current.Size {
|
||||||
|
case layout.LayoutSizeSmall:
|
||||||
|
padding = 5
|
||||||
|
case layout.LayoutSizeNormal:
|
||||||
|
padding = 10
|
||||||
|
case layout.LayoutSizeLarge:
|
||||||
|
padding = 15
|
||||||
|
}
|
||||||
|
|
||||||
|
timestamp := time.UnixMilli(int64(message.Metadata.Time.Created)).Local().Format("02 Jan 2006 03:04 PM")
|
||||||
if time.Now().Format("02 Jan 2006") == timestamp[:11] {
|
if time.Now().Format("02 Jan 2006") == timestamp[:11] {
|
||||||
|
// don't show the date if it's today
|
||||||
timestamp = timestamp[12:]
|
timestamp = timestamp[12:]
|
||||||
}
|
}
|
||||||
info := styles.BaseStyle().
|
info := styles.BaseStyle().
|
||||||
Foreground(t.TextMuted()).
|
Foreground(t.TextMuted()).
|
||||||
Render(fmt.Sprintf("%s (%s)", user, timestamp))
|
Render(fmt.Sprintf("%s (%s)", author, timestamp))
|
||||||
|
|
||||||
content := ""
|
align := lipgloss.Left
|
||||||
// if len(styledAttachments) > 0 {
|
switch message.Role {
|
||||||
// attachmentContent := baseStyle.Width(width).Render(lipgloss.JoinHorizontal(lipgloss.Left, styledAttachments...))
|
case client.User:
|
||||||
// content = renderMessage(msg.Content().String(), true, isFocused, width, append(info, attachmentContent)...)
|
align = lipgloss.Right
|
||||||
// } else {
|
case client.Assistant:
|
||||||
for _, p := range msg.Parts {
|
align = lipgloss.Left
|
||||||
part, err := p.ValueByDiscriminator()
|
|
||||||
if err != nil {
|
|
||||||
continue //TODO: handle error?
|
|
||||||
}
|
|
||||||
|
|
||||||
switch part.(type) {
|
|
||||||
case client.MessagePartText:
|
|
||||||
textPart := part.(client.MessagePartText)
|
|
||||||
text := toMarkdown(textPart.Text, width)
|
|
||||||
content = style.Render(lipgloss.JoinVertical(lipgloss.Left, text, info))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return styles.ForceReplaceBackgroundWithLipgloss(content, t.Background())
|
textWidth := lipgloss.Width(text)
|
||||||
|
markdownWidth := min(textWidth, width-padding-4) // -4 for the border and padding
|
||||||
|
content := toMarkdown(text, markdownWidth)
|
||||||
|
content = lipgloss.JoinVertical(align, content, info)
|
||||||
|
|
||||||
|
switch message.Role {
|
||||||
|
case client.User:
|
||||||
|
return renderMarkdown(content,
|
||||||
|
WithAlign(lipgloss.Right),
|
||||||
|
WithBorderColor(t.Secondary()),
|
||||||
|
)
|
||||||
|
case client.Assistant:
|
||||||
|
return renderMarkdown(content,
|
||||||
|
WithAlign(lipgloss.Left),
|
||||||
|
WithBorderColor(t.Primary()),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
func renderAssistantMessage(
|
func renderToolInvocation(
|
||||||
msg client.MessageInfo,
|
toolCall client.MessageToolInvocationToolCall,
|
||||||
width int,
|
result *string,
|
||||||
showToolMessages bool,
|
metadata map[string]any,
|
||||||
appInfo client.AppInfo,
|
showResult bool,
|
||||||
) string {
|
) string {
|
||||||
t := theme.CurrentTheme()
|
ignoredTools := []string{"opencode_todoread"}
|
||||||
style := styles.BaseStyle().
|
if slices.Contains(ignoredTools, toolCall.ToolName) {
|
||||||
PaddingLeft(1).
|
return ""
|
||||||
BorderLeft(true).
|
|
||||||
Foreground(t.TextMuted()).
|
|
||||||
BorderForeground(t.Primary()).
|
|
||||||
BorderStyle(lipgloss.ThickBorder())
|
|
||||||
messages := []string{}
|
|
||||||
|
|
||||||
timestamp := time.UnixMilli(int64(msg.Metadata.Time.Created)).Local().Format("02 Jan 2006 03:04 PM")
|
|
||||||
if time.Now().Format("02 Jan 2006") == timestamp[:11] {
|
|
||||||
timestamp = timestamp[12:]
|
|
||||||
}
|
|
||||||
modelName := msg.Metadata.Assistant.ModelID
|
|
||||||
info := styles.BaseStyle().
|
|
||||||
Foreground(t.TextMuted()).
|
|
||||||
Render(fmt.Sprintf("%s (%s)", modelName, timestamp))
|
|
||||||
|
|
||||||
for _, p := range msg.Parts {
|
|
||||||
part, err := p.ValueByDiscriminator()
|
|
||||||
if err != nil {
|
|
||||||
continue //TODO: handle error?
|
|
||||||
}
|
|
||||||
|
|
||||||
switch part.(type) {
|
|
||||||
// case client.MessagePartReasoning:
|
|
||||||
// reasoningPart := part.(client.MessagePartReasoning)
|
|
||||||
|
|
||||||
case client.MessagePartText:
|
|
||||||
textPart := part.(client.MessagePartText)
|
|
||||||
text := toMarkdown(textPart.Text, width)
|
|
||||||
content := style.Render(lipgloss.JoinVertical(lipgloss.Left, text, info))
|
|
||||||
message := styles.ForceReplaceBackgroundWithLipgloss(content, t.Background())
|
|
||||||
messages = append(messages, message)
|
|
||||||
|
|
||||||
case client.MessagePartToolInvocation:
|
|
||||||
if !showToolMessages {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
toolInvocationPart := part.(client.MessagePartToolInvocation)
|
|
||||||
toolCall, _ := toolInvocationPart.ToolInvocation.AsMessageToolInvocationToolCall()
|
|
||||||
var result *string
|
|
||||||
resultPart, resultError := toolInvocationPart.ToolInvocation.AsMessageToolInvocationToolResult()
|
|
||||||
if resultError == nil {
|
|
||||||
result = &resultPart.Result
|
|
||||||
}
|
|
||||||
metadata := map[string]any{}
|
|
||||||
if _, ok := msg.Metadata.Tool[toolCall.ToolCallId]; ok {
|
|
||||||
metadata = msg.Metadata.Tool[toolCall.ToolCallId].(map[string]any)
|
|
||||||
}
|
|
||||||
message := renderToolInvocation(toolCall, result, metadata, appInfo, width)
|
|
||||||
messages = append(messages, message)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return strings.Join(messages, "\n\n")
|
padding := 1
|
||||||
}
|
outerWidth := layout.Current.Container.Width - 1 // subtract 1 for the border
|
||||||
|
innerWidth := outerWidth - padding - 4 // -4 for the border and padding
|
||||||
|
|
||||||
func renderToolInvocation(toolCall client.MessageToolInvocationToolCall, result *string, metadata map[string]any, appInfo client.AppInfo, width int) string {
|
|
||||||
t := theme.CurrentTheme()
|
t := theme.CurrentTheme()
|
||||||
style := styles.BaseStyle().
|
style := styles.Muted().
|
||||||
|
Width(outerWidth).
|
||||||
|
PaddingLeft(padding).
|
||||||
BorderLeft(true).
|
BorderLeft(true).
|
||||||
PaddingLeft(1).
|
BorderForeground(t.BorderSubtle()).
|
||||||
Foreground(t.TextMuted()).
|
|
||||||
BorderForeground(t.TextMuted()).
|
|
||||||
BorderStyle(lipgloss.ThickBorder())
|
BorderStyle(lipgloss.ThickBorder())
|
||||||
|
|
||||||
toolName := renderToolName(toolCall.ToolName)
|
if toolCall.State == "partial-call" {
|
||||||
|
style = style.Foreground(t.TextMuted())
|
||||||
|
return style.Render(renderToolAction(toolCall.ToolName))
|
||||||
|
}
|
||||||
|
|
||||||
toolArgs := ""
|
toolArgs := ""
|
||||||
toolArgsMap := make(map[string]any)
|
toolArgsMap := make(map[string]any)
|
||||||
if toolCall.Args != nil {
|
if toolCall.Args != nil {
|
||||||
|
@ -185,17 +253,20 @@ func renderToolInvocation(toolCall client.MessageToolInvocationToolCall, result
|
||||||
firstKey = key
|
firstKey = key
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
toolArgs = renderArgs(&toolArgsMap, appInfo, firstKey)
|
toolArgs = renderArgs(&toolArgsMap, firstKey)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
title := fmt.Sprintf("%s: %s", toolName, toolArgs)
|
if len(toolArgsMap) == 0 {
|
||||||
finished := result != nil
|
slog.Debug("no args")
|
||||||
body := styles.BaseStyle().Render("In progress...")
|
}
|
||||||
|
|
||||||
|
body := ""
|
||||||
|
finished := result != nil && *result != ""
|
||||||
if finished {
|
if finished {
|
||||||
body = *result
|
body = *result
|
||||||
}
|
}
|
||||||
footer := ""
|
elapsed := ""
|
||||||
if metadata["time"] != nil {
|
if metadata["time"] != nil {
|
||||||
timeMap := metadata["time"].(map[string]any)
|
timeMap := metadata["time"].(map[string]any)
|
||||||
start := timeMap["start"].(float64)
|
start := timeMap["start"].(float64)
|
||||||
|
@ -206,84 +277,54 @@ func renderToolInvocation(toolCall client.MessageToolInvocationToolCall, result
|
||||||
if durationMs > 1000 {
|
if durationMs > 1000 {
|
||||||
roundedDuration = time.Duration(duration.Round(time.Second))
|
roundedDuration = time.Duration(duration.Round(time.Second))
|
||||||
}
|
}
|
||||||
footer = styles.Muted().Render(fmt.Sprintf("%s", roundedDuration))
|
elapsed = styles.Muted().Render(roundedDuration.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
title := ""
|
||||||
switch toolCall.ToolName {
|
switch toolCall.ToolName {
|
||||||
|
case "opencode_read":
|
||||||
|
toolArgs = renderArgs(&toolArgsMap, "filePath")
|
||||||
|
title = fmt.Sprintf("Read: %s %s", toolArgs, elapsed)
|
||||||
|
body = ""
|
||||||
|
filename := toolArgsMap["filePath"].(string)
|
||||||
|
if metadata["preview"] != nil {
|
||||||
|
body = metadata["preview"].(string)
|
||||||
|
body = renderFile(filename, body, WithTruncate(6))
|
||||||
|
}
|
||||||
case "opencode_edit":
|
case "opencode_edit":
|
||||||
filename := toolArgsMap["filePath"].(string)
|
filename := toolArgsMap["filePath"].(string)
|
||||||
filename = strings.TrimPrefix(filename, appInfo.Path.Root+"/")
|
title = fmt.Sprintf("Edit: %s %s", relative(filename), elapsed)
|
||||||
title = fmt.Sprintf("%s: %s", toolName, filename)
|
if metadata["diff"] != nil {
|
||||||
if finished && metadata["diff"] != nil {
|
|
||||||
patch := metadata["diff"].(string)
|
patch := metadata["diff"].(string)
|
||||||
formattedDiff, _ := diff.FormatDiff(patch, diff.WithTotalWidth(width))
|
diffWidth := min(layout.Current.Viewport.Width, 120)
|
||||||
|
formattedDiff, _ := diff.FormatDiff(filename, patch, diff.WithTotalWidth(diffWidth))
|
||||||
body = strings.TrimSpace(formattedDiff)
|
body = strings.TrimSpace(formattedDiff)
|
||||||
return style.Render(lipgloss.JoinVertical(lipgloss.Left,
|
body = lipgloss.Place(
|
||||||
title,
|
layout.Current.Viewport.Width,
|
||||||
|
lipgloss.Height(body)+2,
|
||||||
|
lipgloss.Center,
|
||||||
|
lipgloss.Center,
|
||||||
body,
|
body,
|
||||||
styles.ForceReplaceBackgroundWithLipgloss(footer, t.Background()),
|
lipgloss.WithWhitespaceBackground(t.Background()),
|
||||||
))
|
)
|
||||||
}
|
|
||||||
case "opencode_read":
|
|
||||||
toolArgs = renderArgs(&toolArgsMap, appInfo, "filePath")
|
|
||||||
title = fmt.Sprintf("%s: %s", toolName, toolArgs)
|
|
||||||
filename := toolArgsMap["filePath"].(string)
|
|
||||||
ext := filepath.Ext(filename)
|
|
||||||
if ext == "" {
|
|
||||||
ext = ""
|
|
||||||
} else {
|
|
||||||
ext = strings.ToLower(ext[1:])
|
|
||||||
}
|
|
||||||
if finished {
|
|
||||||
if metadata["preview"] != nil {
|
|
||||||
body = metadata["preview"].(string)
|
|
||||||
}
|
|
||||||
body = fmt.Sprintf("```%s\n%s\n```", ext, truncateHeight(body, 10))
|
|
||||||
body = toMarkdown(body, width)
|
|
||||||
}
|
}
|
||||||
case "opencode_write":
|
case "opencode_write":
|
||||||
filename := toolArgsMap["filePath"].(string)
|
filename := toolArgsMap["filePath"].(string)
|
||||||
filename = strings.TrimPrefix(filename, appInfo.Path.Root+"/")
|
title = fmt.Sprintf("Write: %s %s", relative(filename), elapsed)
|
||||||
title = fmt.Sprintf("%s: %s", toolName, filename)
|
|
||||||
ext := filepath.Ext(filename)
|
|
||||||
if ext == "" {
|
|
||||||
ext = ""
|
|
||||||
} else {
|
|
||||||
ext = strings.ToLower(ext[1:])
|
|
||||||
}
|
|
||||||
content := toolArgsMap["content"].(string)
|
content := toolArgsMap["content"].(string)
|
||||||
body = fmt.Sprintf("```%s\n%s\n```", ext, truncateHeight(content, 10))
|
body = renderFile(filename, content)
|
||||||
body = toMarkdown(body, width)
|
|
||||||
case "opencode_bash":
|
case "opencode_bash":
|
||||||
if finished && metadata["stdout"] != nil {
|
description := toolArgsMap["description"].(string)
|
||||||
description := toolArgsMap["description"].(string)
|
title = fmt.Sprintf("Shell: %s %s", description, elapsed)
|
||||||
title = fmt.Sprintf("%s: %s", toolName, description)
|
if metadata["stdout"] != nil {
|
||||||
command := toolArgsMap["command"].(string)
|
command := toolArgsMap["command"].(string)
|
||||||
stdout := metadata["stdout"].(string)
|
stdout := metadata["stdout"].(string)
|
||||||
body = fmt.Sprintf("```console\n$ %s\n%s```", command, stdout)
|
body = fmt.Sprintf("```console\n> %s\n%s```", command, stdout)
|
||||||
body = toMarkdown(body, width)
|
body = toMarkdown(body, innerWidth)
|
||||||
}
|
body = renderMarkdown(body, WithFullWidth(), WithPaddingTop(1), WithPaddingBottom(1))
|
||||||
case "opencode_todoread":
|
|
||||||
title = fmt.Sprintf("%s", toolName)
|
|
||||||
if finished && metadata["todos"] != nil {
|
|
||||||
body = ""
|
|
||||||
todos := metadata["todos"].([]any)
|
|
||||||
for _, todo := range todos {
|
|
||||||
t := todo.(map[string]any)
|
|
||||||
content := t["content"].(string)
|
|
||||||
switch t["status"].(string) {
|
|
||||||
case "completed":
|
|
||||||
body += fmt.Sprintf("- [x] %s\n", content)
|
|
||||||
// case "in-progress":
|
|
||||||
// body += fmt.Sprintf("- [ ] _%s_\n", content)
|
|
||||||
default:
|
|
||||||
body += fmt.Sprintf("- [ ] %s\n", content)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
body = toMarkdown(body, width)
|
|
||||||
}
|
}
|
||||||
case "opencode_todowrite":
|
case "opencode_todowrite":
|
||||||
title = fmt.Sprintf("%s", toolName)
|
title = fmt.Sprintf("Planning... %s", elapsed)
|
||||||
if finished && metadata["todos"] != nil {
|
if finished && metadata["todos"] != nil {
|
||||||
body = ""
|
body = ""
|
||||||
todos := metadata["todos"].([]any)
|
todos := metadata["todos"].([]any)
|
||||||
|
@ -299,23 +340,35 @@ func renderToolInvocation(toolCall client.MessageToolInvocationToolCall, result
|
||||||
body += fmt.Sprintf("- [ ] %s\n", content)
|
body += fmt.Sprintf("- [ ] %s\n", content)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
body = toMarkdown(body, width)
|
body = toMarkdown(body, innerWidth)
|
||||||
|
body = renderMarkdown(body, WithFullWidth(), WithPaddingTop(1), WithPaddingBottom(1))
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
body = fmt.Sprintf("```txt\n%s\n```", truncateHeight(body, 10))
|
toolName := renderToolName(toolCall.ToolName)
|
||||||
body = toMarkdown(body, width)
|
title = style.Render(fmt.Sprintf("%s: %s %s", toolName, toolArgs, elapsed))
|
||||||
|
// return title
|
||||||
|
|
||||||
|
// toolName := renderToolName(toolCall.ToolName)
|
||||||
|
// title = fmt.Sprintf("%s: %s", toolName, toolArgs)
|
||||||
|
// body = fmt.Sprintf("```txt\n%s\n```", truncateHeight(body, 10))
|
||||||
|
// body = toMarkdown(body, contentWidth)
|
||||||
}
|
}
|
||||||
|
|
||||||
if metadata["error"] != nil && metadata["message"] != nil {
|
if metadata["error"] != nil && metadata["message"] != nil {
|
||||||
body = styles.BaseStyle().Foreground(t.Error()).Render(metadata["message"].(string))
|
body = styles.BaseStyle().
|
||||||
|
Width(outerWidth).
|
||||||
|
Foreground(t.Error()).
|
||||||
|
Render(metadata["message"].(string))
|
||||||
}
|
}
|
||||||
|
|
||||||
content := style.Render(lipgloss.JoinVertical(lipgloss.Left,
|
content := style.Render(title)
|
||||||
title,
|
content = lipgloss.PlaceHorizontal(layout.Current.Viewport.Width, lipgloss.Center, content)
|
||||||
body,
|
content = styles.ForceReplaceBackgroundWithLipgloss(content, t.Background())
|
||||||
footer,
|
if showResult && body != "" {
|
||||||
))
|
content += "\n" + body
|
||||||
return styles.ForceReplaceBackgroundWithLipgloss(content, t.Background())
|
}
|
||||||
|
return content
|
||||||
|
// return styles.ForceReplaceBackgroundWithLipgloss(content, t.Background())
|
||||||
}
|
}
|
||||||
|
|
||||||
func renderToolName(name string) string {
|
func renderToolName(name string) string {
|
||||||
|
@ -327,9 +380,9 @@ func renderToolName(name string) string {
|
||||||
case "opencode_webfetch":
|
case "opencode_webfetch":
|
||||||
return "Fetch"
|
return "Fetch"
|
||||||
case "opencode_todoread":
|
case "opencode_todoread":
|
||||||
return "Read TODOs"
|
return "Planning"
|
||||||
case "opencode_todowrite":
|
case "opencode_todowrite":
|
||||||
return "Update TODOs"
|
return "Planning"
|
||||||
default:
|
default:
|
||||||
normalizedName := name
|
normalizedName := name
|
||||||
if strings.HasPrefix(name, "opencode_") {
|
if strings.HasPrefix(name, "opencode_") {
|
||||||
|
@ -339,6 +392,59 @@ func renderToolName(name string) string {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type fileRenderer struct {
|
||||||
|
filename string
|
||||||
|
content string
|
||||||
|
height int
|
||||||
|
}
|
||||||
|
|
||||||
|
type fileRenderingOption func(*fileRenderer)
|
||||||
|
|
||||||
|
func WithTruncate(height int) fileRenderingOption {
|
||||||
|
return func(c *fileRenderer) {
|
||||||
|
c.height = height
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func renderFile(filename string, content string, options ...fileRenderingOption) string {
|
||||||
|
renderer := &fileRenderer{
|
||||||
|
filename: filename,
|
||||||
|
content: content,
|
||||||
|
}
|
||||||
|
for _, option := range options {
|
||||||
|
option(renderer)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: is this even needed?
|
||||||
|
lines := []string{}
|
||||||
|
for line := range strings.SplitSeq(content, "\n") {
|
||||||
|
line = strings.TrimRightFunc(line, unicode.IsSpace)
|
||||||
|
line = strings.ReplaceAll(line, "\t", " ")
|
||||||
|
lines = append(lines, line)
|
||||||
|
}
|
||||||
|
content = strings.Join(lines, "\n")
|
||||||
|
|
||||||
|
width := layout.Current.Container.Width - 6
|
||||||
|
if renderer.height > 0 {
|
||||||
|
content = truncateHeight(content, renderer.height)
|
||||||
|
}
|
||||||
|
content = fmt.Sprintf("```%s\n%s\n```", extension(renderer.filename), content)
|
||||||
|
content = toMarkdown(content, width)
|
||||||
|
|
||||||
|
// ensure no line is wider than the width
|
||||||
|
// truncated := []string{}
|
||||||
|
// for line := range strings.SplitSeq(content, "\n") {
|
||||||
|
// line = strings.TrimRightFunc(line, unicode.IsSpace)
|
||||||
|
// // if lipgloss.Width(line) > width-3 {
|
||||||
|
// line = ansi.Truncate(line, width-3, "")
|
||||||
|
// // }
|
||||||
|
// truncated = append(truncated, line)
|
||||||
|
// }
|
||||||
|
// content = strings.Join(truncated, "\n")
|
||||||
|
|
||||||
|
return renderMarkdown(content, WithFullWidth(), WithPaddingTop(1), WithPaddingBottom(1))
|
||||||
|
}
|
||||||
|
|
||||||
func renderToolAction(name string) string {
|
func renderToolAction(name string) string {
|
||||||
switch name {
|
switch name {
|
||||||
// case agent.AgentToolName:
|
// case agent.AgentToolName:
|
||||||
|
@ -367,7 +473,7 @@ func renderToolAction(name string) string {
|
||||||
return "Working..."
|
return "Working..."
|
||||||
}
|
}
|
||||||
|
|
||||||
func renderArgs(args *map[string]any, appInfo client.AppInfo, titleKey string) string {
|
func renderArgs(args *map[string]any, titleKey string) string {
|
||||||
if args == nil || len(*args) == 0 {
|
if args == nil || len(*args) == 0 {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
@ -375,7 +481,7 @@ func renderArgs(args *map[string]any, appInfo client.AppInfo, titleKey string) s
|
||||||
parts := []string{}
|
parts := []string{}
|
||||||
for key, value := range *args {
|
for key, value := range *args {
|
||||||
if key == "filePath" || key == "path" {
|
if key == "filePath" || key == "path" {
|
||||||
value = strings.TrimPrefix(value.(string), appInfo.Path.Root+"/")
|
value = relative(value.(string))
|
||||||
}
|
}
|
||||||
if key == titleKey {
|
if key == titleKey {
|
||||||
title = fmt.Sprintf("%s", value)
|
title = fmt.Sprintf("%s", value)
|
||||||
|
@ -396,3 +502,17 @@ func truncateHeight(content string, height int) string {
|
||||||
}
|
}
|
||||||
return content
|
return content
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func relative(path string) string {
|
||||||
|
return strings.TrimPrefix(path, app.Info.Path.Root+"/")
|
||||||
|
}
|
||||||
|
|
||||||
|
func extension(path string) string {
|
||||||
|
ext := filepath.Ext(path)
|
||||||
|
if ext == "" {
|
||||||
|
ext = ""
|
||||||
|
} else {
|
||||||
|
ext = strings.ToLower(ext[1:])
|
||||||
|
}
|
||||||
|
return ext
|
||||||
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
package chat
|
package chat
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/charmbracelet/bubbles/key"
|
"github.com/charmbracelet/bubbles/key"
|
||||||
|
@ -11,21 +11,23 @@ import (
|
||||||
"github.com/charmbracelet/lipgloss"
|
"github.com/charmbracelet/lipgloss"
|
||||||
"github.com/sst/opencode/internal/app"
|
"github.com/sst/opencode/internal/app"
|
||||||
"github.com/sst/opencode/internal/components/dialog"
|
"github.com/sst/opencode/internal/components/dialog"
|
||||||
|
"github.com/sst/opencode/internal/layout"
|
||||||
"github.com/sst/opencode/internal/state"
|
"github.com/sst/opencode/internal/state"
|
||||||
"github.com/sst/opencode/internal/styles"
|
"github.com/sst/opencode/internal/styles"
|
||||||
"github.com/sst/opencode/internal/theme"
|
"github.com/sst/opencode/internal/theme"
|
||||||
"github.com/sst/opencode/pkg/client"
|
"github.com/sst/opencode/pkg/client"
|
||||||
)
|
)
|
||||||
|
|
||||||
type messagesCmp struct {
|
type messagesComponent struct {
|
||||||
app *app.App
|
app *app.App
|
||||||
width, height int
|
width, height int
|
||||||
viewport viewport.Model
|
viewport viewport.Model
|
||||||
spinner spinner.Model
|
spinner spinner.Model
|
||||||
rendering bool
|
rendering bool
|
||||||
attachments viewport.Model
|
attachments viewport.Model
|
||||||
showToolMessages bool
|
showToolResults bool
|
||||||
cache *MessageCache
|
cache *MessageCache
|
||||||
|
tail bool
|
||||||
}
|
}
|
||||||
type renderFinishedMsg struct{}
|
type renderFinishedMsg struct{}
|
||||||
type ToggleToolMessagesMsg struct{}
|
type ToggleToolMessagesMsg struct{}
|
||||||
|
@ -56,44 +58,54 @@ var messageKeys = MessageKeys{
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *messagesCmp) Init() tea.Cmd {
|
func (m *messagesComponent) Init() tea.Cmd {
|
||||||
return tea.Batch(m.viewport.Init(), m.spinner.Tick)
|
return tea.Batch(m.viewport.Init(), m.spinner.Tick)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *messagesCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
func (m *messagesComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
var cmds []tea.Cmd
|
var cmds []tea.Cmd
|
||||||
switch msg := msg.(type) {
|
switch msg := msg.(type) {
|
||||||
|
case SendMsg:
|
||||||
|
m.viewport.GotoBottom()
|
||||||
|
m.tail = true
|
||||||
|
return m, nil
|
||||||
case dialog.ThemeChangedMsg:
|
case dialog.ThemeChangedMsg:
|
||||||
m.cache.Clear()
|
m.cache.Clear()
|
||||||
m.renderView()
|
m.renderView()
|
||||||
return m, nil
|
return m, nil
|
||||||
case ToggleToolMessagesMsg:
|
case ToggleToolMessagesMsg:
|
||||||
m.showToolMessages = !m.showToolMessages
|
m.showToolResults = !m.showToolResults
|
||||||
m.renderView()
|
m.renderView()
|
||||||
return m, nil
|
return m, nil
|
||||||
case state.SessionSelectedMsg:
|
case state.SessionSelectedMsg:
|
||||||
// Clear cache when switching sessions
|
|
||||||
m.cache.Clear()
|
m.cache.Clear()
|
||||||
cmd := m.Reload()
|
cmd := m.Reload()
|
||||||
|
m.viewport.GotoBottom()
|
||||||
return m, cmd
|
return m, cmd
|
||||||
case state.SessionClearedMsg:
|
case state.SessionClearedMsg:
|
||||||
// Clear cache when session is cleared
|
|
||||||
m.cache.Clear()
|
m.cache.Clear()
|
||||||
cmd := m.Reload()
|
cmd := m.Reload()
|
||||||
return m, cmd
|
return m, cmd
|
||||||
case tea.KeyMsg:
|
case tea.KeyMsg:
|
||||||
if key.Matches(msg, messageKeys.PageUp) || key.Matches(msg, messageKeys.PageDown) ||
|
if key.Matches(msg, messageKeys.PageUp) ||
|
||||||
key.Matches(msg, messageKeys.HalfPageUp) || key.Matches(msg, messageKeys.HalfPageDown) {
|
key.Matches(msg, messageKeys.PageDown) ||
|
||||||
|
key.Matches(msg, messageKeys.HalfPageUp) ||
|
||||||
|
key.Matches(msg, messageKeys.HalfPageDown) {
|
||||||
u, cmd := m.viewport.Update(msg)
|
u, cmd := m.viewport.Update(msg)
|
||||||
m.viewport = u
|
m.viewport = u
|
||||||
|
m.tail = m.viewport.AtBottom()
|
||||||
cmds = append(cmds, cmd)
|
cmds = append(cmds, cmd)
|
||||||
}
|
}
|
||||||
case renderFinishedMsg:
|
case renderFinishedMsg:
|
||||||
m.rendering = false
|
m.rendering = false
|
||||||
m.viewport.GotoBottom()
|
if m.tail {
|
||||||
|
m.viewport.GotoBottom()
|
||||||
|
}
|
||||||
case state.StateUpdatedMsg:
|
case state.StateUpdatedMsg:
|
||||||
m.renderView()
|
m.renderView()
|
||||||
m.viewport.GotoBottom()
|
if m.tail {
|
||||||
|
m.viewport.GotoBottom()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
spinner, cmd := m.spinner.Update(msg)
|
spinner, cmd := m.spinner.Update(msg)
|
||||||
|
@ -102,91 +114,159 @@ func (m *messagesCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
return m, tea.Batch(cmds...)
|
return m, tea.Batch(cmds...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *messagesCmp) renderView() {
|
type blockType int
|
||||||
|
|
||||||
|
const (
|
||||||
|
none blockType = iota
|
||||||
|
systemTextBlock
|
||||||
|
userTextBlock
|
||||||
|
assistantTextBlock
|
||||||
|
toolInvocationBlock
|
||||||
|
)
|
||||||
|
|
||||||
|
func (m *messagesComponent) renderView() {
|
||||||
if m.width == 0 {
|
if m.width == 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
messages := make([]string, 0)
|
blocks := make([]string, 0)
|
||||||
for _, msg := range m.app.Messages {
|
previousBlockType := none
|
||||||
|
for _, message := range m.app.Messages {
|
||||||
|
if message.Role == client.System {
|
||||||
|
continue // ignoring system messages for now
|
||||||
|
}
|
||||||
|
|
||||||
var content string
|
var content string
|
||||||
var cached bool
|
var cached bool
|
||||||
|
|
||||||
switch msg.Role {
|
author := ""
|
||||||
|
switch message.Role {
|
||||||
case client.User:
|
case client.User:
|
||||||
content, cached = m.cache.Get(msg, m.width, m.showToolMessages, *m.app.Info)
|
author = app.Info.User
|
||||||
if !cached {
|
|
||||||
content = renderUserMessage(m.app.Info.User, msg, m.width)
|
|
||||||
m.cache.Set(msg, m.width, m.showToolMessages, *m.app.Info, content)
|
|
||||||
}
|
|
||||||
messages = append(messages, content+"\n")
|
|
||||||
case client.Assistant:
|
case client.Assistant:
|
||||||
content, cached = m.cache.Get(msg, m.width, m.showToolMessages, *m.app.Info)
|
author = message.Metadata.Assistant.ModelID
|
||||||
if !cached {
|
}
|
||||||
content = renderAssistantMessage(msg, m.width, m.showToolMessages, *m.app.Info)
|
|
||||||
m.cache.Set(msg, m.width, m.showToolMessages, *m.app.Info, content)
|
for _, p := range message.Parts {
|
||||||
|
part, err := p.ValueByDiscriminator()
|
||||||
|
if err != nil {
|
||||||
|
continue //TODO: handle error?
|
||||||
|
}
|
||||||
|
|
||||||
|
switch part.(type) {
|
||||||
|
// case client.MessagePartStepStart:
|
||||||
|
// messages = append(messages, "")
|
||||||
|
case client.MessagePartText:
|
||||||
|
text := part.(client.MessagePartText)
|
||||||
|
key := m.cache.GenerateKey(message.Id, text.Text, layout.Current.Viewport.Width)
|
||||||
|
content, cached = m.cache.Get(key)
|
||||||
|
if !cached {
|
||||||
|
content = renderText(message, text.Text, author)
|
||||||
|
m.cache.Set(key, content)
|
||||||
|
}
|
||||||
|
if previousBlockType != none {
|
||||||
|
blocks = append(blocks, "")
|
||||||
|
}
|
||||||
|
blocks = append(blocks, content)
|
||||||
|
if message.Role == client.User {
|
||||||
|
previousBlockType = userTextBlock
|
||||||
|
} else if message.Role == client.Assistant {
|
||||||
|
previousBlockType = assistantTextBlock
|
||||||
|
} else if message.Role == client.System {
|
||||||
|
previousBlockType = systemTextBlock
|
||||||
|
}
|
||||||
|
case client.MessagePartToolInvocation:
|
||||||
|
toolInvocationPart := part.(client.MessagePartToolInvocation)
|
||||||
|
toolCall, _ := toolInvocationPart.ToolInvocation.AsMessageToolInvocationToolCall()
|
||||||
|
metadata := map[string]any{}
|
||||||
|
if _, ok := message.Metadata.Tool[toolCall.ToolCallId]; ok {
|
||||||
|
metadata = message.Metadata.Tool[toolCall.ToolCallId].(map[string]any)
|
||||||
|
}
|
||||||
|
var result *string
|
||||||
|
resultPart, resultError := toolInvocationPart.ToolInvocation.AsMessageToolInvocationToolResult()
|
||||||
|
if resultError == nil {
|
||||||
|
result = &resultPart.Result
|
||||||
|
}
|
||||||
|
|
||||||
|
if toolCall.State == "result" {
|
||||||
|
key := m.cache.GenerateKey(message.Id,
|
||||||
|
toolCall.ToolCallId,
|
||||||
|
m.showToolResults,
|
||||||
|
layout.Current.Viewport.Width,
|
||||||
|
)
|
||||||
|
content, cached = m.cache.Get(key)
|
||||||
|
if !cached {
|
||||||
|
content = renderToolInvocation(toolCall, result, metadata, m.showToolResults)
|
||||||
|
m.cache.Set(key, content)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// if the tool call isn't finished, never cache
|
||||||
|
content = renderToolInvocation(toolCall, result, metadata, m.showToolResults)
|
||||||
|
}
|
||||||
|
|
||||||
|
if previousBlockType != toolInvocationBlock {
|
||||||
|
blocks = append(blocks, "")
|
||||||
|
}
|
||||||
|
blocks = append(blocks, content)
|
||||||
|
previousBlockType = toolInvocationBlock
|
||||||
}
|
}
|
||||||
messages = append(messages, content+"\n")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
m.viewport.SetContent(
|
t := theme.CurrentTheme()
|
||||||
styles.BaseStyle().
|
centered := []string{}
|
||||||
Render(
|
for _, block := range blocks {
|
||||||
lipgloss.JoinVertical(
|
centered = append(centered, lipgloss.PlaceHorizontal(
|
||||||
lipgloss.Top,
|
m.width,
|
||||||
messages...,
|
lipgloss.Center,
|
||||||
),
|
block,
|
||||||
),
|
lipgloss.WithWhitespaceBackground(t.Background()),
|
||||||
)
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
m.viewport.Height = m.height - lipgloss.Height(m.header())
|
||||||
|
m.viewport.SetContent(strings.Join(centered, "\n"))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *messagesCmp) View() string {
|
func (m *messagesComponent) header() string {
|
||||||
baseStyle := styles.BaseStyle()
|
if m.app.Session.Id == "" {
|
||||||
|
return ""
|
||||||
if m.rendering {
|
|
||||||
return baseStyle.
|
|
||||||
Width(m.width).
|
|
||||||
Render(
|
|
||||||
lipgloss.JoinVertical(
|
|
||||||
lipgloss.Top,
|
|
||||||
"Loading...",
|
|
||||||
m.working(),
|
|
||||||
m.help(),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(m.app.Messages) == 0 {
|
t := theme.CurrentTheme()
|
||||||
content := baseStyle.
|
width := layout.Current.Container.Width
|
||||||
Width(m.width).
|
base := styles.BaseStyle().Render
|
||||||
Height(m.height - 1).
|
muted := styles.Muted().Render
|
||||||
Render(
|
headerLines := []string{}
|
||||||
m.initialScreen(),
|
headerLines = append(headerLines, toMarkdown("# "+m.app.Session.Title, width))
|
||||||
)
|
if m.app.Session.Share != nil && m.app.Session.Share.Url != "" {
|
||||||
|
headerLines = append(headerLines, muted(m.app.Session.Share.Url))
|
||||||
return baseStyle.
|
} else {
|
||||||
Width(m.width).
|
headerLines = append(headerLines, base("/share")+muted(" to create a shareable link"))
|
||||||
Render(
|
|
||||||
lipgloss.JoinVertical(
|
|
||||||
lipgloss.Top,
|
|
||||||
content,
|
|
||||||
"",
|
|
||||||
m.help(),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
header := strings.Join(headerLines, "\n")
|
||||||
|
|
||||||
return baseStyle.
|
header = styles.BaseStyle().
|
||||||
Width(m.width).
|
Width(width).
|
||||||
Render(
|
PaddingTop(1).
|
||||||
lipgloss.JoinVertical(
|
BorderBottom(true).
|
||||||
lipgloss.Top,
|
BorderForeground(t.BorderSubtle()).
|
||||||
m.viewport.View(),
|
BorderStyle(lipgloss.NormalBorder()).
|
||||||
m.working(),
|
Background(t.Background()).
|
||||||
m.help(),
|
Render(header)
|
||||||
),
|
|
||||||
)
|
return styles.ForceReplaceBackgroundWithLipgloss(header, t.Background())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *messagesComponent) View() string {
|
||||||
|
if len(m.app.Messages) == 0 || m.rendering {
|
||||||
|
return m.home()
|
||||||
|
}
|
||||||
|
return lipgloss.JoinVertical(
|
||||||
|
lipgloss.Left,
|
||||||
|
lipgloss.PlaceHorizontal(m.width, lipgloss.Center, m.header()),
|
||||||
|
m.viewport.View(),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// func hasToolsWithoutResponse(messages []message.Message) bool {
|
// func hasToolsWithoutResponse(messages []message.Message) bool {
|
||||||
|
@ -225,36 +305,7 @@ func (m *messagesCmp) View() string {
|
||||||
// return false
|
// return false
|
||||||
// }
|
// }
|
||||||
|
|
||||||
func (m *messagesCmp) working() string {
|
func (m *messagesComponent) help() string {
|
||||||
text := ""
|
|
||||||
if len(m.app.Messages) > 0 {
|
|
||||||
t := theme.CurrentTheme()
|
|
||||||
baseStyle := styles.BaseStyle()
|
|
||||||
|
|
||||||
task := ""
|
|
||||||
if m.app.IsBusy() {
|
|
||||||
task = "Working..."
|
|
||||||
}
|
|
||||||
// lastMessage := m.app.Messages[len(m.app.Messages)-1]
|
|
||||||
// if hasToolsWithoutResponse(m.app.Messages) {
|
|
||||||
// task = "Waiting for tool response..."
|
|
||||||
// } else if hasUnfinishedToolCalls(m.app.Messages) {
|
|
||||||
// task = "Building tool call..."
|
|
||||||
// } else if !lastMessage.IsFinished() {
|
|
||||||
// task = "Generating..."
|
|
||||||
// }
|
|
||||||
if task != "" {
|
|
||||||
text += baseStyle.
|
|
||||||
Width(m.width).
|
|
||||||
Foreground(t.Primary()).
|
|
||||||
Bold(true).
|
|
||||||
Render(fmt.Sprintf("%s %s ", m.spinner.View(), task))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return text
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *messagesCmp) help() string {
|
|
||||||
t := theme.CurrentTheme()
|
t := theme.CurrentTheme()
|
||||||
baseStyle := styles.BaseStyle()
|
baseStyle := styles.BaseStyle()
|
||||||
|
|
||||||
|
@ -275,11 +326,7 @@ func (m *messagesCmp) help() string {
|
||||||
baseStyle.Foreground(t.Text()).Bold(true).Render(" \\"),
|
baseStyle.Foreground(t.Text()).Bold(true).Render(" \\"),
|
||||||
baseStyle.Foreground(t.TextMuted()).Bold(true).Render("+"),
|
baseStyle.Foreground(t.TextMuted()).Bold(true).Render("+"),
|
||||||
baseStyle.Foreground(t.Text()).Bold(true).Render("enter"),
|
baseStyle.Foreground(t.Text()).Bold(true).Render("enter"),
|
||||||
baseStyle.Foreground(t.TextMuted()).Bold(true).Render(" for newline,"),
|
baseStyle.Foreground(t.TextMuted()).Bold(true).Render(" for newline"),
|
||||||
baseStyle.Foreground(t.Text()).Bold(true).Render(" ↑↓"),
|
|
||||||
baseStyle.Foreground(t.TextMuted()).Bold(true).Render(" for history,"),
|
|
||||||
baseStyle.Foreground(t.Text()).Bold(true).Render(" ctrl+h"),
|
|
||||||
baseStyle.Foreground(t.TextMuted()).Bold(true).Render(" to toggle tool messages"),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return baseStyle.
|
return baseStyle.
|
||||||
|
@ -287,20 +334,83 @@ func (m *messagesCmp) help() string {
|
||||||
Render(text)
|
Render(text)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *messagesCmp) initialScreen() string {
|
func (m *messagesComponent) home() string {
|
||||||
|
t := theme.CurrentTheme()
|
||||||
baseStyle := styles.BaseStyle()
|
baseStyle := styles.BaseStyle()
|
||||||
|
base := baseStyle.Render
|
||||||
|
muted := styles.Muted().Render
|
||||||
|
|
||||||
return baseStyle.Width(m.width).Render(
|
// mark := `
|
||||||
lipgloss.JoinVertical(
|
// ███▀▀█
|
||||||
lipgloss.Top,
|
// ███ █
|
||||||
header(m.app, m.width),
|
// ▀▀▀▀▀▀ `
|
||||||
"",
|
open := `
|
||||||
lspsConfigured(m.width),
|
█▀▀█ █▀▀█ █▀▀ █▀▀▄
|
||||||
),
|
█░░█ █░░█ █▀▀ █░░█
|
||||||
|
▀▀▀▀ █▀▀▀ ▀▀▀ ▀ ▀ `
|
||||||
|
code := `
|
||||||
|
█▀▀ █▀▀█ █▀▀▄ █▀▀
|
||||||
|
█░░ █░░█ █░░█ █▀▀
|
||||||
|
▀▀▀ ▀▀▀▀ ▀▀▀ ▀▀▀`
|
||||||
|
|
||||||
|
logo := lipgloss.JoinHorizontal(
|
||||||
|
lipgloss.Top,
|
||||||
|
// styles.BaseStyle().Foreground(t.Primary()).Render(mark),
|
||||||
|
styles.Muted().Render(open),
|
||||||
|
styles.BaseStyle().Render(code),
|
||||||
|
)
|
||||||
|
cwd := app.Info.Path.Cwd
|
||||||
|
config := app.Info.Path.Config
|
||||||
|
|
||||||
|
commands := [][]string{
|
||||||
|
{"/help", "show help"},
|
||||||
|
{"/sessions", "list sessions"},
|
||||||
|
{"/new", "start a new session"},
|
||||||
|
{"/model", "switch model"},
|
||||||
|
{"/share", "share the current session"},
|
||||||
|
{"/exit", "exit the app"},
|
||||||
|
}
|
||||||
|
|
||||||
|
commandLines := []string{}
|
||||||
|
for _, command := range commands {
|
||||||
|
commandLines = append(commandLines, (base(command[0]) + " " + muted(command[1])))
|
||||||
|
}
|
||||||
|
|
||||||
|
logoAndVersion := lipgloss.JoinVertical(
|
||||||
|
lipgloss.Right,
|
||||||
|
logo,
|
||||||
|
muted(app.Info.Version),
|
||||||
|
)
|
||||||
|
|
||||||
|
lines := []string{}
|
||||||
|
lines = append(lines, "")
|
||||||
|
lines = append(lines, "")
|
||||||
|
lines = append(lines, logoAndVersion)
|
||||||
|
lines = append(lines, "")
|
||||||
|
lines = append(lines, base("cwd ")+muted(cwd))
|
||||||
|
lines = append(lines, base("config ")+muted(config))
|
||||||
|
lines = append(lines, "")
|
||||||
|
lines = append(lines, commandLines...)
|
||||||
|
lines = append(lines, "")
|
||||||
|
if m.rendering {
|
||||||
|
lines = append(lines, styles.Muted().Render("Loading session..."))
|
||||||
|
} else {
|
||||||
|
lines = append(lines, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
return styles.ForceReplaceBackgroundWithLipgloss(
|
||||||
|
lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center,
|
||||||
|
baseStyle.Width(lipgloss.Width(logoAndVersion)).Render(
|
||||||
|
lipgloss.JoinVertical(
|
||||||
|
lipgloss.Top,
|
||||||
|
lines...,
|
||||||
|
),
|
||||||
|
)),
|
||||||
|
t.Background(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *messagesCmp) SetSize(width, height int) tea.Cmd {
|
func (m *messagesComponent) SetSize(width, height int) tea.Cmd {
|
||||||
if m.width == width && m.height == height {
|
if m.width == width && m.height == height {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -311,18 +421,18 @@ func (m *messagesCmp) SetSize(width, height int) tea.Cmd {
|
||||||
m.width = width
|
m.width = width
|
||||||
m.height = height
|
m.height = height
|
||||||
m.viewport.Width = width
|
m.viewport.Width = width
|
||||||
m.viewport.Height = height - 2
|
m.viewport.Height = height - lipgloss.Height(m.header())
|
||||||
m.attachments.Width = width + 40
|
m.attachments.Width = width + 40
|
||||||
m.attachments.Height = 3
|
m.attachments.Height = 3
|
||||||
m.renderView()
|
m.renderView()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *messagesCmp) GetSize() (int, int) {
|
func (m *messagesComponent) GetSize() (int, int) {
|
||||||
return m.width, m.height
|
return m.width, m.height
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *messagesCmp) Reload() tea.Cmd {
|
func (m *messagesComponent) Reload() tea.Cmd {
|
||||||
m.rendering = true
|
m.rendering = true
|
||||||
return func() tea.Msg {
|
return func() tea.Msg {
|
||||||
m.renderView()
|
m.renderView()
|
||||||
|
@ -330,7 +440,7 @@ func (m *messagesCmp) Reload() tea.Cmd {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *messagesCmp) BindingKeys() []key.Binding {
|
func (m *messagesComponent) BindingKeys() []key.Binding {
|
||||||
return []key.Binding{
|
return []key.Binding{
|
||||||
m.viewport.KeyMap.PageDown,
|
m.viewport.KeyMap.PageDown,
|
||||||
m.viewport.KeyMap.PageUp,
|
m.viewport.KeyMap.PageUp,
|
||||||
|
@ -339,7 +449,7 @@ func (m *messagesCmp) BindingKeys() []key.Binding {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewMessagesCmp(app *app.App) tea.Model {
|
func NewMessagesComponent(app *app.App) tea.Model {
|
||||||
customSpinner := spinner.Spinner{
|
customSpinner := spinner.Spinner{
|
||||||
Frames: []string{" ", "┃", "┃"},
|
Frames: []string{" ", "┃", "┃"},
|
||||||
FPS: time.Second / 3,
|
FPS: time.Second / 3,
|
||||||
|
@ -353,12 +463,13 @@ func NewMessagesCmp(app *app.App) tea.Model {
|
||||||
vp.KeyMap.HalfPageUp = messageKeys.HalfPageUp
|
vp.KeyMap.HalfPageUp = messageKeys.HalfPageUp
|
||||||
vp.KeyMap.HalfPageDown = messageKeys.HalfPageDown
|
vp.KeyMap.HalfPageDown = messageKeys.HalfPageDown
|
||||||
|
|
||||||
return &messagesCmp{
|
return &messagesComponent{
|
||||||
app: app,
|
app: app,
|
||||||
viewport: vp,
|
viewport: vp,
|
||||||
spinner: s,
|
spinner: s,
|
||||||
attachments: attachments,
|
attachments: attachments,
|
||||||
showToolMessages: true,
|
showToolResults: true,
|
||||||
cache: NewMessageCache(),
|
cache: NewMessageCache(),
|
||||||
|
tail: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,212 +0,0 @@
|
||||||
package chat
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"sort"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
|
||||||
"github.com/charmbracelet/lipgloss"
|
|
||||||
"github.com/sst/opencode/internal/app"
|
|
||||||
"github.com/sst/opencode/internal/state"
|
|
||||||
"github.com/sst/opencode/internal/styles"
|
|
||||||
"github.com/sst/opencode/internal/theme"
|
|
||||||
)
|
|
||||||
|
|
||||||
type sidebarCmp struct {
|
|
||||||
app *app.App
|
|
||||||
width, height int
|
|
||||||
modFiles map[string]struct {
|
|
||||||
additions int
|
|
||||||
removals int
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *sidebarCmp) Init() tea.Cmd {
|
|
||||||
// TODO: History service not implemented in API yet
|
|
||||||
// Initialize the modified files map
|
|
||||||
m.modFiles = make(map[string]struct {
|
|
||||||
additions int
|
|
||||||
removals int
|
|
||||||
})
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *sidebarCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|
||||||
switch msg.(type) {
|
|
||||||
case state.SessionSelectedMsg:
|
|
||||||
// TODO: History service not implemented in API yet
|
|
||||||
// ctx := context.Background()
|
|
||||||
// m.loadModifiedFiles(ctx)
|
|
||||||
// case pubsub.Event[history.File]:
|
|
||||||
// TODO: History service not implemented in API yet
|
|
||||||
// if msg.Payload.SessionID == m.app.CurrentSession.ID {
|
|
||||||
// // Process the individual file change instead of reloading all files
|
|
||||||
// ctx := context.Background()
|
|
||||||
// m.processFileChanges(ctx, msg.Payload)
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
return m, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *sidebarCmp) View() string {
|
|
||||||
t := theme.CurrentTheme()
|
|
||||||
baseStyle := styles.BaseStyle()
|
|
||||||
shareUrl := ""
|
|
||||||
if m.app.Session.Share != nil {
|
|
||||||
shareUrl = baseStyle.Foreground(t.TextMuted()).Render(m.app.Session.Share.Url)
|
|
||||||
}
|
|
||||||
|
|
||||||
// qrcode := ""
|
|
||||||
// if m.app.Session.ShareID != nil {
|
|
||||||
// url := "https://dev.opencode.ai/share?id="
|
|
||||||
// qrcode, _, _ = qr.Generate(url + m.app.Session.Id)
|
|
||||||
// }
|
|
||||||
|
|
||||||
return baseStyle.
|
|
||||||
Width(m.width).
|
|
||||||
PaddingLeft(4).
|
|
||||||
PaddingRight(1).
|
|
||||||
Render(
|
|
||||||
lipgloss.JoinVertical(
|
|
||||||
lipgloss.Top,
|
|
||||||
header(m.app, m.width),
|
|
||||||
" ",
|
|
||||||
m.sessionSection(),
|
|
||||||
shareUrl,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *sidebarCmp) sessionSection() string {
|
|
||||||
t := theme.CurrentTheme()
|
|
||||||
baseStyle := styles.BaseStyle()
|
|
||||||
|
|
||||||
sessionKey := baseStyle.
|
|
||||||
Foreground(t.Primary()).
|
|
||||||
Bold(true).
|
|
||||||
Render("Session")
|
|
||||||
|
|
||||||
sessionValue := baseStyle.
|
|
||||||
Foreground(t.Text()).
|
|
||||||
Render(fmt.Sprintf(": %s", m.app.Session.Title))
|
|
||||||
|
|
||||||
return sessionKey + sessionValue
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *sidebarCmp) modifiedFile(filePath string, additions, removals int) string {
|
|
||||||
t := theme.CurrentTheme()
|
|
||||||
baseStyle := styles.BaseStyle()
|
|
||||||
|
|
||||||
stats := ""
|
|
||||||
if additions > 0 && removals > 0 {
|
|
||||||
additionsStr := baseStyle.
|
|
||||||
Foreground(t.Success()).
|
|
||||||
PaddingLeft(1).
|
|
||||||
Render(fmt.Sprintf("+%d", additions))
|
|
||||||
|
|
||||||
removalsStr := baseStyle.
|
|
||||||
Foreground(t.Error()).
|
|
||||||
PaddingLeft(1).
|
|
||||||
Render(fmt.Sprintf("-%d", removals))
|
|
||||||
|
|
||||||
content := lipgloss.JoinHorizontal(lipgloss.Left, additionsStr, removalsStr)
|
|
||||||
stats = baseStyle.Width(lipgloss.Width(content)).Render(content)
|
|
||||||
} else if additions > 0 {
|
|
||||||
additionsStr := fmt.Sprintf(" %s", baseStyle.
|
|
||||||
PaddingLeft(1).
|
|
||||||
Foreground(t.Success()).
|
|
||||||
Render(fmt.Sprintf("+%d", additions)))
|
|
||||||
stats = baseStyle.Width(lipgloss.Width(additionsStr)).Render(additionsStr)
|
|
||||||
} else if removals > 0 {
|
|
||||||
removalsStr := fmt.Sprintf(" %s", baseStyle.
|
|
||||||
PaddingLeft(1).
|
|
||||||
Foreground(t.Error()).
|
|
||||||
Render(fmt.Sprintf("-%d", removals)))
|
|
||||||
stats = baseStyle.Width(lipgloss.Width(removalsStr)).Render(removalsStr)
|
|
||||||
}
|
|
||||||
|
|
||||||
filePathStr := baseStyle.Render(filePath)
|
|
||||||
|
|
||||||
return baseStyle.
|
|
||||||
Width(m.width).
|
|
||||||
Render(
|
|
||||||
lipgloss.JoinHorizontal(
|
|
||||||
lipgloss.Left,
|
|
||||||
filePathStr,
|
|
||||||
stats,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *sidebarCmp) modifiedFiles() string {
|
|
||||||
t := theme.CurrentTheme()
|
|
||||||
baseStyle := styles.BaseStyle()
|
|
||||||
|
|
||||||
modifiedFiles := baseStyle.
|
|
||||||
Width(m.width).
|
|
||||||
Foreground(t.Primary()).
|
|
||||||
Bold(true).
|
|
||||||
Render("Modified Files:")
|
|
||||||
|
|
||||||
// If no modified files, show a placeholder message
|
|
||||||
if m.modFiles == nil || len(m.modFiles) == 0 {
|
|
||||||
message := "No modified files"
|
|
||||||
remainingWidth := m.width - lipgloss.Width(message)
|
|
||||||
if remainingWidth > 0 {
|
|
||||||
message += strings.Repeat(" ", remainingWidth)
|
|
||||||
}
|
|
||||||
return baseStyle.
|
|
||||||
Width(m.width).
|
|
||||||
Render(
|
|
||||||
lipgloss.JoinVertical(
|
|
||||||
lipgloss.Top,
|
|
||||||
modifiedFiles,
|
|
||||||
baseStyle.Foreground(t.TextMuted()).Render(message),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sort file paths alphabetically for consistent ordering
|
|
||||||
var paths []string
|
|
||||||
for path := range m.modFiles {
|
|
||||||
paths = append(paths, path)
|
|
||||||
}
|
|
||||||
sort.Strings(paths)
|
|
||||||
|
|
||||||
// Create views for each file in sorted order
|
|
||||||
var fileViews []string
|
|
||||||
for _, path := range paths {
|
|
||||||
stats := m.modFiles[path]
|
|
||||||
fileViews = append(fileViews, m.modifiedFile(path, stats.additions, stats.removals))
|
|
||||||
}
|
|
||||||
|
|
||||||
return baseStyle.
|
|
||||||
Width(m.width).
|
|
||||||
Render(
|
|
||||||
lipgloss.JoinVertical(
|
|
||||||
lipgloss.Top,
|
|
||||||
modifiedFiles,
|
|
||||||
lipgloss.JoinVertical(
|
|
||||||
lipgloss.Left,
|
|
||||||
fileViews...,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *sidebarCmp) SetSize(width, height int) tea.Cmd {
|
|
||||||
m.width = width
|
|
||||||
m.height = height
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *sidebarCmp) GetSize() (int, int) {
|
|
||||||
return m.width, m.height
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewSidebarCmp(app *app.App) tea.Model {
|
|
||||||
return &sidebarCmp{
|
|
||||||
app: app,
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -98,16 +98,16 @@ func (m statusCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// getHelpWidget returns the help widget with current theme colors
|
func logo() string {
|
||||||
func getHelpWidget() string {
|
|
||||||
t := theme.CurrentTheme()
|
t := theme.CurrentTheme()
|
||||||
helpText := "ctrl+? help"
|
mark := styles.Bold().Foreground(t.Primary()).Render("◧ ")
|
||||||
|
open := styles.Muted().Render("open")
|
||||||
return styles.Padded().
|
code := styles.BaseStyle().Bold(true).Render("code")
|
||||||
Background(t.TextMuted()).
|
version := styles.Muted().Render(app.Info.Version)
|
||||||
Foreground(t.BackgroundDarker()).
|
return styles.ForceReplaceBackgroundWithLipgloss(
|
||||||
Bold(true).
|
styles.Padded().Render(mark+open+code+" "+version),
|
||||||
Render(helpText)
|
t.BackgroundElement(),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func formatTokensAndCost(tokens float32, contextWindow float32, cost float32) string {
|
func formatTokensAndCost(tokens float32, contextWindow float32, cost float32) string {
|
||||||
|
@ -132,16 +132,28 @@ func formatTokensAndCost(tokens float32, contextWindow float32, cost float32) st
|
||||||
|
|
||||||
// Format cost with $ symbol and 2 decimal places
|
// Format cost with $ symbol and 2 decimal places
|
||||||
formattedCost := fmt.Sprintf("$%.2f", cost)
|
formattedCost := fmt.Sprintf("$%.2f", cost)
|
||||||
|
|
||||||
percentage := (float64(tokens) / float64(contextWindow)) * 100
|
percentage := (float64(tokens) / float64(contextWindow)) * 100
|
||||||
|
|
||||||
return fmt.Sprintf("Tokens: %s (%d%%), Cost: %s", formattedTokens, int(percentage), formattedCost)
|
return fmt.Sprintf("Tokens: %s (%d%%), Cost: %s", formattedTokens, int(percentage), formattedCost)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m statusCmp) View() string {
|
func (m statusCmp) View() string {
|
||||||
t := theme.CurrentTheme()
|
if m.app.Session.Id == "" {
|
||||||
status := getHelpWidget()
|
return styles.BaseStyle().
|
||||||
|
Width(m.width).
|
||||||
|
Height(2).
|
||||||
|
Render("")
|
||||||
|
}
|
||||||
|
|
||||||
|
t := theme.CurrentTheme()
|
||||||
|
logo := logo()
|
||||||
|
|
||||||
|
cwd := styles.Padded().
|
||||||
|
Foreground(t.TextMuted()).
|
||||||
|
Background(t.BackgroundSubtle()).
|
||||||
|
Render(app.Info.Path.Cwd)
|
||||||
|
|
||||||
|
sessionInfo := ""
|
||||||
if m.app.Session.Id != "" {
|
if m.app.Session.Id != "" {
|
||||||
tokens := float32(0)
|
tokens := float32(0)
|
||||||
cost := float32(0)
|
cost := float32(0)
|
||||||
|
@ -157,87 +169,85 @@ func (m statusCmp) View() string {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
tokensInfo := styles.Padded().
|
sessionInfo = styles.Padded().
|
||||||
Background(t.Text()).
|
Background(t.BackgroundElement()).
|
||||||
Foreground(t.BackgroundSecondary()).
|
Foreground(t.TextMuted()).
|
||||||
Render(formatTokensAndCost(tokens, contextWindow, cost))
|
Render(formatTokensAndCost(tokens, contextWindow, cost))
|
||||||
status += tokensInfo
|
|
||||||
}
|
}
|
||||||
|
|
||||||
diagnostics := styles.Padded().Background(t.BackgroundDarker()).Render(m.projectDiagnostics())
|
// diagnostics := styles.Padded().Background(t.BackgroundElement()).Render(m.projectDiagnostics())
|
||||||
|
|
||||||
modelName := m.model()
|
space := max(
|
||||||
|
|
||||||
statusWidth := max(
|
|
||||||
0,
|
0,
|
||||||
m.width-
|
m.width-lipgloss.Width(logo)-lipgloss.Width(cwd)-lipgloss.Width(sessionInfo),
|
||||||
lipgloss.Width(status)-
|
|
||||||
lipgloss.Width(modelName)-
|
|
||||||
lipgloss.Width(diagnostics),
|
|
||||||
)
|
)
|
||||||
|
spacer := lipgloss.NewStyle().Background(t.BackgroundSubtle()).Width(space).Render("")
|
||||||
|
|
||||||
const minInlineWidth = 30
|
status := logo + cwd + spacer + sessionInfo
|
||||||
|
|
||||||
|
blank := styles.BaseStyle().Background(t.Background()).Width(m.width).Render("")
|
||||||
|
return blank + "\n" + status
|
||||||
|
|
||||||
// Display the first status message if available
|
// Display the first status message if available
|
||||||
var statusMessage string
|
// var statusMessage string
|
||||||
if len(m.queue) > 0 {
|
// if len(m.queue) > 0 {
|
||||||
sm := m.queue[0]
|
// sm := m.queue[0]
|
||||||
infoStyle := styles.Padded().
|
// infoStyle := styles.Padded().
|
||||||
Foreground(t.Background())
|
// Foreground(t.Background())
|
||||||
|
//
|
||||||
|
// switch sm.Level {
|
||||||
|
// case "info":
|
||||||
|
// infoStyle = infoStyle.Background(t.Info())
|
||||||
|
// case "warn":
|
||||||
|
// infoStyle = infoStyle.Background(t.Warning())
|
||||||
|
// case "error":
|
||||||
|
// infoStyle = infoStyle.Background(t.Error())
|
||||||
|
// case "debug":
|
||||||
|
// infoStyle = infoStyle.Background(t.TextMuted())
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // Truncate message if it's longer than available width
|
||||||
|
// msg := sm.Message
|
||||||
|
// availWidth := statusWidth - 10
|
||||||
|
//
|
||||||
|
// // If we have enough space, show inline
|
||||||
|
// if availWidth >= minInlineWidth {
|
||||||
|
// if len(msg) > availWidth && availWidth > 0 {
|
||||||
|
// msg = msg[:availWidth] + "..."
|
||||||
|
// }
|
||||||
|
// status += infoStyle.Width(statusWidth).Render(msg)
|
||||||
|
// } else {
|
||||||
|
// // Otherwise, prepare a full-width message to show above
|
||||||
|
// if len(msg) > m.width-10 && m.width > 10 {
|
||||||
|
// msg = msg[:m.width-10] + "..."
|
||||||
|
// }
|
||||||
|
// statusMessage = infoStyle.Width(m.width).Render(msg)
|
||||||
|
//
|
||||||
|
// // Add empty space in the status bar
|
||||||
|
// status += styles.Padded().
|
||||||
|
// Foreground(t.Text()).
|
||||||
|
// Background(t.BackgroundSubtle()).
|
||||||
|
// Width(statusWidth).
|
||||||
|
// Render("")
|
||||||
|
// }
|
||||||
|
// } else {
|
||||||
|
// status += styles.Padded().
|
||||||
|
// Foreground(t.Text()).
|
||||||
|
// Background(t.BackgroundSubtle()).
|
||||||
|
// Width(statusWidth).
|
||||||
|
// Render("")
|
||||||
|
// }
|
||||||
|
|
||||||
switch sm.Level {
|
// status += diagnostics
|
||||||
case "info":
|
// status += modelName
|
||||||
infoStyle = infoStyle.Background(t.Info())
|
|
||||||
case "warn":
|
|
||||||
infoStyle = infoStyle.Background(t.Warning())
|
|
||||||
case "error":
|
|
||||||
infoStyle = infoStyle.Background(t.Error())
|
|
||||||
case "debug":
|
|
||||||
infoStyle = infoStyle.Background(t.TextMuted())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Truncate message if it's longer than available width
|
|
||||||
msg := sm.Message
|
|
||||||
availWidth := statusWidth - 10
|
|
||||||
|
|
||||||
// If we have enough space, show inline
|
|
||||||
if availWidth >= minInlineWidth {
|
|
||||||
if len(msg) > availWidth && availWidth > 0 {
|
|
||||||
msg = msg[:availWidth] + "..."
|
|
||||||
}
|
|
||||||
status += infoStyle.Width(statusWidth).Render(msg)
|
|
||||||
} else {
|
|
||||||
// Otherwise, prepare a full-width message to show above
|
|
||||||
if len(msg) > m.width-10 && m.width > 10 {
|
|
||||||
msg = msg[:m.width-10] + "..."
|
|
||||||
}
|
|
||||||
statusMessage = infoStyle.Width(m.width).Render(msg)
|
|
||||||
|
|
||||||
// Add empty space in the status bar
|
|
||||||
status += styles.Padded().
|
|
||||||
Foreground(t.Text()).
|
|
||||||
Background(t.BackgroundSecondary()).
|
|
||||||
Width(statusWidth).
|
|
||||||
Render("")
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
status += styles.Padded().
|
|
||||||
Foreground(t.Text()).
|
|
||||||
Background(t.BackgroundSecondary()).
|
|
||||||
Width(statusWidth).
|
|
||||||
Render("")
|
|
||||||
}
|
|
||||||
|
|
||||||
status += diagnostics
|
|
||||||
status += modelName
|
|
||||||
|
|
||||||
// If we have a separate status message, prepend it
|
// If we have a separate status message, prepend it
|
||||||
if statusMessage != "" {
|
// if statusMessage != "" {
|
||||||
return statusMessage + "\n" + status
|
// return statusMessage + "\n" + status
|
||||||
} else {
|
// } else {
|
||||||
blank := styles.BaseStyle().Background(t.Background()).Width(m.width).Render("")
|
// blank := styles.BaseStyle().Background(t.Background()).Width(m.width).Render("")
|
||||||
return blank + "\n" + status
|
// return blank + "\n" + status
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *statusCmp) projectDiagnostics() string {
|
func (m *statusCmp) projectDiagnostics() string {
|
||||||
|
@ -281,7 +291,7 @@ func (m *statusCmp) projectDiagnostics() string {
|
||||||
// }
|
// }
|
||||||
return styles.ForceReplaceBackgroundWithLipgloss(
|
return styles.ForceReplaceBackgroundWithLipgloss(
|
||||||
styles.Padded().Render("No diagnostics"),
|
styles.Padded().Render("No diagnostics"),
|
||||||
t.BackgroundDarker(),
|
t.BackgroundElement(),
|
||||||
)
|
)
|
||||||
|
|
||||||
// if len(errorDiagnostics) == 0 &&
|
// if len(errorDiagnostics) == 0 &&
|
||||||
|
|
|
@ -22,7 +22,7 @@ const (
|
||||||
var namedArgPattern = regexp.MustCompile(`\$([A-Z][A-Z0-9_]*)`)
|
var namedArgPattern = regexp.MustCompile(`\$([A-Z][A-Z0-9_]*)`)
|
||||||
|
|
||||||
// LoadCustomCommands loads custom commands from both XDG_CONFIG_HOME and project data directory
|
// LoadCustomCommands loads custom commands from both XDG_CONFIG_HOME and project data directory
|
||||||
func LoadCustomCommands(app *app.App) ([]Command, error) {
|
func LoadCustomCommands() ([]Command, error) {
|
||||||
var commands []Command
|
var commands []Command
|
||||||
|
|
||||||
homeCommandsDir := filepath.Join(app.Info.Path.Config, "commands")
|
homeCommandsDir := filepath.Join(app.Info.Path.Config, "commands")
|
||||||
|
|
|
@ -404,10 +404,10 @@ func SyntaxHighlight(w io.Writer, source, fileName, formatter string, bg lipglos
|
||||||
<entry type="TextWhitespace" style="%s"/>
|
<entry type="TextWhitespace" style="%s"/>
|
||||||
</style>
|
</style>
|
||||||
`,
|
`,
|
||||||
getColor(t.Background()), // Background
|
getColor(t.BackgroundSubtle()), // Background
|
||||||
getColor(t.Text()), // Text
|
getColor(t.Text()), // Text
|
||||||
getColor(t.Text()), // Other
|
getColor(t.Text()), // Other
|
||||||
getColor(t.Error()), // Error
|
getColor(t.Error()), // Error
|
||||||
|
|
||||||
getColor(t.SyntaxKeyword()), // Keyword
|
getColor(t.SyntaxKeyword()), // Keyword
|
||||||
getColor(t.SyntaxKeyword()), // KeywordConstant
|
getColor(t.SyntaxKeyword()), // KeywordConstant
|
||||||
|
@ -531,8 +531,7 @@ func createStyles(t theme.Theme) (removedLineStyle, addedLineStyle, contextLineS
|
||||||
removedLineStyle = lipgloss.NewStyle().Background(t.DiffRemovedBg())
|
removedLineStyle = lipgloss.NewStyle().Background(t.DiffRemovedBg())
|
||||||
addedLineStyle = lipgloss.NewStyle().Background(t.DiffAddedBg())
|
addedLineStyle = lipgloss.NewStyle().Background(t.DiffAddedBg())
|
||||||
contextLineStyle = lipgloss.NewStyle().Background(t.DiffContextBg())
|
contextLineStyle = lipgloss.NewStyle().Background(t.DiffContextBg())
|
||||||
lineNumberStyle = lipgloss.NewStyle().Foreground(t.DiffLineNumber())
|
lineNumberStyle = lipgloss.NewStyle().Background(t.DiffLineNumber()).Foreground(t.TextMuted())
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -581,7 +580,7 @@ func applyHighlighting(content string, segments []Segment, segmentType LineType,
|
||||||
|
|
||||||
// Get the appropriate color based on terminal background
|
// Get the appropriate color based on terminal background
|
||||||
bgColor := lipgloss.Color(getColor(highlightBg))
|
bgColor := lipgloss.Color(getColor(highlightBg))
|
||||||
fgColor := lipgloss.Color(getColor(theme.CurrentTheme().Background()))
|
fgColor := lipgloss.Color(getColor(theme.CurrentTheme().BackgroundSubtle()))
|
||||||
|
|
||||||
for i := 0; i < len(content); {
|
for i := 0; i < len(content); {
|
||||||
// Check if we're at an ANSI sequence
|
// Check if we're at an ANSI sequence
|
||||||
|
@ -794,24 +793,24 @@ func RenderSideBySideHunk(fileName string, h Hunk, opts ...SideBySideOption) str
|
||||||
}
|
}
|
||||||
|
|
||||||
// FormatDiff creates a side-by-side formatted view of a diff
|
// FormatDiff creates a side-by-side formatted view of a diff
|
||||||
func FormatDiff(diffText string, opts ...SideBySideOption) (string, error) {
|
func FormatDiff(filename string, diffText string, opts ...SideBySideOption) (string, error) {
|
||||||
t := theme.CurrentTheme()
|
// t := theme.CurrentTheme()
|
||||||
diffResult, err := ParseUnifiedDiff(diffText)
|
diffResult, err := ParseUnifiedDiff(diffText)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
var sb strings.Builder
|
var sb strings.Builder
|
||||||
config := NewSideBySideConfig(opts...)
|
// config := NewSideBySideConfig(opts...)
|
||||||
for _, h := range diffResult.Hunks {
|
for _, h := range diffResult.Hunks {
|
||||||
sb.WriteString(
|
// sb.WriteString(
|
||||||
lipgloss.NewStyle().
|
// lipgloss.NewStyle().
|
||||||
Background(t.DiffHunkHeader()).
|
// Background(t.DiffHunkHeader()).
|
||||||
Foreground(t.Background()).
|
// Foreground(t.Background()).
|
||||||
Width(config.TotalWidth).
|
// Width(config.TotalWidth).
|
||||||
Render(h.Header) + "\n",
|
// Render(h.Header) + "\n",
|
||||||
)
|
// )
|
||||||
sb.WriteString(RenderSideBySideHunk(diffResult.OldFile, h, opts...))
|
sb.WriteString(RenderSideBySideHunk(filename, h, opts...))
|
||||||
}
|
}
|
||||||
|
|
||||||
return sb.String(), nil
|
return sb.String(), nil
|
||||||
|
|
|
@ -1,127 +0,0 @@
|
||||||
package spinner
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"github.com/charmbracelet/bubbles/spinner"
|
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
|
||||||
"github.com/charmbracelet/lipgloss"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Spinner wraps the bubbles spinner for both interactive and non-interactive mode
|
|
||||||
type Spinner struct {
|
|
||||||
model spinner.Model
|
|
||||||
done chan struct{}
|
|
||||||
prog *tea.Program
|
|
||||||
ctx context.Context
|
|
||||||
cancel context.CancelFunc
|
|
||||||
}
|
|
||||||
|
|
||||||
// spinnerModel is the tea.Model for the spinner
|
|
||||||
type spinnerModel struct {
|
|
||||||
spinner spinner.Model
|
|
||||||
message string
|
|
||||||
quitting bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m spinnerModel) Init() tea.Cmd {
|
|
||||||
return m.spinner.Tick
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m spinnerModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|
||||||
switch msg := msg.(type) {
|
|
||||||
case tea.KeyMsg:
|
|
||||||
m.quitting = true
|
|
||||||
return m, tea.Quit
|
|
||||||
case spinner.TickMsg:
|
|
||||||
var cmd tea.Cmd
|
|
||||||
m.spinner, cmd = m.spinner.Update(msg)
|
|
||||||
return m, cmd
|
|
||||||
case quitMsg:
|
|
||||||
m.quitting = true
|
|
||||||
return m, tea.Quit
|
|
||||||
default:
|
|
||||||
return m, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m spinnerModel) View() string {
|
|
||||||
if m.quitting {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return fmt.Sprintf("%s %s", m.spinner.View(), m.message)
|
|
||||||
}
|
|
||||||
|
|
||||||
// quitMsg is sent when we want to quit the spinner
|
|
||||||
type quitMsg struct{}
|
|
||||||
|
|
||||||
// NewSpinner creates a new spinner with the given message
|
|
||||||
func NewSpinner(message string) *Spinner {
|
|
||||||
s := spinner.New()
|
|
||||||
s.Spinner = spinner.Dot
|
|
||||||
s.Style = s.Style.Foreground(s.Style.GetForeground())
|
|
||||||
|
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
|
||||||
|
|
||||||
model := spinnerModel{
|
|
||||||
spinner: s,
|
|
||||||
message: message,
|
|
||||||
}
|
|
||||||
|
|
||||||
prog := tea.NewProgram(model, tea.WithOutput(os.Stderr), tea.WithoutCatchPanics())
|
|
||||||
|
|
||||||
return &Spinner{
|
|
||||||
model: s,
|
|
||||||
done: make(chan struct{}),
|
|
||||||
prog: prog,
|
|
||||||
ctx: ctx,
|
|
||||||
cancel: cancel,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewThemedSpinner creates a new spinner with the given message and color
|
|
||||||
func NewThemedSpinner(message string, color lipgloss.AdaptiveColor) *Spinner {
|
|
||||||
s := spinner.New()
|
|
||||||
s.Spinner = spinner.Dot
|
|
||||||
s.Style = s.Style.Foreground(color)
|
|
||||||
|
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
|
||||||
|
|
||||||
model := spinnerModel{
|
|
||||||
spinner: s,
|
|
||||||
message: message,
|
|
||||||
}
|
|
||||||
|
|
||||||
prog := tea.NewProgram(model, tea.WithOutput(os.Stderr), tea.WithoutCatchPanics())
|
|
||||||
|
|
||||||
return &Spinner{
|
|
||||||
model: s,
|
|
||||||
done: make(chan struct{}),
|
|
||||||
prog: prog,
|
|
||||||
ctx: ctx,
|
|
||||||
cancel: cancel,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start begins the spinner animation
|
|
||||||
func (s *Spinner) Start() {
|
|
||||||
go func() {
|
|
||||||
defer close(s.done)
|
|
||||||
go func() {
|
|
||||||
<-s.ctx.Done()
|
|
||||||
s.prog.Send(quitMsg{})
|
|
||||||
}()
|
|
||||||
_, err := s.prog.Run()
|
|
||||||
if err != nil {
|
|
||||||
fmt.Fprintf(os.Stderr, "Error running spinner: %v\n", err)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Stop ends the spinner animation
|
|
||||||
func (s *Spinner) Stop() {
|
|
||||||
s.cancel()
|
|
||||||
<-s.done
|
|
||||||
}
|
|
|
@ -1,24 +0,0 @@
|
||||||
package spinner
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestSpinner(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
// Create a spinner
|
|
||||||
s := NewSpinner("Test spinner")
|
|
||||||
|
|
||||||
// Start the spinner
|
|
||||||
s.Start()
|
|
||||||
|
|
||||||
// Wait a bit to let it run
|
|
||||||
time.Sleep(100 * time.Millisecond)
|
|
||||||
|
|
||||||
// Stop the spinner
|
|
||||||
s.Stop()
|
|
||||||
|
|
||||||
// If we got here without panicking, the test passes
|
|
||||||
}
|
|
|
@ -13,6 +13,8 @@ type Container interface {
|
||||||
Bindings
|
Bindings
|
||||||
Focus()
|
Focus()
|
||||||
Blur()
|
Blur()
|
||||||
|
MaxWidth() int
|
||||||
|
Alignment() lipgloss.Position
|
||||||
}
|
}
|
||||||
|
|
||||||
type container struct {
|
type container struct {
|
||||||
|
@ -32,6 +34,9 @@ type container struct {
|
||||||
borderLeft bool
|
borderLeft bool
|
||||||
borderStyle lipgloss.Border
|
borderStyle lipgloss.Border
|
||||||
|
|
||||||
|
maxWidth int
|
||||||
|
align lipgloss.Position
|
||||||
|
|
||||||
focused bool
|
focused bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -51,6 +56,11 @@ func (c *container) View() string {
|
||||||
width := c.width
|
width := c.width
|
||||||
height := c.height
|
height := c.height
|
||||||
|
|
||||||
|
// Apply max width constraint if set
|
||||||
|
if c.maxWidth > 0 && width > c.maxWidth {
|
||||||
|
width = c.maxWidth
|
||||||
|
}
|
||||||
|
|
||||||
style = style.Background(t.Background())
|
style = style.Background(t.Background())
|
||||||
|
|
||||||
// Apply border if any side is enabled
|
// Apply border if any side is enabled
|
||||||
|
@ -74,7 +84,7 @@ func (c *container) View() string {
|
||||||
if c.focused {
|
if c.focused {
|
||||||
style = style.BorderBackground(t.Background()).BorderForeground(t.Primary())
|
style = style.BorderBackground(t.Background()).BorderForeground(t.Primary())
|
||||||
} else {
|
} else {
|
||||||
style = style.BorderBackground(t.Background()).BorderForeground(t.BorderNormal())
|
style = style.BorderBackground(t.Background()).BorderForeground(t.Border())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
style = style.
|
style = style.
|
||||||
|
@ -92,6 +102,12 @@ func (c *container) SetSize(width, height int) tea.Cmd {
|
||||||
c.width = width
|
c.width = width
|
||||||
c.height = height
|
c.height = height
|
||||||
|
|
||||||
|
// Apply max width constraint if set
|
||||||
|
effectiveWidth := width
|
||||||
|
if c.maxWidth > 0 && width > c.maxWidth {
|
||||||
|
effectiveWidth = c.maxWidth
|
||||||
|
}
|
||||||
|
|
||||||
// If the content implements Sizeable, adjust its size to account for padding and borders
|
// If the content implements Sizeable, adjust its size to account for padding and borders
|
||||||
if sizeable, ok := c.content.(Sizeable); ok {
|
if sizeable, ok := c.content.(Sizeable); ok {
|
||||||
// Calculate horizontal space taken by padding and borders
|
// Calculate horizontal space taken by padding and borders
|
||||||
|
@ -113,7 +129,7 @@ func (c *container) SetSize(width, height int) tea.Cmd {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set content size with adjusted dimensions
|
// Set content size with adjusted dimensions
|
||||||
contentWidth := max(0, width-horizontalSpace)
|
contentWidth := max(0, effectiveWidth-horizontalSpace)
|
||||||
contentHeight := max(0, height-verticalSpace)
|
contentHeight := max(0, height-verticalSpace)
|
||||||
return sizeable.SetSize(contentWidth, contentHeight)
|
return sizeable.SetSize(contentWidth, contentHeight)
|
||||||
}
|
}
|
||||||
|
@ -124,6 +140,14 @@ func (c *container) GetSize() (int, int) {
|
||||||
return c.width, c.height
|
return c.width, c.height
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *container) MaxWidth() int {
|
||||||
|
return c.maxWidth
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *container) Alignment() lipgloss.Position {
|
||||||
|
return c.align
|
||||||
|
}
|
||||||
|
|
||||||
func (c *container) BindingKeys() []key.Binding {
|
func (c *container) BindingKeys() []key.Binding {
|
||||||
if b, ok := c.content.(Bindings); ok {
|
if b, ok := c.content.(Bindings); ok {
|
||||||
return b.BindingKeys()
|
return b.BindingKeys()
|
||||||
|
@ -228,3 +252,27 @@ func WithThickBorder() ContainerOption {
|
||||||
func WithDoubleBorder() ContainerOption {
|
func WithDoubleBorder() ContainerOption {
|
||||||
return WithBorderStyle(lipgloss.DoubleBorder())
|
return WithBorderStyle(lipgloss.DoubleBorder())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func WithMaxWidth(maxWidth int) ContainerOption {
|
||||||
|
return func(c *container) {
|
||||||
|
c.maxWidth = maxWidth
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithAlign(align lipgloss.Position) ContainerOption {
|
||||||
|
return func(c *container) {
|
||||||
|
c.align = align
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithAlignLeft() ContainerOption {
|
||||||
|
return WithAlign(lipgloss.Left)
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithAlignCenter() ContainerOption {
|
||||||
|
return WithAlign(lipgloss.Center)
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithAlignRight() ContainerOption {
|
||||||
|
return WithAlign(lipgloss.Right)
|
||||||
|
}
|
||||||
|
|
248
packages/tui/internal/layout/flex.go
Normal file
248
packages/tui/internal/layout/flex.go
Normal file
|
@ -0,0 +1,248 @@
|
||||||
|
package layout
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/charmbracelet/bubbles/key"
|
||||||
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
|
"github.com/charmbracelet/lipgloss"
|
||||||
|
"github.com/sst/opencode/internal/theme"
|
||||||
|
)
|
||||||
|
|
||||||
|
type FlexDirection int
|
||||||
|
|
||||||
|
const (
|
||||||
|
FlexDirectionHorizontal FlexDirection = iota
|
||||||
|
FlexDirectionVertical
|
||||||
|
)
|
||||||
|
|
||||||
|
type FlexPaneSize struct {
|
||||||
|
Fixed bool
|
||||||
|
Size int
|
||||||
|
}
|
||||||
|
|
||||||
|
var FlexPaneSizeGrow = FlexPaneSize{Fixed: false}
|
||||||
|
|
||||||
|
func FlexPaneSizeFixed(size int) FlexPaneSize {
|
||||||
|
return FlexPaneSize{Fixed: true, Size: size}
|
||||||
|
}
|
||||||
|
|
||||||
|
type FlexLayout interface {
|
||||||
|
tea.Model
|
||||||
|
Sizeable
|
||||||
|
Bindings
|
||||||
|
SetPanes(panes []Container) tea.Cmd
|
||||||
|
SetPaneSizes(sizes []FlexPaneSize) tea.Cmd
|
||||||
|
SetDirection(direction FlexDirection) tea.Cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
type flexLayout struct {
|
||||||
|
width int
|
||||||
|
height int
|
||||||
|
direction FlexDirection
|
||||||
|
panes []Container
|
||||||
|
sizes []FlexPaneSize
|
||||||
|
}
|
||||||
|
|
||||||
|
type FlexLayoutOption func(*flexLayout)
|
||||||
|
|
||||||
|
func (f *flexLayout) Init() tea.Cmd {
|
||||||
|
var cmds []tea.Cmd
|
||||||
|
for _, pane := range f.panes {
|
||||||
|
if pane != nil {
|
||||||
|
cmds = append(cmds, pane.Init())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return tea.Batch(cmds...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *flexLayout) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
|
var cmds []tea.Cmd
|
||||||
|
switch msg := msg.(type) {
|
||||||
|
case tea.WindowSizeMsg:
|
||||||
|
return f, f.SetSize(msg.Width, msg.Height)
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, pane := range f.panes {
|
||||||
|
if pane != nil {
|
||||||
|
u, cmd := pane.Update(msg)
|
||||||
|
f.panes[i] = u.(Container)
|
||||||
|
if cmd != nil {
|
||||||
|
cmds = append(cmds, cmd)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return f, tea.Batch(cmds...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *flexLayout) View() string {
|
||||||
|
t := theme.CurrentTheme()
|
||||||
|
|
||||||
|
if len(f.panes) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
views := make([]string, 0, len(f.panes))
|
||||||
|
for i, pane := range f.panes {
|
||||||
|
if pane == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
var paneWidth, paneHeight int
|
||||||
|
if f.direction == FlexDirectionHorizontal {
|
||||||
|
paneWidth, paneHeight = f.calculatePaneSize(i)
|
||||||
|
view := lipgloss.PlaceHorizontal(
|
||||||
|
paneWidth,
|
||||||
|
pane.Alignment(),
|
||||||
|
pane.View(),
|
||||||
|
lipgloss.WithWhitespaceBackground(t.Background()),
|
||||||
|
)
|
||||||
|
views = append(views, view)
|
||||||
|
} else {
|
||||||
|
paneWidth, paneHeight = f.calculatePaneSize(i)
|
||||||
|
view := lipgloss.Place(
|
||||||
|
f.width,
|
||||||
|
paneHeight,
|
||||||
|
lipgloss.Center,
|
||||||
|
pane.Alignment(),
|
||||||
|
pane.View(),
|
||||||
|
lipgloss.WithWhitespaceBackground(t.Background()),
|
||||||
|
)
|
||||||
|
views = append(views, view)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if f.direction == FlexDirectionHorizontal {
|
||||||
|
return lipgloss.JoinHorizontal(lipgloss.Center, views...)
|
||||||
|
}
|
||||||
|
return lipgloss.JoinVertical(lipgloss.Center, views...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *flexLayout) calculatePaneSize(index int) (width, height int) {
|
||||||
|
if index >= len(f.panes) {
|
||||||
|
return 0, 0
|
||||||
|
}
|
||||||
|
|
||||||
|
totalFixed := 0
|
||||||
|
flexCount := 0
|
||||||
|
|
||||||
|
for i, pane := range f.panes {
|
||||||
|
if pane == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if i < len(f.sizes) && f.sizes[i].Fixed {
|
||||||
|
if f.direction == FlexDirectionHorizontal {
|
||||||
|
totalFixed += f.sizes[i].Size
|
||||||
|
} else {
|
||||||
|
totalFixed += f.sizes[i].Size
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
flexCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if f.direction == FlexDirectionHorizontal {
|
||||||
|
height = f.height
|
||||||
|
if index < len(f.sizes) && f.sizes[index].Fixed {
|
||||||
|
width = f.sizes[index].Size
|
||||||
|
} else if flexCount > 0 {
|
||||||
|
remainingSpace := f.width - totalFixed
|
||||||
|
width = remainingSpace / flexCount
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
width = f.width
|
||||||
|
if index < len(f.sizes) && f.sizes[index].Fixed {
|
||||||
|
height = f.sizes[index].Size
|
||||||
|
} else if flexCount > 0 {
|
||||||
|
remainingSpace := f.height - totalFixed
|
||||||
|
height = remainingSpace / flexCount
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return width, height
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *flexLayout) SetSize(width, height int) tea.Cmd {
|
||||||
|
f.width = width
|
||||||
|
f.height = height
|
||||||
|
|
||||||
|
var cmds []tea.Cmd
|
||||||
|
for i, pane := range f.panes {
|
||||||
|
if pane != nil {
|
||||||
|
paneWidth, paneHeight := f.calculatePaneSize(i)
|
||||||
|
cmd := pane.SetSize(paneWidth, paneHeight)
|
||||||
|
cmds = append(cmds, cmd)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return tea.Batch(cmds...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *flexLayout) GetSize() (int, int) {
|
||||||
|
return f.width, f.height
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *flexLayout) SetPanes(panes []Container) tea.Cmd {
|
||||||
|
f.panes = panes
|
||||||
|
if f.width > 0 && f.height > 0 {
|
||||||
|
return f.SetSize(f.width, f.height)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *flexLayout) SetPaneSizes(sizes []FlexPaneSize) tea.Cmd {
|
||||||
|
f.sizes = sizes
|
||||||
|
if f.width > 0 && f.height > 0 {
|
||||||
|
return f.SetSize(f.width, f.height)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *flexLayout) SetDirection(direction FlexDirection) tea.Cmd {
|
||||||
|
f.direction = direction
|
||||||
|
if f.width > 0 && f.height > 0 {
|
||||||
|
return f.SetSize(f.width, f.height)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *flexLayout) BindingKeys() []key.Binding {
|
||||||
|
keys := []key.Binding{}
|
||||||
|
for _, pane := range f.panes {
|
||||||
|
if pane != nil {
|
||||||
|
if b, ok := pane.(Bindings); ok {
|
||||||
|
keys = append(keys, b.BindingKeys()...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return keys
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewFlexLayout(options ...FlexLayoutOption) FlexLayout {
|
||||||
|
layout := &flexLayout{
|
||||||
|
direction: FlexDirectionHorizontal,
|
||||||
|
panes: []Container{},
|
||||||
|
sizes: []FlexPaneSize{},
|
||||||
|
}
|
||||||
|
for _, option := range options {
|
||||||
|
option(layout)
|
||||||
|
}
|
||||||
|
return layout
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithDirection(direction FlexDirection) FlexLayoutOption {
|
||||||
|
return func(f *flexLayout) {
|
||||||
|
f.direction = direction
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithPanes(panes ...Container) FlexLayoutOption {
|
||||||
|
return func(f *flexLayout) {
|
||||||
|
f.panes = panes
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithPaneSizes(sizes ...FlexPaneSize) FlexLayoutOption {
|
||||||
|
return func(f *flexLayout) {
|
||||||
|
f.sizes = sizes
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -7,6 +7,35 @@ import (
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var Current *LayoutInfo
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
Current = &LayoutInfo{
|
||||||
|
Size: LayoutSizeNormal,
|
||||||
|
Viewport: Dimensions{Width: 80, Height: 25},
|
||||||
|
Container: Dimensions{Width: 80, Height: 25},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type LayoutSize string
|
||||||
|
|
||||||
|
const (
|
||||||
|
LayoutSizeSmall LayoutSize = "small"
|
||||||
|
LayoutSizeNormal LayoutSize = "normal"
|
||||||
|
LayoutSizeLarge LayoutSize = "large"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Dimensions struct {
|
||||||
|
Width int
|
||||||
|
Height int
|
||||||
|
}
|
||||||
|
|
||||||
|
type LayoutInfo struct {
|
||||||
|
Size LayoutSize
|
||||||
|
Viewport Dimensions
|
||||||
|
Container Dimensions
|
||||||
|
}
|
||||||
|
|
||||||
type Focusable interface {
|
type Focusable interface {
|
||||||
Focus() tea.Cmd
|
Focus() tea.Cmd
|
||||||
Blur() tea.Cmd
|
Blur() tea.Cmd
|
||||||
|
|
|
@ -17,18 +17,15 @@ import (
|
||||||
// https://github.com/charmbracelet/lipgloss/pull/102
|
// https://github.com/charmbracelet/lipgloss/pull/102
|
||||||
// as well as the lipgloss library, with some modification for what I needed.
|
// as well as the lipgloss library, with some modification for what I needed.
|
||||||
|
|
||||||
// Split a string into lines, additionally returning the size of the widest
|
// Split a string into lines, additionally returning the size of the widest line.
|
||||||
// line.
|
|
||||||
func getLines(s string) (lines []string, widest int) {
|
func getLines(s string) (lines []string, widest int) {
|
||||||
lines = strings.Split(s, "\n")
|
lines = strings.Split(s, "\n")
|
||||||
|
|
||||||
for _, l := range lines {
|
for _, l := range lines {
|
||||||
w := ansi.PrintableRuneWidth(l)
|
w := ansi.PrintableRuneWidth(l)
|
||||||
if widest < w {
|
if widest < w {
|
||||||
widest = w
|
widest = w
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return lines, widest
|
return lines, widest
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -49,7 +46,7 @@ func PlaceOverlay(
|
||||||
|
|
||||||
var shadowbg string = ""
|
var shadowbg string = ""
|
||||||
shadowchar := lipgloss.NewStyle().
|
shadowchar := lipgloss.NewStyle().
|
||||||
Background(t.BackgroundDarker()).
|
Background(t.BackgroundElement()).
|
||||||
Foreground(t.Background()).
|
Foreground(t.Background()).
|
||||||
Render("░")
|
Render("░")
|
||||||
bgchar := baseStyle.Render(" ")
|
bgchar := baseStyle.Render(" ")
|
||||||
|
|
|
@ -1,283 +0,0 @@
|
||||||
package layout
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/charmbracelet/bubbles/key"
|
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
|
||||||
"github.com/charmbracelet/lipgloss"
|
|
||||||
"github.com/sst/opencode/internal/theme"
|
|
||||||
)
|
|
||||||
|
|
||||||
type SplitPaneLayout interface {
|
|
||||||
tea.Model
|
|
||||||
Sizeable
|
|
||||||
Bindings
|
|
||||||
SetLeftPanel(panel Container) tea.Cmd
|
|
||||||
SetRightPanel(panel Container) tea.Cmd
|
|
||||||
SetBottomPanel(panel Container) tea.Cmd
|
|
||||||
|
|
||||||
ClearLeftPanel() tea.Cmd
|
|
||||||
ClearRightPanel() tea.Cmd
|
|
||||||
ClearBottomPanel() tea.Cmd
|
|
||||||
}
|
|
||||||
|
|
||||||
type splitPaneLayout struct {
|
|
||||||
width int
|
|
||||||
height int
|
|
||||||
ratio float64
|
|
||||||
verticalRatio float64
|
|
||||||
|
|
||||||
rightPanel Container
|
|
||||||
leftPanel Container
|
|
||||||
bottomPanel Container
|
|
||||||
}
|
|
||||||
|
|
||||||
type SplitPaneOption func(*splitPaneLayout)
|
|
||||||
|
|
||||||
func (s *splitPaneLayout) Init() tea.Cmd {
|
|
||||||
var cmds []tea.Cmd
|
|
||||||
|
|
||||||
if s.leftPanel != nil {
|
|
||||||
cmds = append(cmds, s.leftPanel.Init())
|
|
||||||
}
|
|
||||||
|
|
||||||
if s.rightPanel != nil {
|
|
||||||
cmds = append(cmds, s.rightPanel.Init())
|
|
||||||
}
|
|
||||||
|
|
||||||
if s.bottomPanel != nil {
|
|
||||||
cmds = append(cmds, s.bottomPanel.Init())
|
|
||||||
}
|
|
||||||
|
|
||||||
return tea.Batch(cmds...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *splitPaneLayout) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|
||||||
var cmds []tea.Cmd
|
|
||||||
switch msg := msg.(type) {
|
|
||||||
case tea.WindowSizeMsg:
|
|
||||||
return s, s.SetSize(msg.Width, msg.Height)
|
|
||||||
}
|
|
||||||
|
|
||||||
if s.rightPanel != nil {
|
|
||||||
u, cmd := s.rightPanel.Update(msg)
|
|
||||||
s.rightPanel = u.(Container)
|
|
||||||
if cmd != nil {
|
|
||||||
cmds = append(cmds, cmd)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if s.leftPanel != nil {
|
|
||||||
u, cmd := s.leftPanel.Update(msg)
|
|
||||||
s.leftPanel = u.(Container)
|
|
||||||
if cmd != nil {
|
|
||||||
cmds = append(cmds, cmd)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if s.bottomPanel != nil {
|
|
||||||
u, cmd := s.bottomPanel.Update(msg)
|
|
||||||
s.bottomPanel = u.(Container)
|
|
||||||
if cmd != nil {
|
|
||||||
cmds = append(cmds, cmd)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return s, tea.Batch(cmds...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *splitPaneLayout) View() string {
|
|
||||||
var topSection string
|
|
||||||
|
|
||||||
if s.leftPanel != nil && s.rightPanel != nil {
|
|
||||||
leftView := s.leftPanel.View()
|
|
||||||
rightView := s.rightPanel.View()
|
|
||||||
topSection = lipgloss.JoinHorizontal(lipgloss.Top, leftView, rightView)
|
|
||||||
} else if s.leftPanel != nil {
|
|
||||||
topSection = s.leftPanel.View()
|
|
||||||
} else if s.rightPanel != nil {
|
|
||||||
topSection = s.rightPanel.View()
|
|
||||||
} else {
|
|
||||||
topSection = ""
|
|
||||||
}
|
|
||||||
|
|
||||||
var finalView string
|
|
||||||
|
|
||||||
if s.bottomPanel != nil && topSection != "" {
|
|
||||||
bottomView := s.bottomPanel.View()
|
|
||||||
finalView = lipgloss.JoinVertical(lipgloss.Left, topSection, bottomView)
|
|
||||||
} else if s.bottomPanel != nil {
|
|
||||||
finalView = s.bottomPanel.View()
|
|
||||||
} else {
|
|
||||||
finalView = topSection
|
|
||||||
}
|
|
||||||
|
|
||||||
if finalView != "" {
|
|
||||||
t := theme.CurrentTheme()
|
|
||||||
|
|
||||||
style := lipgloss.NewStyle().
|
|
||||||
Width(s.width).
|
|
||||||
Height(s.height).
|
|
||||||
Background(t.Background())
|
|
||||||
|
|
||||||
return style.Render(finalView)
|
|
||||||
}
|
|
||||||
|
|
||||||
return finalView
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *splitPaneLayout) SetSize(width, height int) tea.Cmd {
|
|
||||||
s.width = width
|
|
||||||
s.height = height
|
|
||||||
|
|
||||||
var topHeight, bottomHeight int
|
|
||||||
if s.bottomPanel != nil {
|
|
||||||
topHeight = int(float64(height) * s.verticalRatio)
|
|
||||||
bottomHeight = height - topHeight
|
|
||||||
} else {
|
|
||||||
topHeight = height
|
|
||||||
bottomHeight = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
var leftWidth, rightWidth int
|
|
||||||
if s.leftPanel != nil && s.rightPanel != nil {
|
|
||||||
leftWidth = int(float64(width) * s.ratio)
|
|
||||||
rightWidth = width - leftWidth
|
|
||||||
} else if s.leftPanel != nil {
|
|
||||||
leftWidth = width
|
|
||||||
rightWidth = 0
|
|
||||||
} else if s.rightPanel != nil {
|
|
||||||
leftWidth = 0
|
|
||||||
rightWidth = width
|
|
||||||
}
|
|
||||||
|
|
||||||
var cmds []tea.Cmd
|
|
||||||
if s.leftPanel != nil {
|
|
||||||
cmd := s.leftPanel.SetSize(leftWidth, topHeight)
|
|
||||||
cmds = append(cmds, cmd)
|
|
||||||
}
|
|
||||||
|
|
||||||
if s.rightPanel != nil {
|
|
||||||
cmd := s.rightPanel.SetSize(rightWidth, topHeight)
|
|
||||||
cmds = append(cmds, cmd)
|
|
||||||
}
|
|
||||||
|
|
||||||
if s.bottomPanel != nil {
|
|
||||||
cmd := s.bottomPanel.SetSize(width, bottomHeight)
|
|
||||||
cmds = append(cmds, cmd)
|
|
||||||
}
|
|
||||||
return tea.Batch(cmds...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *splitPaneLayout) GetSize() (int, int) {
|
|
||||||
return s.width, s.height
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *splitPaneLayout) SetLeftPanel(panel Container) tea.Cmd {
|
|
||||||
s.leftPanel = panel
|
|
||||||
if s.width > 0 && s.height > 0 {
|
|
||||||
return s.SetSize(s.width, s.height)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *splitPaneLayout) SetRightPanel(panel Container) tea.Cmd {
|
|
||||||
s.rightPanel = panel
|
|
||||||
if s.width > 0 && s.height > 0 {
|
|
||||||
return s.SetSize(s.width, s.height)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *splitPaneLayout) SetBottomPanel(panel Container) tea.Cmd {
|
|
||||||
s.bottomPanel = panel
|
|
||||||
if s.width > 0 && s.height > 0 {
|
|
||||||
return s.SetSize(s.width, s.height)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *splitPaneLayout) ClearLeftPanel() tea.Cmd {
|
|
||||||
s.leftPanel = nil
|
|
||||||
if s.width > 0 && s.height > 0 {
|
|
||||||
return s.SetSize(s.width, s.height)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *splitPaneLayout) ClearRightPanel() tea.Cmd {
|
|
||||||
s.rightPanel = nil
|
|
||||||
if s.width > 0 && s.height > 0 {
|
|
||||||
return s.SetSize(s.width, s.height)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *splitPaneLayout) ClearBottomPanel() tea.Cmd {
|
|
||||||
s.bottomPanel = nil
|
|
||||||
if s.width > 0 && s.height > 0 {
|
|
||||||
return s.SetSize(s.width, s.height)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *splitPaneLayout) BindingKeys() []key.Binding {
|
|
||||||
keys := []key.Binding{}
|
|
||||||
if s.leftPanel != nil {
|
|
||||||
if b, ok := s.leftPanel.(Bindings); ok {
|
|
||||||
keys = append(keys, b.BindingKeys()...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if s.rightPanel != nil {
|
|
||||||
if b, ok := s.rightPanel.(Bindings); ok {
|
|
||||||
keys = append(keys, b.BindingKeys()...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if s.bottomPanel != nil {
|
|
||||||
if b, ok := s.bottomPanel.(Bindings); ok {
|
|
||||||
keys = append(keys, b.BindingKeys()...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return keys
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewSplitPane(options ...SplitPaneOption) SplitPaneLayout {
|
|
||||||
|
|
||||||
layout := &splitPaneLayout{
|
|
||||||
ratio: 0.7,
|
|
||||||
verticalRatio: 0.9, // Default 90% for top section, 10% for bottom
|
|
||||||
}
|
|
||||||
for _, option := range options {
|
|
||||||
option(layout)
|
|
||||||
}
|
|
||||||
return layout
|
|
||||||
}
|
|
||||||
|
|
||||||
func WithLeftPanel(panel Container) SplitPaneOption {
|
|
||||||
return func(s *splitPaneLayout) {
|
|
||||||
s.leftPanel = panel
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func WithRightPanel(panel Container) SplitPaneOption {
|
|
||||||
return func(s *splitPaneLayout) {
|
|
||||||
s.rightPanel = panel
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func WithRatio(ratio float64) SplitPaneOption {
|
|
||||||
return func(s *splitPaneLayout) {
|
|
||||||
s.ratio = ratio
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func WithBottomPanel(panel Container) SplitPaneOption {
|
|
||||||
return func(s *splitPaneLayout) {
|
|
||||||
s.bottomPanel = panel
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func WithVerticalRatio(ratio float64) SplitPaneOption {
|
|
||||||
return func(s *splitPaneLayout) {
|
|
||||||
s.verticalRatio = ratio
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -24,7 +24,7 @@ type chatPage struct {
|
||||||
app *app.App
|
app *app.App
|
||||||
editor layout.Container
|
editor layout.Container
|
||||||
messages layout.Container
|
messages layout.Container
|
||||||
layout layout.SplitPaneLayout
|
layout layout.FlexLayout
|
||||||
completionDialog dialog.CompletionDialog
|
completionDialog dialog.CompletionDialog
|
||||||
showCompletionDialog bool
|
showCompletionDialog bool
|
||||||
}
|
}
|
||||||
|
@ -96,12 +96,6 @@ func (p *chatPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
if cmd != nil {
|
if cmd != nil {
|
||||||
return p, cmd
|
return p, cmd
|
||||||
}
|
}
|
||||||
case state.SessionSelectedMsg:
|
|
||||||
cmd := p.setSidebar()
|
|
||||||
cmds = append(cmds, cmd)
|
|
||||||
case state.SessionClearedMsg:
|
|
||||||
cmd := p.setSidebar()
|
|
||||||
cmds = append(cmds, cmd)
|
|
||||||
|
|
||||||
case dialog.CompletionDialogCloseMsg:
|
case dialog.CompletionDialogCloseMsg:
|
||||||
p.showCompletionDialog = false
|
p.showCompletionDialog = false
|
||||||
|
@ -116,7 +110,6 @@ func (p *chatPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
p.app.Session = &client.SessionInfo{}
|
p.app.Session = &client.SessionInfo{}
|
||||||
p.app.Messages = []client.MessageInfo{}
|
p.app.Messages = []client.MessageInfo{}
|
||||||
return p, tea.Batch(
|
return p, tea.Batch(
|
||||||
p.clearSidebar(),
|
|
||||||
util.CmdHandler(state.SessionClearedMsg{}),
|
util.CmdHandler(state.SessionClearedMsg{}),
|
||||||
)
|
)
|
||||||
case key.Matches(msg, keyMap.Cancel):
|
case key.Matches(msg, keyMap.Cancel):
|
||||||
|
@ -145,30 +138,14 @@ func (p *chatPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
|
|
||||||
u, cmd := p.layout.Update(msg)
|
u, cmd := p.layout.Update(msg)
|
||||||
cmds = append(cmds, cmd)
|
cmds = append(cmds, cmd)
|
||||||
p.layout = u.(layout.SplitPaneLayout)
|
p.layout = u.(layout.FlexLayout)
|
||||||
return p, tea.Batch(cmds...)
|
return p, tea.Batch(cmds...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *chatPage) setSidebar() tea.Cmd {
|
|
||||||
sidebarContainer := layout.NewContainer(
|
|
||||||
chat.NewSidebarCmp(p.app),
|
|
||||||
layout.WithPadding(1, 1, 1, 1),
|
|
||||||
)
|
|
||||||
return tea.Batch(p.layout.SetRightPanel(sidebarContainer), sidebarContainer.Init())
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *chatPage) clearSidebar() tea.Cmd {
|
|
||||||
return p.layout.ClearRightPanel()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *chatPage) sendMessage(text string, attachments []app.Attachment) tea.Cmd {
|
func (p *chatPage) sendMessage(text string, attachments []app.Attachment) tea.Cmd {
|
||||||
var cmds []tea.Cmd
|
var cmds []tea.Cmd
|
||||||
cmd := p.app.SendChatMessage(context.Background(), text, attachments)
|
cmd := p.app.SendChatMessage(context.Background(), text, attachments)
|
||||||
cmds = append(cmds, cmd)
|
cmds = append(cmds, cmd)
|
||||||
cmd = p.setSidebar()
|
|
||||||
if cmd != nil {
|
|
||||||
cmds = append(cmds, cmd)
|
|
||||||
}
|
|
||||||
return tea.Batch(cmds...)
|
return tea.Batch(cmds...)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -183,6 +160,7 @@ func (p *chatPage) GetSize() (int, int) {
|
||||||
func (p *chatPage) View() string {
|
func (p *chatPage) View() string {
|
||||||
layoutView := p.layout.View()
|
layoutView := p.layout.View()
|
||||||
|
|
||||||
|
// TODO: Fix this with our new layout
|
||||||
if p.showCompletionDialog {
|
if p.showCompletionDialog {
|
||||||
_, layoutHeight := p.layout.GetSize()
|
_, layoutHeight := p.layout.GetSize()
|
||||||
editorWidth, editorHeight := p.editor.GetSize()
|
editorWidth, editorHeight := p.editor.GetSize()
|
||||||
|
@ -213,21 +191,25 @@ func NewChatPage(app *app.App) tea.Model {
|
||||||
cg := completions.NewFileAndFolderContextGroup()
|
cg := completions.NewFileAndFolderContextGroup()
|
||||||
completionDialog := dialog.NewCompletionDialogCmp(cg)
|
completionDialog := dialog.NewCompletionDialogCmp(cg)
|
||||||
messagesContainer := layout.NewContainer(
|
messagesContainer := layout.NewContainer(
|
||||||
chat.NewMessagesCmp(app),
|
chat.NewMessagesComponent(app),
|
||||||
layout.WithPadding(1, 1, 0, 1),
|
|
||||||
)
|
)
|
||||||
editorContainer := layout.NewContainer(
|
editorContainer := layout.NewContainer(
|
||||||
chat.NewEditorCmp(app),
|
chat.NewEditorComponent(app),
|
||||||
layout.WithBorder(true, false, false, false),
|
layout.WithMaxWidth(layout.Current.Container.Width),
|
||||||
|
layout.WithAlignCenter(),
|
||||||
)
|
)
|
||||||
return &chatPage{
|
return &chatPage{
|
||||||
app: app,
|
app: app,
|
||||||
editor: editorContainer,
|
editor: editorContainer,
|
||||||
messages: messagesContainer,
|
messages: messagesContainer,
|
||||||
completionDialog: completionDialog,
|
completionDialog: completionDialog,
|
||||||
layout: layout.NewSplitPane(
|
layout: layout.NewFlexLayout(
|
||||||
layout.WithLeftPanel(messagesContainer),
|
layout.WithPanes(messagesContainer, editorContainer),
|
||||||
layout.WithBottomPanel(editorContainer),
|
layout.WithDirection(layout.FlexDirectionVertical),
|
||||||
|
layout.WithPaneSizes(
|
||||||
|
layout.FlexPaneSizeGrow,
|
||||||
|
layout.FlexPaneSizeFixed(6),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,23 +13,33 @@ func BaseStyle() lipgloss.Style {
|
||||||
Foreground(t.Text())
|
Foreground(t.Text())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Panel() lipgloss.Style {
|
||||||
|
t := theme.CurrentTheme()
|
||||||
|
return lipgloss.NewStyle().
|
||||||
|
Background(t.BackgroundSubtle()).
|
||||||
|
Border(lipgloss.NormalBorder(), true, false, true, false).
|
||||||
|
BorderForeground(t.BorderSubtle()).
|
||||||
|
Foreground(t.Text())
|
||||||
|
}
|
||||||
|
|
||||||
// Regular returns a basic unstyled lipgloss.Style
|
// Regular returns a basic unstyled lipgloss.Style
|
||||||
func Regular() lipgloss.Style {
|
func Regular() lipgloss.Style {
|
||||||
return lipgloss.NewStyle()
|
return lipgloss.NewStyle()
|
||||||
}
|
}
|
||||||
|
|
||||||
func Muted() lipgloss.Style {
|
func Muted() lipgloss.Style {
|
||||||
return lipgloss.NewStyle().Foreground(theme.CurrentTheme().TextMuted())
|
t := theme.CurrentTheme()
|
||||||
|
return lipgloss.NewStyle().Background(t.Background()).Foreground(t.TextMuted())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Bold returns a bold style
|
// Bold returns a bold style
|
||||||
func Bold() lipgloss.Style {
|
func Bold() lipgloss.Style {
|
||||||
return Regular().Bold(true)
|
return BaseStyle().Bold(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Padded returns a style with horizontal padding
|
// Padded returns a style with horizontal padding
|
||||||
func Padded() lipgloss.Style {
|
func Padded() lipgloss.Style {
|
||||||
return Regular().Padding(0, 1)
|
return BaseStyle().Padding(0, 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Border returns a style with a normal border
|
// Border returns a style with a normal border
|
||||||
|
@ -37,7 +47,7 @@ func Border() lipgloss.Style {
|
||||||
t := theme.CurrentTheme()
|
t := theme.CurrentTheme()
|
||||||
return Regular().
|
return Regular().
|
||||||
Border(lipgloss.NormalBorder()).
|
Border(lipgloss.NormalBorder()).
|
||||||
BorderForeground(t.BorderNormal())
|
BorderForeground(t.Border())
|
||||||
}
|
}
|
||||||
|
|
||||||
// ThickBorder returns a style with a thick border
|
// ThickBorder returns a style with a thick border
|
||||||
|
@ -45,7 +55,7 @@ func ThickBorder() lipgloss.Style {
|
||||||
t := theme.CurrentTheme()
|
t := theme.CurrentTheme()
|
||||||
return Regular().
|
return Regular().
|
||||||
Border(lipgloss.ThickBorder()).
|
Border(lipgloss.ThickBorder()).
|
||||||
BorderForeground(t.BorderNormal())
|
BorderForeground(t.Border())
|
||||||
}
|
}
|
||||||
|
|
||||||
// DoubleBorder returns a style with a double border
|
// DoubleBorder returns a style with a double border
|
||||||
|
@ -53,7 +63,7 @@ func DoubleBorder() lipgloss.Style {
|
||||||
t := theme.CurrentTheme()
|
t := theme.CurrentTheme()
|
||||||
return Regular().
|
return Regular().
|
||||||
Border(lipgloss.DoubleBorder()).
|
Border(lipgloss.DoubleBorder()).
|
||||||
BorderForeground(t.BorderNormal())
|
BorderForeground(t.Border())
|
||||||
}
|
}
|
||||||
|
|
||||||
// FocusedBorder returns a style with a border using the focused border color
|
// FocusedBorder returns a style with a border using the focused border color
|
||||||
|
@ -61,7 +71,7 @@ func FocusedBorder() lipgloss.Style {
|
||||||
t := theme.CurrentTheme()
|
t := theme.CurrentTheme()
|
||||||
return Regular().
|
return Regular().
|
||||||
Border(lipgloss.NormalBorder()).
|
Border(lipgloss.NormalBorder()).
|
||||||
BorderForeground(t.BorderFocused())
|
BorderForeground(t.BorderActive())
|
||||||
}
|
}
|
||||||
|
|
||||||
// DimBorder returns a style with a border using the dim border color
|
// DimBorder returns a style with a border using the dim border color
|
||||||
|
@ -69,7 +79,7 @@ func DimBorder() lipgloss.Style {
|
||||||
t := theme.CurrentTheme()
|
t := theme.CurrentTheme()
|
||||||
return Regular().
|
return Regular().
|
||||||
Border(lipgloss.NormalBorder()).
|
Border(lipgloss.NormalBorder()).
|
||||||
BorderForeground(t.BorderDim())
|
BorderForeground(t.BorderSubtle())
|
||||||
}
|
}
|
||||||
|
|
||||||
// PrimaryColor returns the primary color from the current theme
|
// PrimaryColor returns the primary color from the current theme
|
||||||
|
@ -117,37 +127,32 @@ func TextMutedColor() lipgloss.AdaptiveColor {
|
||||||
return theme.CurrentTheme().TextMuted()
|
return theme.CurrentTheme().TextMuted()
|
||||||
}
|
}
|
||||||
|
|
||||||
// TextEmphasizedColor returns the emphasized text color from the current theme
|
|
||||||
func TextEmphasizedColor() lipgloss.AdaptiveColor {
|
|
||||||
return theme.CurrentTheme().TextEmphasized()
|
|
||||||
}
|
|
||||||
|
|
||||||
// BackgroundColor returns the background color from the current theme
|
// BackgroundColor returns the background color from the current theme
|
||||||
func BackgroundColor() lipgloss.AdaptiveColor {
|
func BackgroundColor() lipgloss.AdaptiveColor {
|
||||||
return theme.CurrentTheme().Background()
|
return theme.CurrentTheme().Background()
|
||||||
}
|
}
|
||||||
|
|
||||||
// BackgroundSecondaryColor returns the secondary background color from the current theme
|
// BackgroundSubtleColor returns the subtle background color from the current theme
|
||||||
func BackgroundSecondaryColor() lipgloss.AdaptiveColor {
|
func BackgroundSubtleColor() lipgloss.AdaptiveColor {
|
||||||
return theme.CurrentTheme().BackgroundSecondary()
|
return theme.CurrentTheme().BackgroundSubtle()
|
||||||
}
|
}
|
||||||
|
|
||||||
// BackgroundDarkerColor returns the darker background color from the current theme
|
// BackgroundElementColor returns the darker background color from the current theme
|
||||||
func BackgroundDarkerColor() lipgloss.AdaptiveColor {
|
func BackgroundElementColor() lipgloss.AdaptiveColor {
|
||||||
return theme.CurrentTheme().BackgroundDarker()
|
return theme.CurrentTheme().BackgroundElement()
|
||||||
}
|
}
|
||||||
|
|
||||||
// BorderNormalColor returns the normal border color from the current theme
|
// BorderColor returns the border color from the current theme
|
||||||
func BorderNormalColor() lipgloss.AdaptiveColor {
|
func BorderColor() lipgloss.AdaptiveColor {
|
||||||
return theme.CurrentTheme().BorderNormal()
|
return theme.CurrentTheme().Border()
|
||||||
}
|
}
|
||||||
|
|
||||||
// BorderFocusedColor returns the focused border color from the current theme
|
// BorderActiveColor returns the active border color from the current theme
|
||||||
func BorderFocusedColor() lipgloss.AdaptiveColor {
|
func BorderActiveColor() lipgloss.AdaptiveColor {
|
||||||
return theme.CurrentTheme().BorderFocused()
|
return theme.CurrentTheme().BorderActive()
|
||||||
}
|
}
|
||||||
|
|
||||||
// BorderDimColor returns the dim border color from the current theme
|
// BorderSubtleColor returns the subtle border color from the current theme
|
||||||
func BorderDimColor() lipgloss.AdaptiveColor {
|
func BorderSubtleColor() lipgloss.AdaptiveColor {
|
||||||
return theme.CurrentTheme().BorderDim()
|
return theme.CurrentTheme().BorderSubtle()
|
||||||
}
|
}
|
||||||
|
|
|
@ -92,35 +92,31 @@ func NewAyuDarkTheme() *AyuDarkTheme {
|
||||||
Dark: darkComment,
|
Dark: darkComment,
|
||||||
Light: lightComment,
|
Light: lightComment,
|
||||||
}
|
}
|
||||||
theme.TextEmphasizedColor = lipgloss.AdaptiveColor{
|
|
||||||
Dark: darkPurple,
|
|
||||||
Light: lightPurple,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Background colors
|
// Background colors
|
||||||
theme.BackgroundColor = lipgloss.AdaptiveColor{
|
theme.BackgroundColor = lipgloss.AdaptiveColor{
|
||||||
Dark: darkBackground,
|
Dark: darkBackground,
|
||||||
Light: lightBackground,
|
Light: lightBackground,
|
||||||
}
|
}
|
||||||
theme.BackgroundSecondaryColor = lipgloss.AdaptiveColor{
|
theme.BackgroundSubtleColor = lipgloss.AdaptiveColor{
|
||||||
Dark: darkCurrentLine,
|
Dark: darkCurrentLine,
|
||||||
Light: lightCurrentLine,
|
Light: lightCurrentLine,
|
||||||
}
|
}
|
||||||
theme.BackgroundDarkerColor = lipgloss.AdaptiveColor{
|
theme.BackgroundElementColor = lipgloss.AdaptiveColor{
|
||||||
Dark: "#0b0e14", // Darker than background
|
Dark: "#0b0e14", // Darker than background
|
||||||
Light: "#ffffff", // Lighter than background
|
Light: "#ffffff", // Lighter than background
|
||||||
}
|
}
|
||||||
|
|
||||||
// Border colors
|
// Border colors
|
||||||
theme.BorderNormalColor = lipgloss.AdaptiveColor{
|
theme.BorderColor = lipgloss.AdaptiveColor{
|
||||||
Dark: darkBorder,
|
Dark: darkBorder,
|
||||||
Light: lightBorder,
|
Light: lightBorder,
|
||||||
}
|
}
|
||||||
theme.BorderFocusedColor = lipgloss.AdaptiveColor{
|
theme.BorderActiveColor = lipgloss.AdaptiveColor{
|
||||||
Dark: darkBlue,
|
Dark: darkBlue,
|
||||||
Light: lightBlue,
|
Light: lightBlue,
|
||||||
}
|
}
|
||||||
theme.BorderDimColor = lipgloss.AdaptiveColor{
|
theme.BorderSubtleColor = lipgloss.AdaptiveColor{
|
||||||
Dark: darkSelection,
|
Dark: darkSelection,
|
||||||
Light: lightSelection,
|
Light: lightSelection,
|
||||||
}
|
}
|
||||||
|
|
|
@ -60,35 +60,31 @@ func NewCatppuccinTheme() *CatppuccinTheme {
|
||||||
Dark: mocha.Subtext0().Hex,
|
Dark: mocha.Subtext0().Hex,
|
||||||
Light: latte.Subtext0().Hex,
|
Light: latte.Subtext0().Hex,
|
||||||
}
|
}
|
||||||
theme.TextEmphasizedColor = lipgloss.AdaptiveColor{
|
|
||||||
Dark: mocha.Lavender().Hex,
|
|
||||||
Light: latte.Lavender().Hex,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Background colors
|
// Background colors
|
||||||
theme.BackgroundColor = lipgloss.AdaptiveColor{
|
theme.BackgroundColor = lipgloss.AdaptiveColor{
|
||||||
Dark: "#212121", // From existing styles
|
Dark: "#212121", // From existing styles
|
||||||
Light: "#EEEEEE", // Light equivalent
|
Light: "#EEEEEE", // Light equivalent
|
||||||
}
|
}
|
||||||
theme.BackgroundSecondaryColor = lipgloss.AdaptiveColor{
|
theme.BackgroundSubtleColor = lipgloss.AdaptiveColor{
|
||||||
Dark: "#2c2c2c", // From existing styles
|
Dark: "#2c2c2c", // From existing styles
|
||||||
Light: "#E0E0E0", // Light equivalent
|
Light: "#E0E0E0", // Light equivalent
|
||||||
}
|
}
|
||||||
theme.BackgroundDarkerColor = lipgloss.AdaptiveColor{
|
theme.BackgroundElementColor = lipgloss.AdaptiveColor{
|
||||||
Dark: "#181818", // From existing styles
|
Dark: "#181818", // From existing styles
|
||||||
Light: "#F5F5F5", // Light equivalent
|
Light: "#F5F5F5", // Light equivalent
|
||||||
}
|
}
|
||||||
|
|
||||||
// Border colors
|
// Border colors
|
||||||
theme.BorderNormalColor = lipgloss.AdaptiveColor{
|
theme.BorderColor = lipgloss.AdaptiveColor{
|
||||||
Dark: "#4b4c5c", // From existing styles
|
Dark: "#4b4c5c", // From existing styles
|
||||||
Light: "#BDBDBD", // Light equivalent
|
Light: "#BDBDBD", // Light equivalent
|
||||||
}
|
}
|
||||||
theme.BorderFocusedColor = lipgloss.AdaptiveColor{
|
theme.BorderActiveColor = lipgloss.AdaptiveColor{
|
||||||
Dark: mocha.Blue().Hex,
|
Dark: mocha.Blue().Hex,
|
||||||
Light: latte.Blue().Hex,
|
Light: latte.Blue().Hex,
|
||||||
}
|
}
|
||||||
theme.BorderDimColor = lipgloss.AdaptiveColor{
|
theme.BorderSubtleColor = lipgloss.AdaptiveColor{
|
||||||
Dark: mocha.Surface0().Hex,
|
Dark: mocha.Surface0().Hex,
|
||||||
Light: latte.Surface0().Hex,
|
Light: latte.Surface0().Hex,
|
||||||
}
|
}
|
||||||
|
|
|
@ -86,35 +86,31 @@ func NewDraculaTheme() *DraculaTheme {
|
||||||
Dark: darkComment,
|
Dark: darkComment,
|
||||||
Light: lightComment,
|
Light: lightComment,
|
||||||
}
|
}
|
||||||
theme.TextEmphasizedColor = lipgloss.AdaptiveColor{
|
|
||||||
Dark: darkYellow,
|
|
||||||
Light: lightYellow,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Background colors
|
// Background colors
|
||||||
theme.BackgroundColor = lipgloss.AdaptiveColor{
|
theme.BackgroundElementColor = lipgloss.AdaptiveColor{
|
||||||
Dark: darkBackground,
|
Dark: darkBackground,
|
||||||
Light: lightBackground,
|
Light: lightBackground,
|
||||||
}
|
}
|
||||||
theme.BackgroundSecondaryColor = lipgloss.AdaptiveColor{
|
theme.BackgroundSubtleColor = lipgloss.AdaptiveColor{
|
||||||
Dark: darkCurrentLine,
|
Dark: darkCurrentLine,
|
||||||
Light: lightCurrentLine,
|
Light: lightCurrentLine,
|
||||||
}
|
}
|
||||||
theme.BackgroundDarkerColor = lipgloss.AdaptiveColor{
|
theme.BackgroundColor = lipgloss.AdaptiveColor{
|
||||||
Dark: "#21222c", // Slightly darker than background
|
Dark: "#21222c", // Slightly darker than background
|
||||||
Light: "#ffffff", // Slightly lighter than background
|
Light: "#ffffff", // Slightly lighter than background
|
||||||
}
|
}
|
||||||
|
|
||||||
// Border colors
|
// Border colors
|
||||||
theme.BorderNormalColor = lipgloss.AdaptiveColor{
|
theme.BorderColor = lipgloss.AdaptiveColor{
|
||||||
Dark: darkBorder,
|
Dark: darkBorder,
|
||||||
Light: lightBorder,
|
Light: lightBorder,
|
||||||
}
|
}
|
||||||
theme.BorderFocusedColor = lipgloss.AdaptiveColor{
|
theme.BorderActiveColor = lipgloss.AdaptiveColor{
|
||||||
Dark: darkPurple,
|
Dark: darkPurple,
|
||||||
Light: lightPurple,
|
Light: lightPurple,
|
||||||
}
|
}
|
||||||
theme.BorderDimColor = lipgloss.AdaptiveColor{
|
theme.BorderSubtleColor = lipgloss.AdaptiveColor{
|
||||||
Dark: darkSelection,
|
Dark: darkSelection,
|
||||||
Light: lightSelection,
|
Light: lightSelection,
|
||||||
}
|
}
|
||||||
|
@ -133,8 +129,8 @@ func NewDraculaTheme() *DraculaTheme {
|
||||||
Light: lightComment,
|
Light: lightComment,
|
||||||
}
|
}
|
||||||
theme.DiffHunkHeaderColor = lipgloss.AdaptiveColor{
|
theme.DiffHunkHeaderColor = lipgloss.AdaptiveColor{
|
||||||
Dark: darkPurple,
|
Dark: darkCurrentLine,
|
||||||
Light: lightPurple,
|
Light: lightCurrentLine,
|
||||||
}
|
}
|
||||||
theme.DiffHighlightAddedColor = lipgloss.AdaptiveColor{
|
theme.DiffHighlightAddedColor = lipgloss.AdaptiveColor{
|
||||||
Dark: "#50fa7b",
|
Dark: "#50fa7b",
|
||||||
|
|
|
@ -94,35 +94,31 @@ func NewFlexokiTheme() *FlexokiTheme {
|
||||||
Dark: flexokiBase700,
|
Dark: flexokiBase700,
|
||||||
Light: flexokiBase500,
|
Light: flexokiBase500,
|
||||||
}
|
}
|
||||||
theme.TextEmphasizedColor = lipgloss.AdaptiveColor{
|
|
||||||
Dark: flexokiYellow400,
|
|
||||||
Light: flexokiYellow600,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Background colors
|
// Background colors
|
||||||
theme.BackgroundColor = lipgloss.AdaptiveColor{
|
theme.BackgroundColor = lipgloss.AdaptiveColor{
|
||||||
Dark: flexokiBlack,
|
Dark: flexokiBlack,
|
||||||
Light: flexokiPaper,
|
Light: flexokiPaper,
|
||||||
}
|
}
|
||||||
theme.BackgroundSecondaryColor = lipgloss.AdaptiveColor{
|
theme.BackgroundSubtleColor = lipgloss.AdaptiveColor{
|
||||||
Dark: flexokiBase950,
|
Dark: flexokiBase950,
|
||||||
Light: flexokiBase50,
|
Light: flexokiBase50,
|
||||||
}
|
}
|
||||||
theme.BackgroundDarkerColor = lipgloss.AdaptiveColor{
|
theme.BackgroundElementColor = lipgloss.AdaptiveColor{
|
||||||
Dark: flexokiBase900,
|
Dark: flexokiBase900,
|
||||||
Light: flexokiBase100,
|
Light: flexokiBase100,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Border colors
|
// Border colors
|
||||||
theme.BorderNormalColor = lipgloss.AdaptiveColor{
|
theme.BorderColor = lipgloss.AdaptiveColor{
|
||||||
Dark: flexokiBase900,
|
Dark: flexokiBase900,
|
||||||
Light: flexokiBase100,
|
Light: flexokiBase100,
|
||||||
}
|
}
|
||||||
theme.BorderFocusedColor = lipgloss.AdaptiveColor{
|
theme.BorderActiveColor = lipgloss.AdaptiveColor{
|
||||||
Dark: flexokiBlue400,
|
Dark: flexokiBlue400,
|
||||||
Light: flexokiBlue600,
|
Light: flexokiBlue600,
|
||||||
}
|
}
|
||||||
theme.BorderDimColor = lipgloss.AdaptiveColor{
|
theme.BorderSubtleColor = lipgloss.AdaptiveColor{
|
||||||
Dark: flexokiBase850,
|
Dark: flexokiBase850,
|
||||||
Light: flexokiBase150,
|
Light: flexokiBase150,
|
||||||
}
|
}
|
||||||
|
|
|
@ -114,35 +114,31 @@ func NewGruvboxTheme() *GruvboxTheme {
|
||||||
Dark: gruvboxDarkFg4,
|
Dark: gruvboxDarkFg4,
|
||||||
Light: gruvboxLightFg4,
|
Light: gruvboxLightFg4,
|
||||||
}
|
}
|
||||||
theme.TextEmphasizedColor = lipgloss.AdaptiveColor{
|
|
||||||
Dark: gruvboxDarkYellowBright,
|
|
||||||
Light: gruvboxLightYellowBright,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Background colors
|
// Background colors
|
||||||
theme.BackgroundColor = lipgloss.AdaptiveColor{
|
theme.BackgroundColor = lipgloss.AdaptiveColor{
|
||||||
Dark: gruvboxDarkBg0,
|
Dark: gruvboxDarkBg0,
|
||||||
Light: gruvboxLightBg0,
|
Light: gruvboxLightBg0,
|
||||||
}
|
}
|
||||||
theme.BackgroundSecondaryColor = lipgloss.AdaptiveColor{
|
theme.BackgroundSubtleColor = lipgloss.AdaptiveColor{
|
||||||
Dark: gruvboxDarkBg1,
|
Dark: gruvboxDarkBg1,
|
||||||
Light: gruvboxLightBg1,
|
Light: gruvboxLightBg1,
|
||||||
}
|
}
|
||||||
theme.BackgroundDarkerColor = lipgloss.AdaptiveColor{
|
theme.BackgroundElementColor = lipgloss.AdaptiveColor{
|
||||||
Dark: gruvboxDarkBg0Soft,
|
Dark: gruvboxDarkBg0Soft,
|
||||||
Light: gruvboxLightBg0Soft,
|
Light: gruvboxLightBg0Soft,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Border colors
|
// Border colors
|
||||||
theme.BorderNormalColor = lipgloss.AdaptiveColor{
|
theme.BorderColor = lipgloss.AdaptiveColor{
|
||||||
Dark: gruvboxDarkBg2,
|
Dark: gruvboxDarkBg2,
|
||||||
Light: gruvboxLightBg2,
|
Light: gruvboxLightBg2,
|
||||||
}
|
}
|
||||||
theme.BorderFocusedColor = lipgloss.AdaptiveColor{
|
theme.BorderActiveColor = lipgloss.AdaptiveColor{
|
||||||
Dark: gruvboxDarkBlueBright,
|
Dark: gruvboxDarkBlueBright,
|
||||||
Light: gruvboxLightBlueBright,
|
Light: gruvboxLightBlueBright,
|
||||||
}
|
}
|
||||||
theme.BorderDimColor = lipgloss.AdaptiveColor{
|
theme.BorderSubtleColor = lipgloss.AdaptiveColor{
|
||||||
Dark: gruvboxDarkBg1,
|
Dark: gruvboxDarkBg1,
|
||||||
Light: gruvboxLightBg1,
|
Light: gruvboxLightBg1,
|
||||||
}
|
}
|
||||||
|
|
|
@ -157,20 +157,18 @@ func LoadCustomTheme(customTheme map[string]any) (Theme, error) {
|
||||||
theme.TextColor = adaptiveColor
|
theme.TextColor = adaptiveColor
|
||||||
case "textmuted":
|
case "textmuted":
|
||||||
theme.TextMutedColor = adaptiveColor
|
theme.TextMutedColor = adaptiveColor
|
||||||
case "textemphasized":
|
|
||||||
theme.TextEmphasizedColor = adaptiveColor
|
|
||||||
case "background":
|
case "background":
|
||||||
theme.BackgroundColor = adaptiveColor
|
theme.BackgroundColor = adaptiveColor
|
||||||
case "backgroundsecondary":
|
case "backgroundsubtle":
|
||||||
theme.BackgroundSecondaryColor = adaptiveColor
|
theme.BackgroundSubtleColor = adaptiveColor
|
||||||
case "backgrounddarker":
|
case "backgroundelement":
|
||||||
theme.BackgroundDarkerColor = adaptiveColor
|
theme.BackgroundElementColor = adaptiveColor
|
||||||
case "bordernormal":
|
case "border":
|
||||||
theme.BorderNormalColor = adaptiveColor
|
theme.BorderColor = adaptiveColor
|
||||||
case "borderfocused":
|
case "borderactive":
|
||||||
theme.BorderFocusedColor = adaptiveColor
|
theme.BorderActiveColor = adaptiveColor
|
||||||
case "borderdim":
|
case "bordersubtle":
|
||||||
theme.BorderDimColor = adaptiveColor
|
theme.BorderSubtleColor = adaptiveColor
|
||||||
case "diffadded":
|
case "diffadded":
|
||||||
theme.DiffAddedColor = adaptiveColor
|
theme.DiffAddedColor = adaptiveColor
|
||||||
case "diffremoved":
|
case "diffremoved":
|
||||||
|
|
|
@ -85,35 +85,31 @@ func NewMonokaiProTheme() *MonokaiProTheme {
|
||||||
Dark: darkComment,
|
Dark: darkComment,
|
||||||
Light: lightComment,
|
Light: lightComment,
|
||||||
}
|
}
|
||||||
theme.TextEmphasizedColor = lipgloss.AdaptiveColor{
|
|
||||||
Dark: darkYellow,
|
|
||||||
Light: lightYellow,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Background colors
|
// Background colors
|
||||||
theme.BackgroundColor = lipgloss.AdaptiveColor{
|
theme.BackgroundColor = lipgloss.AdaptiveColor{
|
||||||
Dark: darkBackground,
|
Dark: darkBackground,
|
||||||
Light: lightBackground,
|
Light: lightBackground,
|
||||||
}
|
}
|
||||||
theme.BackgroundSecondaryColor = lipgloss.AdaptiveColor{
|
theme.BackgroundSubtleColor = lipgloss.AdaptiveColor{
|
||||||
Dark: darkCurrentLine,
|
Dark: darkCurrentLine,
|
||||||
Light: lightCurrentLine,
|
Light: lightCurrentLine,
|
||||||
}
|
}
|
||||||
theme.BackgroundDarkerColor = lipgloss.AdaptiveColor{
|
theme.BackgroundElementColor = lipgloss.AdaptiveColor{
|
||||||
Dark: "#221f22", // Slightly darker than background
|
Dark: "#221f22", // Slightly darker than background
|
||||||
Light: "#ffffff", // Slightly lighter than background
|
Light: "#ffffff", // Slightly lighter than background
|
||||||
}
|
}
|
||||||
|
|
||||||
// Border colors
|
// Border colors
|
||||||
theme.BorderNormalColor = lipgloss.AdaptiveColor{
|
theme.BorderColor = lipgloss.AdaptiveColor{
|
||||||
Dark: darkBorder,
|
Dark: darkBorder,
|
||||||
Light: lightBorder,
|
Light: lightBorder,
|
||||||
}
|
}
|
||||||
theme.BorderFocusedColor = lipgloss.AdaptiveColor{
|
theme.BorderActiveColor = lipgloss.AdaptiveColor{
|
||||||
Dark: darkCyan,
|
Dark: darkCyan,
|
||||||
Light: lightCyan,
|
Light: lightCyan,
|
||||||
}
|
}
|
||||||
theme.BorderDimColor = lipgloss.AdaptiveColor{
|
theme.BorderSubtleColor = lipgloss.AdaptiveColor{
|
||||||
Dark: darkSelection,
|
Dark: darkSelection,
|
||||||
Light: lightSelection,
|
Light: lightSelection,
|
||||||
}
|
}
|
||||||
|
|
|
@ -86,35 +86,31 @@ func NewOneDarkTheme() *OneDarkTheme {
|
||||||
Dark: darkComment,
|
Dark: darkComment,
|
||||||
Light: lightComment,
|
Light: lightComment,
|
||||||
}
|
}
|
||||||
theme.TextEmphasizedColor = lipgloss.AdaptiveColor{
|
|
||||||
Dark: darkYellow,
|
|
||||||
Light: lightYellow,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Background colors
|
// Background colors
|
||||||
theme.BackgroundColor = lipgloss.AdaptiveColor{
|
theme.BackgroundColor = lipgloss.AdaptiveColor{
|
||||||
Dark: darkBackground,
|
Dark: darkBackground,
|
||||||
Light: lightBackground,
|
Light: lightBackground,
|
||||||
}
|
}
|
||||||
theme.BackgroundSecondaryColor = lipgloss.AdaptiveColor{
|
theme.BackgroundSubtleColor = lipgloss.AdaptiveColor{
|
||||||
Dark: darkCurrentLine,
|
Dark: darkCurrentLine,
|
||||||
Light: lightCurrentLine,
|
Light: lightCurrentLine,
|
||||||
}
|
}
|
||||||
theme.BackgroundDarkerColor = lipgloss.AdaptiveColor{
|
theme.BackgroundElementColor = lipgloss.AdaptiveColor{
|
||||||
Dark: "#21252b", // Slightly darker than background
|
Dark: "#21252b", // Slightly darker than background
|
||||||
Light: "#ffffff", // Slightly lighter than background
|
Light: "#ffffff", // Slightly lighter than background
|
||||||
}
|
}
|
||||||
|
|
||||||
// Border colors
|
// Border colors
|
||||||
theme.BorderNormalColor = lipgloss.AdaptiveColor{
|
theme.BorderColor = lipgloss.AdaptiveColor{
|
||||||
Dark: darkBorder,
|
Dark: darkBorder,
|
||||||
Light: lightBorder,
|
Light: lightBorder,
|
||||||
}
|
}
|
||||||
theme.BorderFocusedColor = lipgloss.AdaptiveColor{
|
theme.BorderActiveColor = lipgloss.AdaptiveColor{
|
||||||
Dark: darkBlue,
|
Dark: darkBlue,
|
||||||
Light: lightBlue,
|
Light: lightBlue,
|
||||||
}
|
}
|
||||||
theme.BorderDimColor = lipgloss.AdaptiveColor{
|
theme.BorderSubtleColor = lipgloss.AdaptiveColor{
|
||||||
Dark: darkSelection,
|
Dark: darkSelection,
|
||||||
Light: lightSelection,
|
Light: lightSelection,
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,14 +12,23 @@ type OpenCodeTheme struct {
|
||||||
|
|
||||||
// NewOpenCodeTheme creates a new instance of the OpenCode theme.
|
// NewOpenCodeTheme creates a new instance of the OpenCode theme.
|
||||||
func NewOpenCodeTheme() *OpenCodeTheme {
|
func NewOpenCodeTheme() *OpenCodeTheme {
|
||||||
// OpenCode color palette
|
// OpenCode color palette with Radix-inspired scale progression
|
||||||
// Dark mode colors
|
// Dark mode colors - using a neutral gray scale as base
|
||||||
darkBackground := "#212121"
|
darkStep1 := "#0a0a0a" // App background
|
||||||
darkCurrentLine := "#252525"
|
darkStep2 := "#141414" // Subtle background
|
||||||
darkSelection := "#303030"
|
darkStep3 := "#1e1e1e" // UI element background
|
||||||
darkForeground := "#e0e0e0"
|
darkStep4 := "#282828" // Hovered UI element background
|
||||||
darkComment := "#6a6a6a"
|
darkStep5 := "#323232" // Active/Selected UI element background
|
||||||
darkPrimary := "#fab283" // Primary orange/gold
|
darkStep6 := "#3c3c3c" // Subtle borders and separators
|
||||||
|
darkStep7 := "#484848" // UI element border and focus rings
|
||||||
|
darkStep8 := "#606060" // Hovered UI element border
|
||||||
|
darkStep9 := "#fab283" // Solid backgrounds (primary orange/gold)
|
||||||
|
darkStep10 := "#ffc09f" // Hovered solid backgrounds
|
||||||
|
darkStep11 := "#808080" // Low-contrast text (more muted)
|
||||||
|
darkStep12 := "#eeeeee" // High-contrast text
|
||||||
|
|
||||||
|
// Dark mode accent colors
|
||||||
|
darkPrimary := darkStep9 // Primary uses step 9 (solid background)
|
||||||
darkSecondary := "#5c9cf5" // Secondary blue
|
darkSecondary := "#5c9cf5" // Secondary blue
|
||||||
darkAccent := "#9d7cd8" // Accent purple
|
darkAccent := "#9d7cd8" // Accent purple
|
||||||
darkRed := "#e06c75" // Error red
|
darkRed := "#e06c75" // Error red
|
||||||
|
@ -27,15 +36,23 @@ func NewOpenCodeTheme() *OpenCodeTheme {
|
||||||
darkGreen := "#7fd88f" // Success green
|
darkGreen := "#7fd88f" // Success green
|
||||||
darkCyan := "#56b6c2" // Info cyan
|
darkCyan := "#56b6c2" // Info cyan
|
||||||
darkYellow := "#e5c07b" // Emphasized text
|
darkYellow := "#e5c07b" // Emphasized text
|
||||||
darkBorder := "#4b4c5c" // Border color
|
|
||||||
|
|
||||||
// Light mode colors
|
// Light mode colors - using a neutral gray scale as base
|
||||||
lightBackground := "#f8f8f8"
|
lightStep1 := "#ffffff" // App background
|
||||||
lightCurrentLine := "#f0f0f0"
|
lightStep2 := "#fafafa" // Subtle background
|
||||||
lightSelection := "#e5e5e6"
|
lightStep3 := "#f5f5f5" // UI element background
|
||||||
lightForeground := "#2a2a2a"
|
lightStep4 := "#ebebeb" // Hovered UI element background
|
||||||
lightComment := "#8a8a8a"
|
lightStep5 := "#e1e1e1" // Active/Selected UI element background
|
||||||
lightPrimary := "#3b7dd8" // Primary blue
|
lightStep6 := "#d4d4d4" // Subtle borders and separators
|
||||||
|
lightStep7 := "#b8b8b8" // UI element border and focus rings
|
||||||
|
lightStep8 := "#a0a0a0" // Hovered UI element border
|
||||||
|
lightStep9 := "#3b7dd8" // Solid backgrounds (primary blue)
|
||||||
|
lightStep10 := "#2968c3" // Hovered solid backgrounds
|
||||||
|
lightStep11 := "#8a8a8a" // Low-contrast text (more muted)
|
||||||
|
lightStep12 := "#1a1a1a" // High-contrast text
|
||||||
|
|
||||||
|
// Light mode accent colors
|
||||||
|
lightPrimary := lightStep9 // Primary uses step 9 (solid background)
|
||||||
lightSecondary := "#7b5bb6" // Secondary purple
|
lightSecondary := "#7b5bb6" // Secondary purple
|
||||||
lightAccent := "#d68c27" // Accent orange/gold
|
lightAccent := "#d68c27" // Accent orange/gold
|
||||||
lightRed := "#d1383d" // Error red
|
lightRed := "#d1383d" // Error red
|
||||||
|
@ -43,7 +60,14 @@ func NewOpenCodeTheme() *OpenCodeTheme {
|
||||||
lightGreen := "#3d9a57" // Success green
|
lightGreen := "#3d9a57" // Success green
|
||||||
lightCyan := "#318795" // Info cyan
|
lightCyan := "#318795" // Info cyan
|
||||||
lightYellow := "#b0851f" // Emphasized text
|
lightYellow := "#b0851f" // Emphasized text
|
||||||
lightBorder := "#d3d3d3" // Border color
|
|
||||||
|
// Unused variables to avoid compiler errors (these could be used for hover states)
|
||||||
|
_ = darkStep4
|
||||||
|
_ = darkStep5
|
||||||
|
_ = darkStep10
|
||||||
|
_ = lightStep4
|
||||||
|
_ = lightStep5
|
||||||
|
_ = lightStep10
|
||||||
|
|
||||||
theme := &OpenCodeTheme{}
|
theme := &OpenCodeTheme{}
|
||||||
|
|
||||||
|
@ -81,44 +105,40 @@ func NewOpenCodeTheme() *OpenCodeTheme {
|
||||||
|
|
||||||
// Text colors
|
// Text colors
|
||||||
theme.TextColor = lipgloss.AdaptiveColor{
|
theme.TextColor = lipgloss.AdaptiveColor{
|
||||||
Dark: darkForeground,
|
Dark: darkStep12,
|
||||||
Light: lightForeground,
|
Light: lightStep12,
|
||||||
}
|
}
|
||||||
theme.TextMutedColor = lipgloss.AdaptiveColor{
|
theme.TextMutedColor = lipgloss.AdaptiveColor{
|
||||||
Dark: darkComment,
|
Dark: darkStep11,
|
||||||
Light: lightComment,
|
Light: lightStep11,
|
||||||
}
|
|
||||||
theme.TextEmphasizedColor = lipgloss.AdaptiveColor{
|
|
||||||
Dark: darkYellow,
|
|
||||||
Light: lightYellow,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Background colors
|
// Background colors
|
||||||
theme.BackgroundColor = lipgloss.AdaptiveColor{
|
theme.BackgroundColor = lipgloss.AdaptiveColor{
|
||||||
Dark: darkBackground,
|
Dark: darkStep1,
|
||||||
Light: lightBackground,
|
Light: lightStep1,
|
||||||
}
|
}
|
||||||
theme.BackgroundSecondaryColor = lipgloss.AdaptiveColor{
|
theme.BackgroundSubtleColor = lipgloss.AdaptiveColor{
|
||||||
Dark: darkCurrentLine,
|
Dark: darkStep2,
|
||||||
Light: lightCurrentLine,
|
Light: lightStep2,
|
||||||
}
|
}
|
||||||
theme.BackgroundDarkerColor = lipgloss.AdaptiveColor{
|
theme.BackgroundElementColor = lipgloss.AdaptiveColor{
|
||||||
Dark: "#121212", // Slightly darker than background
|
Dark: darkStep3,
|
||||||
Light: "#ffffff", // Slightly lighter than background
|
Light: lightStep3,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Border colors
|
// Border colors
|
||||||
theme.BorderNormalColor = lipgloss.AdaptiveColor{
|
theme.BorderColor = lipgloss.AdaptiveColor{
|
||||||
Dark: darkBorder,
|
Dark: darkStep7,
|
||||||
Light: lightBorder,
|
Light: lightStep7,
|
||||||
}
|
}
|
||||||
theme.BorderFocusedColor = lipgloss.AdaptiveColor{
|
theme.BorderActiveColor = lipgloss.AdaptiveColor{
|
||||||
Dark: darkPrimary,
|
Dark: darkStep8,
|
||||||
Light: lightPrimary,
|
Light: lightStep8,
|
||||||
}
|
}
|
||||||
theme.BorderDimColor = lipgloss.AdaptiveColor{
|
theme.BorderSubtleColor = lipgloss.AdaptiveColor{
|
||||||
Dark: darkSelection,
|
Dark: darkStep6,
|
||||||
Light: lightSelection,
|
Light: lightStep6,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Diff view colors
|
// Diff view colors
|
||||||
|
@ -155,12 +175,12 @@ func NewOpenCodeTheme() *OpenCodeTheme {
|
||||||
Light: "#FFEBEE",
|
Light: "#FFEBEE",
|
||||||
}
|
}
|
||||||
theme.DiffContextBgColor = lipgloss.AdaptiveColor{
|
theme.DiffContextBgColor = lipgloss.AdaptiveColor{
|
||||||
Dark: darkBackground,
|
Dark: darkStep2,
|
||||||
Light: lightBackground,
|
Light: lightStep2,
|
||||||
}
|
}
|
||||||
theme.DiffLineNumberColor = lipgloss.AdaptiveColor{
|
theme.DiffLineNumberColor = lipgloss.AdaptiveColor{
|
||||||
Dark: "#888888",
|
Dark: darkStep3,
|
||||||
Light: "#9E9E9E",
|
Light: lightStep3,
|
||||||
}
|
}
|
||||||
theme.DiffAddedLineNumberBgColor = lipgloss.AdaptiveColor{
|
theme.DiffAddedLineNumberBgColor = lipgloss.AdaptiveColor{
|
||||||
Dark: "#293229",
|
Dark: "#293229",
|
||||||
|
@ -173,8 +193,8 @@ func NewOpenCodeTheme() *OpenCodeTheme {
|
||||||
|
|
||||||
// Markdown colors
|
// Markdown colors
|
||||||
theme.MarkdownTextColor = lipgloss.AdaptiveColor{
|
theme.MarkdownTextColor = lipgloss.AdaptiveColor{
|
||||||
Dark: darkForeground,
|
Dark: darkStep12,
|
||||||
Light: lightForeground,
|
Light: lightStep12,
|
||||||
}
|
}
|
||||||
theme.MarkdownHeadingColor = lipgloss.AdaptiveColor{
|
theme.MarkdownHeadingColor = lipgloss.AdaptiveColor{
|
||||||
Dark: darkSecondary,
|
Dark: darkSecondary,
|
||||||
|
@ -205,8 +225,8 @@ func NewOpenCodeTheme() *OpenCodeTheme {
|
||||||
Light: lightAccent,
|
Light: lightAccent,
|
||||||
}
|
}
|
||||||
theme.MarkdownHorizontalRuleColor = lipgloss.AdaptiveColor{
|
theme.MarkdownHorizontalRuleColor = lipgloss.AdaptiveColor{
|
||||||
Dark: darkComment,
|
Dark: darkStep11,
|
||||||
Light: lightComment,
|
Light: lightStep11,
|
||||||
}
|
}
|
||||||
theme.MarkdownListItemColor = lipgloss.AdaptiveColor{
|
theme.MarkdownListItemColor = lipgloss.AdaptiveColor{
|
||||||
Dark: darkPrimary,
|
Dark: darkPrimary,
|
||||||
|
@ -225,14 +245,14 @@ func NewOpenCodeTheme() *OpenCodeTheme {
|
||||||
Light: lightCyan,
|
Light: lightCyan,
|
||||||
}
|
}
|
||||||
theme.MarkdownCodeBlockColor = lipgloss.AdaptiveColor{
|
theme.MarkdownCodeBlockColor = lipgloss.AdaptiveColor{
|
||||||
Dark: darkForeground,
|
Dark: darkStep12,
|
||||||
Light: lightForeground,
|
Light: lightStep12,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Syntax highlighting colors
|
// Syntax highlighting colors
|
||||||
theme.SyntaxCommentColor = lipgloss.AdaptiveColor{
|
theme.SyntaxCommentColor = lipgloss.AdaptiveColor{
|
||||||
Dark: darkComment,
|
Dark: darkStep11,
|
||||||
Light: lightComment,
|
Light: lightStep11,
|
||||||
}
|
}
|
||||||
theme.SyntaxKeywordColor = lipgloss.AdaptiveColor{
|
theme.SyntaxKeywordColor = lipgloss.AdaptiveColor{
|
||||||
Dark: darkSecondary,
|
Dark: darkSecondary,
|
||||||
|
@ -263,8 +283,8 @@ func NewOpenCodeTheme() *OpenCodeTheme {
|
||||||
Light: lightCyan,
|
Light: lightCyan,
|
||||||
}
|
}
|
||||||
theme.SyntaxPunctuationColor = lipgloss.AdaptiveColor{
|
theme.SyntaxPunctuationColor = lipgloss.AdaptiveColor{
|
||||||
Dark: darkForeground,
|
Dark: darkStep12,
|
||||||
Light: lightForeground,
|
Light: lightStep12,
|
||||||
}
|
}
|
||||||
|
|
||||||
return theme
|
return theme
|
||||||
|
|
|
@ -11,32 +11,31 @@ import (
|
||||||
// All colors must be defined as lipgloss.AdaptiveColor to support
|
// All colors must be defined as lipgloss.AdaptiveColor to support
|
||||||
// both light and dark terminal backgrounds.
|
// both light and dark terminal backgrounds.
|
||||||
type Theme interface {
|
type Theme interface {
|
||||||
// Base colors
|
// Background colors
|
||||||
Primary() lipgloss.AdaptiveColor
|
Background() lipgloss.AdaptiveColor // Radix 1
|
||||||
|
BackgroundSubtle() lipgloss.AdaptiveColor // Radix 2
|
||||||
|
BackgroundElement() lipgloss.AdaptiveColor // Radix 3
|
||||||
|
|
||||||
|
// Border colors
|
||||||
|
BorderSubtle() lipgloss.AdaptiveColor // Radix 6
|
||||||
|
Border() lipgloss.AdaptiveColor // Radix 7
|
||||||
|
BorderActive() lipgloss.AdaptiveColor // Radix 8
|
||||||
|
|
||||||
|
// Brand colors
|
||||||
|
Primary() lipgloss.AdaptiveColor // Radix 9
|
||||||
Secondary() lipgloss.AdaptiveColor
|
Secondary() lipgloss.AdaptiveColor
|
||||||
Accent() lipgloss.AdaptiveColor
|
Accent() lipgloss.AdaptiveColor
|
||||||
|
|
||||||
|
// Text colors
|
||||||
|
TextMuted() lipgloss.AdaptiveColor // Radix 11
|
||||||
|
Text() lipgloss.AdaptiveColor // Radix 12
|
||||||
|
|
||||||
// Status colors
|
// Status colors
|
||||||
Error() lipgloss.AdaptiveColor
|
Error() lipgloss.AdaptiveColor
|
||||||
Warning() lipgloss.AdaptiveColor
|
Warning() lipgloss.AdaptiveColor
|
||||||
Success() lipgloss.AdaptiveColor
|
Success() lipgloss.AdaptiveColor
|
||||||
Info() lipgloss.AdaptiveColor
|
Info() lipgloss.AdaptiveColor
|
||||||
|
|
||||||
// Text colors
|
|
||||||
Text() lipgloss.AdaptiveColor
|
|
||||||
TextMuted() lipgloss.AdaptiveColor
|
|
||||||
TextEmphasized() lipgloss.AdaptiveColor
|
|
||||||
|
|
||||||
// Background colors
|
|
||||||
Background() lipgloss.AdaptiveColor
|
|
||||||
BackgroundSecondary() lipgloss.AdaptiveColor
|
|
||||||
BackgroundDarker() lipgloss.AdaptiveColor
|
|
||||||
|
|
||||||
// Border colors
|
|
||||||
BorderNormal() lipgloss.AdaptiveColor
|
|
||||||
BorderFocused() lipgloss.AdaptiveColor
|
|
||||||
BorderDim() lipgloss.AdaptiveColor
|
|
||||||
|
|
||||||
// Diff view colors
|
// Diff view colors
|
||||||
DiffAdded() lipgloss.AdaptiveColor
|
DiffAdded() lipgloss.AdaptiveColor
|
||||||
DiffRemoved() lipgloss.AdaptiveColor
|
DiffRemoved() lipgloss.AdaptiveColor
|
||||||
|
@ -82,32 +81,31 @@ type Theme interface {
|
||||||
// BaseTheme provides a default implementation of the Theme interface
|
// BaseTheme provides a default implementation of the Theme interface
|
||||||
// that can be embedded in concrete theme implementations.
|
// that can be embedded in concrete theme implementations.
|
||||||
type BaseTheme struct {
|
type BaseTheme struct {
|
||||||
// Base colors
|
// Background colors
|
||||||
|
BackgroundColor lipgloss.AdaptiveColor
|
||||||
|
BackgroundSubtleColor lipgloss.AdaptiveColor
|
||||||
|
BackgroundElementColor lipgloss.AdaptiveColor
|
||||||
|
|
||||||
|
// Border colors
|
||||||
|
BorderSubtleColor lipgloss.AdaptiveColor
|
||||||
|
BorderColor lipgloss.AdaptiveColor
|
||||||
|
BorderActiveColor lipgloss.AdaptiveColor
|
||||||
|
|
||||||
|
// Brand colors
|
||||||
PrimaryColor lipgloss.AdaptiveColor
|
PrimaryColor lipgloss.AdaptiveColor
|
||||||
SecondaryColor lipgloss.AdaptiveColor
|
SecondaryColor lipgloss.AdaptiveColor
|
||||||
AccentColor lipgloss.AdaptiveColor
|
AccentColor lipgloss.AdaptiveColor
|
||||||
|
|
||||||
|
// Text colors
|
||||||
|
TextMutedColor lipgloss.AdaptiveColor
|
||||||
|
TextColor lipgloss.AdaptiveColor
|
||||||
|
|
||||||
// Status colors
|
// Status colors
|
||||||
ErrorColor lipgloss.AdaptiveColor
|
ErrorColor lipgloss.AdaptiveColor
|
||||||
WarningColor lipgloss.AdaptiveColor
|
WarningColor lipgloss.AdaptiveColor
|
||||||
SuccessColor lipgloss.AdaptiveColor
|
SuccessColor lipgloss.AdaptiveColor
|
||||||
InfoColor lipgloss.AdaptiveColor
|
InfoColor lipgloss.AdaptiveColor
|
||||||
|
|
||||||
// Text colors
|
|
||||||
TextColor lipgloss.AdaptiveColor
|
|
||||||
TextMutedColor lipgloss.AdaptiveColor
|
|
||||||
TextEmphasizedColor lipgloss.AdaptiveColor
|
|
||||||
|
|
||||||
// Background colors
|
|
||||||
BackgroundColor lipgloss.AdaptiveColor
|
|
||||||
BackgroundSecondaryColor lipgloss.AdaptiveColor
|
|
||||||
BackgroundDarkerColor lipgloss.AdaptiveColor
|
|
||||||
|
|
||||||
// Border colors
|
|
||||||
BorderNormalColor lipgloss.AdaptiveColor
|
|
||||||
BorderFocusedColor lipgloss.AdaptiveColor
|
|
||||||
BorderDimColor lipgloss.AdaptiveColor
|
|
||||||
|
|
||||||
// Diff view colors
|
// Diff view colors
|
||||||
DiffAddedColor lipgloss.AdaptiveColor
|
DiffAddedColor lipgloss.AdaptiveColor
|
||||||
DiffRemovedColor lipgloss.AdaptiveColor
|
DiffRemovedColor lipgloss.AdaptiveColor
|
||||||
|
@ -160,17 +158,16 @@ func (t *BaseTheme) Warning() lipgloss.AdaptiveColor { return t.WarningColor }
|
||||||
func (t *BaseTheme) Success() lipgloss.AdaptiveColor { return t.SuccessColor }
|
func (t *BaseTheme) Success() lipgloss.AdaptiveColor { return t.SuccessColor }
|
||||||
func (t *BaseTheme) Info() lipgloss.AdaptiveColor { return t.InfoColor }
|
func (t *BaseTheme) Info() lipgloss.AdaptiveColor { return t.InfoColor }
|
||||||
|
|
||||||
func (t *BaseTheme) Text() lipgloss.AdaptiveColor { return t.TextColor }
|
func (t *BaseTheme) Text() lipgloss.AdaptiveColor { return t.TextColor }
|
||||||
func (t *BaseTheme) TextMuted() lipgloss.AdaptiveColor { return t.TextMutedColor }
|
func (t *BaseTheme) TextMuted() lipgloss.AdaptiveColor { return t.TextMutedColor }
|
||||||
func (t *BaseTheme) TextEmphasized() lipgloss.AdaptiveColor { return t.TextEmphasizedColor }
|
|
||||||
|
|
||||||
func (t *BaseTheme) Background() lipgloss.AdaptiveColor { return t.BackgroundColor }
|
func (t *BaseTheme) Background() lipgloss.AdaptiveColor { return t.BackgroundColor }
|
||||||
func (t *BaseTheme) BackgroundSecondary() lipgloss.AdaptiveColor { return t.BackgroundSecondaryColor }
|
func (t *BaseTheme) BackgroundSubtle() lipgloss.AdaptiveColor { return t.BackgroundSubtleColor }
|
||||||
func (t *BaseTheme) BackgroundDarker() lipgloss.AdaptiveColor { return t.BackgroundDarkerColor }
|
func (t *BaseTheme) BackgroundElement() lipgloss.AdaptiveColor { return t.BackgroundElementColor }
|
||||||
|
|
||||||
func (t *BaseTheme) BorderNormal() lipgloss.AdaptiveColor { return t.BorderNormalColor }
|
func (t *BaseTheme) Border() lipgloss.AdaptiveColor { return t.BorderColor }
|
||||||
func (t *BaseTheme) BorderFocused() lipgloss.AdaptiveColor { return t.BorderFocusedColor }
|
func (t *BaseTheme) BorderActive() lipgloss.AdaptiveColor { return t.BorderActiveColor }
|
||||||
func (t *BaseTheme) BorderDim() lipgloss.AdaptiveColor { return t.BorderDimColor }
|
func (t *BaseTheme) BorderSubtle() lipgloss.AdaptiveColor { return t.BorderSubtleColor }
|
||||||
|
|
||||||
func (t *BaseTheme) DiffAdded() lipgloss.AdaptiveColor { return t.DiffAddedColor }
|
func (t *BaseTheme) DiffAdded() lipgloss.AdaptiveColor { return t.DiffAddedColor }
|
||||||
func (t *BaseTheme) DiffRemoved() lipgloss.AdaptiveColor { return t.DiffRemovedColor }
|
func (t *BaseTheme) DiffRemoved() lipgloss.AdaptiveColor { return t.DiffRemovedColor }
|
||||||
|
|
|
@ -1,89 +0,0 @@
|
||||||
package theme
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestThemeRegistration(t *testing.T) {
|
|
||||||
// Get list of available themes
|
|
||||||
availableThemes := AvailableThemes()
|
|
||||||
|
|
||||||
// Check if "catppuccin" theme is registered
|
|
||||||
catppuccinFound := false
|
|
||||||
for _, themeName := range availableThemes {
|
|
||||||
if themeName == "catppuccin" {
|
|
||||||
catppuccinFound = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !catppuccinFound {
|
|
||||||
t.Errorf("Catppuccin theme is not registered")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if "gruvbox" theme is registered
|
|
||||||
gruvboxFound := false
|
|
||||||
for _, themeName := range availableThemes {
|
|
||||||
if themeName == "gruvbox" {
|
|
||||||
gruvboxFound = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !gruvboxFound {
|
|
||||||
t.Errorf("Gruvbox theme is not registered")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if "monokai" theme is registered
|
|
||||||
monokaiFound := false
|
|
||||||
for _, themeName := range availableThemes {
|
|
||||||
if themeName == "monokai" {
|
|
||||||
monokaiFound = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !monokaiFound {
|
|
||||||
t.Errorf("Monokai theme is not registered")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try to get the themes and make sure they're not nil
|
|
||||||
catppuccin := GetTheme("catppuccin")
|
|
||||||
if catppuccin == nil {
|
|
||||||
t.Errorf("Catppuccin theme is nil")
|
|
||||||
}
|
|
||||||
|
|
||||||
gruvbox := GetTheme("gruvbox")
|
|
||||||
if gruvbox == nil {
|
|
||||||
t.Errorf("Gruvbox theme is nil")
|
|
||||||
}
|
|
||||||
|
|
||||||
monokai := GetTheme("monokai")
|
|
||||||
if monokai == nil {
|
|
||||||
t.Errorf("Monokai theme is nil")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test switching theme
|
|
||||||
originalTheme := CurrentThemeName()
|
|
||||||
|
|
||||||
err := SetTheme("gruvbox")
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Failed to set theme to gruvbox: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if CurrentThemeName() != "gruvbox" {
|
|
||||||
t.Errorf("Theme not properly switched to gruvbox")
|
|
||||||
}
|
|
||||||
|
|
||||||
err = SetTheme("monokai")
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Failed to set theme to monokai: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if CurrentThemeName() != "monokai" {
|
|
||||||
t.Errorf("Theme not properly switched to monokai")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Switch back to original theme
|
|
||||||
_ = SetTheme(originalTheme)
|
|
||||||
}
|
|
|
@ -12,36 +12,60 @@ type TokyoNightTheme struct {
|
||||||
|
|
||||||
// NewTokyoNightTheme creates a new instance of the Tokyo Night theme.
|
// NewTokyoNightTheme creates a new instance of the Tokyo Night theme.
|
||||||
func NewTokyoNightTheme() *TokyoNightTheme {
|
func NewTokyoNightTheme() *TokyoNightTheme {
|
||||||
// Tokyo Night color palette
|
// Tokyo Night color palette with Radix-inspired scale progression
|
||||||
// Dark mode colors
|
// Dark mode colors - Tokyo Night Moon variant
|
||||||
darkBackground := "#222436"
|
darkStep1 := "#1a1b26" // App background (bg)
|
||||||
darkCurrentLine := "#1e2030"
|
darkStep2 := "#1e2030" // Subtle background (bg_dark)
|
||||||
darkSelection := "#2f334d"
|
darkStep3 := "#222436" // UI element background (bg_highlight)
|
||||||
darkForeground := "#c8d3f5"
|
darkStep4 := "#292e42" // Hovered UI element background
|
||||||
darkComment := "#636da6"
|
darkStep5 := "#3b4261" // Active/Selected UI element background (bg_visual)
|
||||||
|
darkStep6 := "#545c7e" // Subtle borders and separators (dark3)
|
||||||
|
darkStep7 := "#737aa2" // UI element border and focus rings (dark5)
|
||||||
|
darkStep8 := "#9099b2" // Hovered UI element border
|
||||||
|
darkStep9 := "#82aaff" // Solid backgrounds (blue)
|
||||||
|
darkStep10 := "#89b4fa" // Hovered solid backgrounds
|
||||||
|
darkStep11 := "#828bb8" // Low-contrast text (using fg_dark for better contrast)
|
||||||
|
darkStep12 := "#c8d3f5" // High-contrast text (fg)
|
||||||
|
|
||||||
|
// Dark mode accent colors
|
||||||
darkRed := "#ff757f"
|
darkRed := "#ff757f"
|
||||||
darkOrange := "#ff966c"
|
darkOrange := "#ff966c"
|
||||||
darkYellow := "#ffc777"
|
darkYellow := "#ffc777"
|
||||||
darkGreen := "#c3e88d"
|
darkGreen := "#c3e88d"
|
||||||
darkCyan := "#86e1fc"
|
darkCyan := "#86e1fc"
|
||||||
darkBlue := "#82aaff"
|
darkBlue := darkStep9 // Using step 9 for primary
|
||||||
darkPurple := "#c099ff"
|
darkPurple := "#c099ff"
|
||||||
darkBorder := "#3b4261"
|
|
||||||
|
|
||||||
// Light mode colors (Tokyo Night Day)
|
// Light mode colors - Tokyo Night Day variant
|
||||||
lightBackground := "#e1e2e7"
|
lightStep1 := "#e1e2e7" // App background
|
||||||
lightCurrentLine := "#d5d6db"
|
lightStep2 := "#d5d6db" // Subtle background
|
||||||
lightSelection := "#c8c9ce"
|
lightStep3 := "#c8c9ce" // UI element background
|
||||||
lightForeground := "#3760bf"
|
lightStep4 := "#b9bac1" // Hovered UI element background
|
||||||
lightComment := "#848cb5"
|
lightStep5 := "#a8aecb" // Active/Selected UI element background
|
||||||
|
lightStep6 := "#9699a8" // Subtle borders and separators
|
||||||
|
lightStep7 := "#737a8c" // UI element border and focus rings
|
||||||
|
lightStep8 := "#5a607d" // Hovered UI element border
|
||||||
|
lightStep9 := "#2e7de9" // Solid backgrounds (blue)
|
||||||
|
lightStep10 := "#1a6ce7" // Hovered solid backgrounds
|
||||||
|
lightStep11 := "#8990a3" // Low-contrast text (more muted)
|
||||||
|
lightStep12 := "#3760bf" // High-contrast text
|
||||||
|
|
||||||
|
// Light mode accent colors
|
||||||
lightRed := "#f52a65"
|
lightRed := "#f52a65"
|
||||||
lightOrange := "#b15c00"
|
lightOrange := "#b15c00"
|
||||||
lightYellow := "#8c6c3e"
|
lightYellow := "#8c6c3e"
|
||||||
lightGreen := "#587539"
|
lightGreen := "#587539"
|
||||||
lightCyan := "#007197"
|
lightCyan := "#007197"
|
||||||
lightBlue := "#2e7de9"
|
lightBlue := lightStep9 // Using step 9 for primary
|
||||||
lightPurple := "#9854f1"
|
lightPurple := "#9854f1"
|
||||||
lightBorder := "#a8aecb"
|
|
||||||
|
// Unused variables to avoid compiler errors (these could be used for hover states)
|
||||||
|
_ = darkStep4
|
||||||
|
_ = darkStep5
|
||||||
|
_ = darkStep10
|
||||||
|
_ = lightStep4
|
||||||
|
_ = lightStep5
|
||||||
|
_ = lightStep10
|
||||||
|
|
||||||
theme := &TokyoNightTheme{}
|
theme := &TokyoNightTheme{}
|
||||||
|
|
||||||
|
@ -79,44 +103,40 @@ func NewTokyoNightTheme() *TokyoNightTheme {
|
||||||
|
|
||||||
// Text colors
|
// Text colors
|
||||||
theme.TextColor = lipgloss.AdaptiveColor{
|
theme.TextColor = lipgloss.AdaptiveColor{
|
||||||
Dark: darkForeground,
|
Dark: darkStep12,
|
||||||
Light: lightForeground,
|
Light: lightStep12,
|
||||||
}
|
}
|
||||||
theme.TextMutedColor = lipgloss.AdaptiveColor{
|
theme.TextMutedColor = lipgloss.AdaptiveColor{
|
||||||
Dark: darkComment,
|
Dark: darkStep11,
|
||||||
Light: lightComment,
|
Light: lightStep11,
|
||||||
}
|
|
||||||
theme.TextEmphasizedColor = lipgloss.AdaptiveColor{
|
|
||||||
Dark: darkYellow,
|
|
||||||
Light: lightYellow,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Background colors
|
// Background colors
|
||||||
theme.BackgroundColor = lipgloss.AdaptiveColor{
|
theme.BackgroundColor = lipgloss.AdaptiveColor{
|
||||||
Dark: darkBackground,
|
Dark: darkStep1,
|
||||||
Light: lightBackground,
|
Light: lightStep1,
|
||||||
}
|
}
|
||||||
theme.BackgroundSecondaryColor = lipgloss.AdaptiveColor{
|
theme.BackgroundSubtleColor = lipgloss.AdaptiveColor{
|
||||||
Dark: darkCurrentLine,
|
Dark: darkStep2,
|
||||||
Light: lightCurrentLine,
|
Light: lightStep2,
|
||||||
}
|
}
|
||||||
theme.BackgroundDarkerColor = lipgloss.AdaptiveColor{
|
theme.BackgroundElementColor = lipgloss.AdaptiveColor{
|
||||||
Dark: "#191B29", // Darker background from palette
|
Dark: darkStep3,
|
||||||
Light: "#f0f0f5", // Slightly lighter than background
|
Light: lightStep3,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Border colors
|
// Border colors
|
||||||
theme.BorderNormalColor = lipgloss.AdaptiveColor{
|
theme.BorderColor = lipgloss.AdaptiveColor{
|
||||||
Dark: darkBorder,
|
Dark: darkStep7,
|
||||||
Light: lightBorder,
|
Light: lightStep7,
|
||||||
}
|
}
|
||||||
theme.BorderFocusedColor = lipgloss.AdaptiveColor{
|
theme.BorderActiveColor = lipgloss.AdaptiveColor{
|
||||||
Dark: darkBlue,
|
Dark: darkStep8,
|
||||||
Light: lightBlue,
|
Light: lightStep8,
|
||||||
}
|
}
|
||||||
theme.BorderDimColor = lipgloss.AdaptiveColor{
|
theme.BorderSubtleColor = lipgloss.AdaptiveColor{
|
||||||
Dark: darkSelection,
|
Dark: darkStep6,
|
||||||
Light: lightSelection,
|
Light: lightStep6,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Diff view colors
|
// Diff view colors
|
||||||
|
@ -153,12 +173,12 @@ func NewTokyoNightTheme() *TokyoNightTheme {
|
||||||
Light: "#f7d8db",
|
Light: "#f7d8db",
|
||||||
}
|
}
|
||||||
theme.DiffContextBgColor = lipgloss.AdaptiveColor{
|
theme.DiffContextBgColor = lipgloss.AdaptiveColor{
|
||||||
Dark: darkBackground,
|
Dark: darkStep2,
|
||||||
Light: lightBackground,
|
Light: lightStep2,
|
||||||
}
|
}
|
||||||
theme.DiffLineNumberColor = lipgloss.AdaptiveColor{
|
theme.DiffLineNumberColor = lipgloss.AdaptiveColor{
|
||||||
Dark: "#545c7e", // dark3 from palette
|
Dark: darkStep3, // dark3 from palette
|
||||||
Light: "#848cb5",
|
Light: lightStep3,
|
||||||
}
|
}
|
||||||
theme.DiffAddedLineNumberBgColor = lipgloss.AdaptiveColor{
|
theme.DiffAddedLineNumberBgColor = lipgloss.AdaptiveColor{
|
||||||
Dark: "#1b2b34",
|
Dark: "#1b2b34",
|
||||||
|
@ -171,8 +191,8 @@ func NewTokyoNightTheme() *TokyoNightTheme {
|
||||||
|
|
||||||
// Markdown colors
|
// Markdown colors
|
||||||
theme.MarkdownTextColor = lipgloss.AdaptiveColor{
|
theme.MarkdownTextColor = lipgloss.AdaptiveColor{
|
||||||
Dark: darkForeground,
|
Dark: darkStep12,
|
||||||
Light: lightForeground,
|
Light: lightStep12,
|
||||||
}
|
}
|
||||||
theme.MarkdownHeadingColor = lipgloss.AdaptiveColor{
|
theme.MarkdownHeadingColor = lipgloss.AdaptiveColor{
|
||||||
Dark: darkPurple,
|
Dark: darkPurple,
|
||||||
|
@ -203,8 +223,8 @@ func NewTokyoNightTheme() *TokyoNightTheme {
|
||||||
Light: lightOrange,
|
Light: lightOrange,
|
||||||
}
|
}
|
||||||
theme.MarkdownHorizontalRuleColor = lipgloss.AdaptiveColor{
|
theme.MarkdownHorizontalRuleColor = lipgloss.AdaptiveColor{
|
||||||
Dark: darkComment,
|
Dark: darkStep11,
|
||||||
Light: lightComment,
|
Light: lightStep11,
|
||||||
}
|
}
|
||||||
theme.MarkdownListItemColor = lipgloss.AdaptiveColor{
|
theme.MarkdownListItemColor = lipgloss.AdaptiveColor{
|
||||||
Dark: darkBlue,
|
Dark: darkBlue,
|
||||||
|
@ -223,14 +243,14 @@ func NewTokyoNightTheme() *TokyoNightTheme {
|
||||||
Light: lightCyan,
|
Light: lightCyan,
|
||||||
}
|
}
|
||||||
theme.MarkdownCodeBlockColor = lipgloss.AdaptiveColor{
|
theme.MarkdownCodeBlockColor = lipgloss.AdaptiveColor{
|
||||||
Dark: darkForeground,
|
Dark: darkStep12,
|
||||||
Light: lightForeground,
|
Light: lightStep12,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Syntax highlighting colors
|
// Syntax highlighting colors
|
||||||
theme.SyntaxCommentColor = lipgloss.AdaptiveColor{
|
theme.SyntaxCommentColor = lipgloss.AdaptiveColor{
|
||||||
Dark: darkComment,
|
Dark: darkStep11,
|
||||||
Light: lightComment,
|
Light: lightStep11,
|
||||||
}
|
}
|
||||||
theme.SyntaxKeywordColor = lipgloss.AdaptiveColor{
|
theme.SyntaxKeywordColor = lipgloss.AdaptiveColor{
|
||||||
Dark: darkPurple,
|
Dark: darkPurple,
|
||||||
|
@ -261,8 +281,8 @@ func NewTokyoNightTheme() *TokyoNightTheme {
|
||||||
Light: lightCyan,
|
Light: lightCyan,
|
||||||
}
|
}
|
||||||
theme.SyntaxPunctuationColor = lipgloss.AdaptiveColor{
|
theme.SyntaxPunctuationColor = lipgloss.AdaptiveColor{
|
||||||
Dark: darkForeground,
|
Dark: darkStep12,
|
||||||
Light: lightForeground,
|
Light: lightStep12,
|
||||||
}
|
}
|
||||||
|
|
||||||
return theme
|
return theme
|
||||||
|
|
|
@ -88,35 +88,31 @@ func NewTronTheme() *TronTheme {
|
||||||
Dark: darkComment,
|
Dark: darkComment,
|
||||||
Light: lightComment,
|
Light: lightComment,
|
||||||
}
|
}
|
||||||
theme.TextEmphasizedColor = lipgloss.AdaptiveColor{
|
|
||||||
Dark: darkYellow,
|
|
||||||
Light: lightYellow,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Background colors
|
// Background colors
|
||||||
theme.BackgroundColor = lipgloss.AdaptiveColor{
|
theme.BackgroundColor = lipgloss.AdaptiveColor{
|
||||||
Dark: darkBackground,
|
Dark: darkBackground,
|
||||||
Light: lightBackground,
|
Light: lightBackground,
|
||||||
}
|
}
|
||||||
theme.BackgroundSecondaryColor = lipgloss.AdaptiveColor{
|
theme.BackgroundSubtleColor = lipgloss.AdaptiveColor{
|
||||||
Dark: darkCurrentLine,
|
Dark: darkCurrentLine,
|
||||||
Light: lightCurrentLine,
|
Light: lightCurrentLine,
|
||||||
}
|
}
|
||||||
theme.BackgroundDarkerColor = lipgloss.AdaptiveColor{
|
theme.BackgroundElementColor = lipgloss.AdaptiveColor{
|
||||||
Dark: "#070d14", // Slightly darker than background
|
Dark: "#070d14", // Slightly darker than background
|
||||||
Light: "#ffffff", // Slightly lighter than background
|
Light: "#ffffff", // Slightly lighter than background
|
||||||
}
|
}
|
||||||
|
|
||||||
// Border colors
|
// Border colors
|
||||||
theme.BorderNormalColor = lipgloss.AdaptiveColor{
|
theme.BorderColor = lipgloss.AdaptiveColor{
|
||||||
Dark: darkBorder,
|
Dark: darkBorder,
|
||||||
Light: lightBorder,
|
Light: lightBorder,
|
||||||
}
|
}
|
||||||
theme.BorderFocusedColor = lipgloss.AdaptiveColor{
|
theme.BorderActiveColor = lipgloss.AdaptiveColor{
|
||||||
Dark: darkCyan,
|
Dark: darkCyan,
|
||||||
Light: lightCyan,
|
Light: lightCyan,
|
||||||
}
|
}
|
||||||
theme.BorderDimColor = lipgloss.AdaptiveColor{
|
theme.BorderSubtleColor = lipgloss.AdaptiveColor{
|
||||||
Dark: darkSelection,
|
Dark: darkSelection,
|
||||||
Light: lightSelection,
|
Light: lightSelection,
|
||||||
}
|
}
|
||||||
|
|
|
@ -158,7 +158,7 @@ func (a appModel) Init() tea.Cmd {
|
||||||
|
|
||||||
// Check if we should show the init dialog
|
// Check if we should show the init dialog
|
||||||
cmds = append(cmds, func() tea.Msg {
|
cmds = append(cmds, func() tea.Msg {
|
||||||
shouldShow := a.app.Info.Git && a.app.Info.Time.Initialized == nil
|
shouldShow := app.Info.Git && app.Info.Time.Initialized == nil
|
||||||
return dialog.ShowInitDialogMsg{Show: shouldShow}
|
return dialog.ShowInitDialogMsg{Show: shouldShow}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -212,6 +212,27 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
msg.Height -= 2 // Make space for the status bar
|
msg.Height -= 2 // Make space for the status bar
|
||||||
a.width, a.height = msg.Width, msg.Height
|
a.width, a.height = msg.Width, msg.Height
|
||||||
|
|
||||||
|
size := layout.LayoutSizeNormal
|
||||||
|
if a.width < 40 {
|
||||||
|
size = layout.LayoutSizeSmall
|
||||||
|
} else if a.width < 80 {
|
||||||
|
size = layout.LayoutSizeNormal
|
||||||
|
} else {
|
||||||
|
size = layout.LayoutSizeLarge
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: move away from global state
|
||||||
|
layout.Current = &layout.LayoutInfo{
|
||||||
|
Size: size,
|
||||||
|
Viewport: layout.Dimensions{
|
||||||
|
Width: a.width,
|
||||||
|
Height: a.height,
|
||||||
|
},
|
||||||
|
Container: layout.Dimensions{
|
||||||
|
Width: min(a.width, 80),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
s, _ := a.status.Update(msg)
|
s, _ := a.status.Update(msg)
|
||||||
a.status = s.(core.StatusCmp)
|
a.status = s.(core.StatusCmp)
|
||||||
a.pages[a.currentPage], cmd = a.pages[a.currentPage].Update(msg)
|
a.pages[a.currentPage], cmd = a.pages[a.currentPage].Update(msg)
|
||||||
|
@ -711,7 +732,6 @@ func (a appModel) View() string {
|
||||||
components := []string{
|
components := []string{
|
||||||
a.pages[a.currentPage].View(),
|
a.pages[a.currentPage].View(),
|
||||||
}
|
}
|
||||||
|
|
||||||
components = append(components, a.status.View())
|
components = append(components, a.status.View())
|
||||||
|
|
||||||
appView := lipgloss.JoinVertical(lipgloss.Top, components...)
|
appView := lipgloss.JoinVertical(lipgloss.Top, components...)
|
||||||
|
@ -943,7 +963,7 @@ func NewModel(app *app.App) tea.Model {
|
||||||
})
|
})
|
||||||
|
|
||||||
// Load custom commands
|
// Load custom commands
|
||||||
customCommands, err := dialog.LoadCustomCommands(app)
|
customCommands, err := dialog.LoadCustomCommands()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Warn("Failed to load custom commands", "error", err)
|
slog.Warn("Failed to load custom commands", "error", err)
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -11,6 +11,7 @@ func CmdHandler(msg tea.Msg) tea.Cmd {
|
||||||
}
|
}
|
||||||
|
|
||||||
func Clamp(v, low, high int) int {
|
func Clamp(v, low, high int) int {
|
||||||
|
// Swap if needed to ensure low <= high
|
||||||
if high < low {
|
if high < low {
|
||||||
low, high = high, low
|
low, high = high, low
|
||||||
}
|
}
|
||||||
|
|
|
@ -59,9 +59,9 @@ You can define any of the following color keys in your `customTheme`.
|
||||||
| ----------------- | ------------------------------------------------------- |
|
| ----------------- | ------------------------------------------------------- |
|
||||||
| Base colors | `primary`, `secondary`, `accent` |
|
| Base colors | `primary`, `secondary`, `accent` |
|
||||||
| Status colors | `error`, `warning`, `success`, `info` |
|
| Status colors | `error`, `warning`, `success`, `info` |
|
||||||
| Text colors | `text`, `textMuted`, `textEmphasized` |
|
| Text colors | `text`, `textMuted` |
|
||||||
| Background colors | `background`, `backgroundSecondary`, `backgroundDarker` |
|
| Background colors | `background`, `backgroundSubtle`, `backgroundElement` |
|
||||||
| Border colors | `borderNormal`, `borderFocused`, `borderDim` |
|
| Border colors | `border`, `borderActive`, `borderSubtle` |
|
||||||
| Diff view colors | `diffAdded`, `diffRemoved`, `diffContext`, etc. |
|
| Diff view colors | `diffAdded`, `diffRemoved`, `diffContext`, etc. |
|
||||||
|
|
||||||
You don't need to define all the color keys. Any undefined colors will fall back to the default `opencode` theme colors.
|
You don't need to define all the color keys. Any undefined colors will fall back to the default `opencode` theme colors.
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue