Detailed changes
@@ -7,6 +7,7 @@ package db
import (
"context"
+ "database/sql"
)
const createMessage = `-- name: CreateMessage :one
@@ -15,19 +16,21 @@ INSERT INTO messages (
session_id,
role,
parts,
+ model,
created_at,
updated_at
) VALUES (
- ?, ?, ?, ?, strftime('%s', 'now'), strftime('%s', 'now')
+ ?, ?, ?, ?, ?, strftime('%s', 'now'), strftime('%s', 'now')
)
-RETURNING id, session_id, role, parts, created_at, updated_at
+RETURNING id, session_id, role, parts, model, created_at, updated_at, finished_at
`
type CreateMessageParams struct {
- ID string `json:"id"`
- SessionID string `json:"session_id"`
- Role string `json:"role"`
- Parts string `json:"parts"`
+ ID string `json:"id"`
+ SessionID string `json:"session_id"`
+ Role string `json:"role"`
+ Parts string `json:"parts"`
+ Model sql.NullString `json:"model"`
}
func (q *Queries) CreateMessage(ctx context.Context, arg CreateMessageParams) (Message, error) {
@@ -36,6 +39,7 @@ func (q *Queries) CreateMessage(ctx context.Context, arg CreateMessageParams) (M
arg.SessionID,
arg.Role,
arg.Parts,
+ arg.Model,
)
var i Message
err := row.Scan(
@@ -43,8 +47,10 @@ func (q *Queries) CreateMessage(ctx context.Context, arg CreateMessageParams) (M
&i.SessionID,
&i.Role,
&i.Parts,
+ &i.Model,
&i.CreatedAt,
&i.UpdatedAt,
+ &i.FinishedAt,
)
return i, err
}
@@ -70,7 +76,7 @@ func (q *Queries) DeleteSessionMessages(ctx context.Context, sessionID string) e
}
const getMessage = `-- name: GetMessage :one
-SELECT id, session_id, role, parts, created_at, updated_at
+SELECT id, session_id, role, parts, model, created_at, updated_at, finished_at
FROM messages
WHERE id = ? LIMIT 1
`
@@ -83,14 +89,16 @@ func (q *Queries) GetMessage(ctx context.Context, id string) (Message, error) {
&i.SessionID,
&i.Role,
&i.Parts,
+ &i.Model,
&i.CreatedAt,
&i.UpdatedAt,
+ &i.FinishedAt,
)
return i, err
}
const listMessagesBySession = `-- name: ListMessagesBySession :many
-SELECT id, session_id, role, parts, created_at, updated_at
+SELECT id, session_id, role, parts, model, created_at, updated_at, finished_at
FROM messages
WHERE session_id = ?
ORDER BY created_at ASC
@@ -110,8 +118,10 @@ func (q *Queries) ListMessagesBySession(ctx context.Context, sessionID string) (
&i.SessionID,
&i.Role,
&i.Parts,
+ &i.Model,
&i.CreatedAt,
&i.UpdatedAt,
+ &i.FinishedAt,
); err != nil {
return nil, err
}
@@ -130,16 +140,18 @@ const updateMessage = `-- name: UpdateMessage :exec
UPDATE messages
SET
parts = ?,
+ finished_at = ?,
updated_at = strftime('%s', 'now')
WHERE id = ?
`
type UpdateMessageParams struct {
- Parts string `json:"parts"`
- ID string `json:"id"`
+ Parts string `json:"parts"`
+ FinishedAt sql.NullInt64 `json:"finished_at"`
+ ID string `json:"id"`
}
func (q *Queries) UpdateMessage(ctx context.Context, arg UpdateMessageParams) error {
- _, err := q.exec(ctx, q.updateMessageStmt, updateMessage, arg.Parts, arg.ID)
+ _, err := q.exec(ctx, q.updateMessageStmt, updateMessage, arg.Parts, arg.FinishedAt, arg.ID)
return err
}
@@ -24,8 +24,10 @@ CREATE TABLE IF NOT EXISTS messages (
session_id TEXT NOT NULL,
role TEXT NOT NULL,
parts TEXT NOT NULL default '[]',
+ model TEXT,
created_at INTEGER NOT NULL, -- Unix timestamp in milliseconds
updated_at INTEGER NOT NULL, -- Unix timestamp in milliseconds
+ finished_at INTEGER, -- Unix timestamp in milliseconds
FOREIGN KEY (session_id) REFERENCES sessions (id) ON DELETE CASCADE
);
@@ -9,12 +9,14 @@ import (
)
type Message struct {
- ID string `json:"id"`
- SessionID string `json:"session_id"`
- Role string `json:"role"`
- Parts string `json:"parts"`
- CreatedAt int64 `json:"created_at"`
- UpdatedAt int64 `json:"updated_at"`
+ ID string `json:"id"`
+ SessionID string `json:"session_id"`
+ Role string `json:"role"`
+ Parts string `json:"parts"`
+ Model sql.NullString `json:"model"`
+ CreatedAt int64 `json:"created_at"`
+ UpdatedAt int64 `json:"updated_at"`
+ FinishedAt sql.NullInt64 `json:"finished_at"`
}
type Session struct {
@@ -15,10 +15,11 @@ INSERT INTO messages (
session_id,
role,
parts,
+ model,
created_at,
updated_at
) VALUES (
- ?, ?, ?, ?, strftime('%s', 'now'), strftime('%s', 'now')
+ ?, ?, ?, ?, ?, strftime('%s', 'now'), strftime('%s', 'now')
)
RETURNING *;
@@ -26,9 +27,11 @@ RETURNING *;
UPDATE messages
SET
parts = ?,
+ finished_at = ?,
updated_at = strftime('%s', 'now')
WHERE id = ?;
+
-- name: DeleteMessage :exec
DELETE FROM messages
WHERE id = ?;
@@ -2,6 +2,7 @@ package message
import (
"encoding/base64"
+ "time"
)
type MessageRole string
@@ -64,6 +65,7 @@ type ToolCall struct {
Name string `json:"name"`
Input string `json:"input"`
Type string `json:"type"`
+ Metadata any `json:"metadata"`
Finished bool `json:"finished"`
}
@@ -80,6 +82,7 @@ func (ToolResult) isPart() {}
type Finish struct {
Reason string `json:"reason"`
+ Time int64 `json:"time"`
}
func (Finish) isPart() {}
@@ -161,6 +164,15 @@ func (m *Message) IsFinished() bool {
return false
}
+func (m *Message) FinishPart() *Finish {
+ for _, part := range m.Parts {
+ if c, ok := part.(Finish); ok {
+ return &c
+ }
+ }
+ return nil
+}
+
func (m *Message) FinishReason() string {
for _, part := range m.Parts {
if c, ok := part.(Finish); ok {
@@ -232,7 +244,7 @@ func (m *Message) SetToolResults(tr []ToolResult) {
}
func (m *Message) AddFinish(reason string) {
- m.Parts = append(m.Parts, Finish{Reason: reason})
+ m.Parts = append(m.Parts, Finish{Reason: reason, Time: time.Now().Unix()})
}
func (m *Message) AddImageURL(url, detail string) {
@@ -2,17 +2,20 @@ package message
import (
"context"
+ "database/sql"
"encoding/json"
"fmt"
"github.com/google/uuid"
"github.com/kujtimiihoxha/termai/internal/db"
+ "github.com/kujtimiihoxha/termai/internal/llm/models"
"github.com/kujtimiihoxha/termai/internal/pubsub"
)
type CreateMessageParams struct {
Role MessageRole
Parts []ContentPart
+ Model models.ModelID
}
type Service interface {
@@ -68,6 +71,7 @@ func (s *service) Create(sessionID string, params CreateMessageParams) (Message,
SessionID: sessionID,
Role: string(params.Role),
Parts: string(partsJSON),
+ Model: sql.NullString{String: string(params.Model), Valid: true},
})
if err != nil {
return Message{}, err
@@ -101,9 +105,15 @@ func (s *service) Update(message Message) error {
if err != nil {
return err
}
+ finishedAt := sql.NullInt64{}
+ if f := message.FinishPart(); f != nil {
+ finishedAt.Int64 = f.Time
+ finishedAt.Valid = true
+ }
err = s.q.UpdateMessage(s.ctx, db.UpdateMessageParams{
- ID: message.ID,
- Parts: string(parts),
+ ID: message.ID,
+ Parts: string(parts),
+ FinishedAt: finishedAt,
})
if err != nil {
return err
@@ -0,0 +1,113 @@
+package chat
+
+import (
+ "fmt"
+
+ "github.com/charmbracelet/lipgloss"
+ "github.com/charmbracelet/x/ansi"
+ "github.com/kujtimiihoxha/termai/internal/config"
+ "github.com/kujtimiihoxha/termai/internal/session"
+ "github.com/kujtimiihoxha/termai/internal/tui/styles"
+ "github.com/kujtimiihoxha/termai/internal/version"
+)
+
+type SendMsg struct {
+ Text string
+}
+
+type SessionSelectedMsg = session.Session
+
+type SessionClearedMsg struct{}
+
+type AgentWorkingMsg bool
+
+type EditorFocusMsg bool
+
+func lspsConfigured(width int) string {
+ cfg := config.Get()
+ title := "LSP Configuration"
+ title = ansi.Truncate(title, width, "…")
+
+ lsps := styles.BaseStyle.Width(width).Foreground(styles.PrimaryColor).Bold(true).Render(title)
+
+ var lspViews []string
+ for name, lsp := range cfg.LSP {
+ lspName := styles.BaseStyle.Foreground(styles.Forground).Render(
+ fmt.Sprintf("• %s", name),
+ )
+ cmd := lsp.Command
+ cmd = ansi.Truncate(cmd, width-lipgloss.Width(lspName)-3, "…")
+ lspPath := styles.BaseStyle.Foreground(styles.ForgroundDim).Render(
+ fmt.Sprintf(" (%s)", cmd),
+ )
+ lspViews = append(lspViews,
+ styles.BaseStyle.
+ Width(width).
+ Render(
+ lipgloss.JoinHorizontal(
+ lipgloss.Left,
+ lspName,
+ lspPath,
+ ),
+ ),
+ )
+
+ }
+ return styles.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")
+
+ version := styles.BaseStyle.Foreground(styles.ForgroundDim).Render(version.Version)
+
+ return styles.BaseStyle.
+ Bold(true).
+ Width(width).
+ Render(
+ lipgloss.JoinHorizontal(
+ lipgloss.Left,
+ logo,
+ " ",
+ version,
+ ),
+ )
+}
+
+func repo(width int) string {
+ repo := "https://github.com/kujtimiihoxha/opencode"
+ return styles.BaseStyle.
+ Foreground(styles.ForgroundDim).
+ Width(width).
+ Render(repo)
+}
+
+func cwd(width int) string {
+ cwd := fmt.Sprintf("cwd: %s", config.WorkingDirectory())
+ return styles.BaseStyle.
+ Foreground(styles.ForgroundDim).
+ Width(width).
+ Render(cwd)
+}
+
+func header(width int) string {
+ header := lipgloss.JoinVertical(
+ lipgloss.Top,
+ logo(width),
+ repo(width),
+ "",
+ cwd(width),
+ )
+ return header
+}
@@ -7,10 +7,12 @@ import (
"github.com/charmbracelet/lipgloss"
"github.com/kujtimiihoxha/termai/internal/tui/layout"
"github.com/kujtimiihoxha/termai/internal/tui/styles"
+ "github.com/kujtimiihoxha/termai/internal/tui/util"
)
type editorCmp struct {
- textarea textarea.Model
+ textarea textarea.Model
+ agentWorking bool
}
type focusedEditorKeyMaps struct {
@@ -49,39 +51,51 @@ func (m *editorCmp) Init() tea.Cmd {
return textarea.Blink
}
+func (m *editorCmp) send() tea.Cmd {
+ if m.agentWorking {
+ return util.ReportWarn("Agent is working, please wait...")
+ }
+
+ value := m.textarea.Value()
+ m.textarea.Reset()
+ m.textarea.Blur()
+ if value == "" {
+ return nil
+ }
+ return tea.Batch(
+ util.CmdHandler(SendMsg{
+ Text: value,
+ }),
+ util.CmdHandler(AgentWorkingMsg(true)),
+ util.CmdHandler(EditorFocusMsg(false)),
+ )
+}
+
func (m *editorCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var cmd tea.Cmd
- if m.textarea.Focused() {
- switch msg := msg.(type) {
- case tea.KeyMsg:
- if key.Matches(msg, focusedKeyMaps.Send) {
- // TODO: send message
- m.textarea.Reset()
- m.textarea.Blur()
- return m, nil
- }
- if key.Matches(msg, focusedKeyMaps.Blur) {
- m.textarea.Blur()
- return m, nil
- }
- }
- m.textarea, cmd = m.textarea.Update(msg)
- return m, cmd
- }
switch msg := msg.(type) {
+ case AgentWorkingMsg:
+ m.agentWorking = bool(msg)
case tea.KeyMsg:
+ if key.Matches(msg, focusedKeyMaps.Send) {
+ return m, m.send()
+ }
if key.Matches(msg, bluredKeyMaps.Send) {
- // TODO: send message
- m.textarea.Reset()
- return m, nil
+ return m, m.send()
+ }
+ if key.Matches(msg, focusedKeyMaps.Blur) {
+ m.textarea.Blur()
+ return m, util.CmdHandler(EditorFocusMsg(false))
}
if key.Matches(msg, bluredKeyMaps.Focus) {
- m.textarea.Focus()
- return m, textarea.Blink
+ if !m.textarea.Focused() {
+ m.textarea.Focus()
+ return m, tea.Batch(textarea.Blink, util.CmdHandler(EditorFocusMsg(true)))
+ }
}
}
-
- return m, nil
+ m.textarea, cmd = m.textarea.Update(msg)
+ return m, cmd
}
func (m *editorCmp) View() string {
@@ -122,6 +136,7 @@ func NewEditorCmp() tea.Model {
ti.FocusedStyle.CursorLine = ti.FocusedStyle.CursorLine.Background(styles.Background)
ti.FocusedStyle.Placeholder = ti.FocusedStyle.Placeholder.Background(styles.Background)
ti.FocusedStyle.Text = ti.BlurredStyle.Text.Background(styles.Background)
+ ti.CharLimit = -1
ti.Focus()
return &editorCmp{
textarea: ti,
@@ -1,21 +1,344 @@
package chat
-import tea "github.com/charmbracelet/bubbletea"
+import (
+ "fmt"
+ "regexp"
+ "strconv"
+ "strings"
-type messagesCmp struct{}
+ "github.com/charmbracelet/bubbles/viewport"
+ tea "github.com/charmbracelet/bubbletea"
+ "github.com/charmbracelet/glamour"
+ "github.com/charmbracelet/lipgloss"
+ "github.com/kujtimiihoxha/termai/internal/app"
+ "github.com/kujtimiihoxha/termai/internal/message"
+ "github.com/kujtimiihoxha/termai/internal/pubsub"
+ "github.com/kujtimiihoxha/termai/internal/session"
+ "github.com/kujtimiihoxha/termai/internal/tui/styles"
+ "github.com/kujtimiihoxha/termai/internal/tui/util"
+)
+
+type uiMessage struct {
+ position int
+ height int
+ content string
+}
+
+type messagesCmp struct {
+ app *app.App
+ width, height int
+ writingMode bool
+ viewport viewport.Model
+ session session.Session
+ messages []message.Message
+ uiMessages []uiMessage
+ currentIndex int
+ renderer *glamour.TermRenderer
+ focusRenderer *glamour.TermRenderer
+ cachedContent map[string]string
+}
func (m *messagesCmp) Init() tea.Cmd {
- return nil
+ return m.viewport.Init()
+}
+
+var ansiEscape = regexp.MustCompile("\x1b\\[[0-9;]*m")
+
+func hexToBgSGR(hex string) (string, error) {
+ hex = strings.TrimPrefix(hex, "#")
+ if len(hex) != 6 {
+ return "", fmt.Errorf("invalid hex color: must be 6 hexadecimal digits")
+ }
+
+ // Parse RGB components in one block
+ rgb := make([]uint64, 3)
+ for i := 0; i < 3; i++ {
+ val, err := strconv.ParseUint(hex[i*2:i*2+2], 16, 8)
+ if err != nil {
+ return "", err
+ }
+ rgb[i] = val
+ }
+
+ return fmt.Sprintf("48;2;%d;%d;%d", rgb[0], rgb[1], rgb[2]), nil
+}
+
+func forceReplaceBackgroundColors(input string, newBg string) string {
+ return ansiEscape.ReplaceAllStringFunc(input, func(seq string) string {
+ // Extract content between "\x1b[" and "m"
+ content := seq[2 : len(seq)-1]
+ tokens := strings.Split(content, ";")
+ var newTokens []string
+
+ // Skip background color tokens
+ for i := 0; i < len(tokens); i++ {
+ if tokens[i] == "" {
+ continue
+ }
+
+ val, err := strconv.Atoi(tokens[i])
+ if err != nil {
+ newTokens = append(newTokens, tokens[i])
+ continue
+ }
+
+ // Skip background color tokens
+ if val == 48 {
+ // Skip "48;5;N" or "48;2;R;G;B" sequences
+ if i+1 < len(tokens) {
+ if nextVal, err := strconv.Atoi(tokens[i+1]); err == nil {
+ switch nextVal {
+ case 5:
+ i += 2 // Skip "5" and color index
+ case 2:
+ i += 4 // Skip "2" and RGB components
+ }
+ }
+ }
+ } else if (val < 40 || val > 47) && (val < 100 || val > 107) && val != 49 {
+ // Keep non-background tokens
+ newTokens = append(newTokens, tokens[i])
+ }
+ }
+
+ // Add new background if provided
+ if newBg != "" {
+ newTokens = append(newTokens, strings.Split(newBg, ";")...)
+ }
+
+ if len(newTokens) == 0 {
+ return ""
+ }
+
+ return "\x1b[" + strings.Join(newTokens, ";") + "m"
+ })
+}
+
+func (m *messagesCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
+ switch msg := msg.(type) {
+ case EditorFocusMsg:
+ m.writingMode = bool(msg)
+ case SessionSelectedMsg:
+ if msg.ID != m.session.ID {
+ cmd := m.SetSession(msg)
+ return m, cmd
+ }
+ return m, nil
+ case pubsub.Event[message.Message]:
+ if msg.Type == pubsub.CreatedEvent {
+ if msg.Payload.SessionID == m.session.ID {
+ // check if message exists
+ for _, v := range m.messages {
+ if v.ID == msg.Payload.ID {
+ return m, nil
+ }
+ }
+
+ m.messages = append(m.messages, msg.Payload)
+ m.renderView()
+ m.viewport.GotoBottom()
+ }
+ for _, v := range m.messages {
+ for _, c := range v.ToolCalls() {
+ // the message is being added to the session of a tool called
+ if c.ID == msg.Payload.SessionID {
+ m.renderView()
+ m.viewport.GotoBottom()
+ }
+ }
+ }
+ } else if msg.Type == pubsub.UpdatedEvent && msg.Payload.SessionID == m.session.ID {
+ for i, v := range m.messages {
+ if v.ID == msg.Payload.ID {
+ m.messages[i] = msg.Payload
+ delete(m.cachedContent, msg.Payload.ID)
+ m.renderView()
+ if i == len(m.messages)-1 {
+ m.viewport.GotoBottom()
+ }
+ break
+ }
+ }
+ }
+ }
+ u, cmd := m.viewport.Update(msg)
+ m.viewport = u
+ return m, cmd
}
-func (m *messagesCmp) Update(tea.Msg) (tea.Model, tea.Cmd) {
- return m, nil
+func (m *messagesCmp) renderUserMessage(inx int, msg message.Message) string {
+ if v, ok := m.cachedContent[msg.ID]; ok {
+ return v
+ }
+ style := styles.BaseStyle.
+ Width(m.width).
+ BorderLeft(true).
+ Foreground(styles.ForgroundDim).
+ BorderForeground(styles.ForgroundDim).
+ BorderStyle(lipgloss.ThickBorder())
+
+ renderer := m.renderer
+ if inx == m.currentIndex {
+ style = style.
+ Foreground(styles.Forground).
+ BorderForeground(styles.Blue).
+ BorderStyle(lipgloss.ThickBorder())
+ renderer = m.focusRenderer
+ }
+ c, _ := renderer.Render(msg.Content().String())
+ col, _ := hexToBgSGR(styles.Background.Dark)
+ rendered := style.Render(forceReplaceBackgroundColors(c, col))
+ m.cachedContent[msg.ID] = rendered
+ return rendered
+}
+
+func (m *messagesCmp) renderView() {
+ m.uiMessages = make([]uiMessage, 0)
+ pos := 0
+
+ for _, v := range m.messages {
+ content := ""
+ switch v.Role {
+ case message.User:
+ content = m.renderUserMessage(pos, v)
+ }
+ m.uiMessages = append(m.uiMessages, uiMessage{
+ position: pos,
+ height: lipgloss.Height(content),
+ content: content,
+ })
+ pos += lipgloss.Height(content) + 1 // + 1 for spacing
+ }
+
+ messages := make([]string, 0)
+ for _, v := range m.uiMessages {
+ messages = append(messages, v.content)
+ }
+ m.viewport.SetContent(
+ styles.BaseStyle.
+ Width(m.width).
+ Render(
+ lipgloss.JoinVertical(
+ lipgloss.Top,
+ messages...,
+ ),
+ ),
+ )
}
func (m *messagesCmp) View() string {
- return "Messages"
+ if len(m.messages) == 0 {
+ content := styles.BaseStyle.
+ Width(m.width).
+ Height(m.height - 1).
+ Render(
+ m.initialScreen(),
+ )
+
+ return styles.BaseStyle.
+ Width(m.width).
+ Render(
+ lipgloss.JoinVertical(
+ lipgloss.Top,
+ content,
+ m.help(),
+ ),
+ )
+ }
+
+ m.renderView()
+ return styles.BaseStyle.
+ Width(m.width).
+ Render(
+ lipgloss.JoinVertical(
+ lipgloss.Top,
+ m.viewport.View(),
+ m.help(),
+ ),
+ )
+}
+
+func (m *messagesCmp) help() string {
+ text := ""
+ if m.writingMode {
+ text = lipgloss.JoinHorizontal(
+ lipgloss.Left,
+ styles.BaseStyle.Foreground(styles.ForgroundDim).Bold(true).Render("press "),
+ styles.BaseStyle.Foreground(styles.Forground).Bold(true).Render("esc"),
+ styles.BaseStyle.Foreground(styles.ForgroundDim).Bold(true).Render(" to exit writing mode"),
+ )
+ } else {
+ text = lipgloss.JoinHorizontal(
+ lipgloss.Left,
+ styles.BaseStyle.Foreground(styles.ForgroundDim).Bold(true).Render("press "),
+ styles.BaseStyle.Foreground(styles.Forground).Bold(true).Render("i"),
+ styles.BaseStyle.Foreground(styles.ForgroundDim).Bold(true).Render(" to start writing"),
+ )
+ }
+
+ return styles.BaseStyle.
+ Width(m.width).
+ Render(text)
+}
+
+func (m *messagesCmp) initialScreen() string {
+ return styles.BaseStyle.Width(m.width).Render(
+ lipgloss.JoinVertical(
+ lipgloss.Top,
+ header(m.width),
+ "",
+ lspsConfigured(m.width),
+ ),
+ )
+}
+
+func (m *messagesCmp) SetSize(width, height int) {
+ m.width = width
+ m.height = height
+ m.viewport.Width = width
+ m.viewport.Height = height - 1
+ focusRenderer, _ := glamour.NewTermRenderer(
+ glamour.WithStyles(styles.MarkdownTheme(true)),
+ glamour.WithWordWrap(width-1),
+ )
+ renderer, _ := glamour.NewTermRenderer(
+ glamour.WithStyles(styles.MarkdownTheme(false)),
+ glamour.WithWordWrap(width-1),
+ )
+ m.focusRenderer = focusRenderer
+ m.renderer = renderer
+}
+
+func (m *messagesCmp) GetSize() (int, int) {
+ return m.width, m.height
+}
+
+func (m *messagesCmp) SetSession(session session.Session) tea.Cmd {
+ m.session = session
+ messages, err := m.app.Messages.List(session.ID)
+ if err != nil {
+ return util.ReportError(err)
+ }
+ m.messages = messages
+ m.messages = append(m.messages, m.messages[0])
+ return nil
}
-func NewMessagesCmp() tea.Model {
- return &messagesCmp{}
+func NewMessagesCmp(app *app.App) tea.Model {
+ focusRenderer, _ := glamour.NewTermRenderer(
+ glamour.WithStyles(styles.MarkdownTheme(true)),
+ glamour.WithWordWrap(80),
+ )
+ renderer, _ := glamour.NewTermRenderer(
+ glamour.WithStyles(styles.MarkdownTheme(false)),
+ glamour.WithWordWrap(80),
+ )
+ return &messagesCmp{
+ app: app,
+ writingMode: true,
+ cachedContent: make(map[string]string),
+ viewport: viewport.New(0, 0),
+ focusRenderer: focusRenderer,
+ renderer: renderer,
+ }
}
@@ -5,40 +5,43 @@ import (
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
- "github.com/kujtimiihoxha/termai/internal/config"
+ "github.com/kujtimiihoxha/termai/internal/session"
"github.com/kujtimiihoxha/termai/internal/tui/styles"
- "github.com/kujtimiihoxha/termai/internal/version"
)
type sidebarCmp struct {
width, height int
+ session session.Session
}
func (m *sidebarCmp) Init() tea.Cmd {
return nil
}
-func (m *sidebarCmp) Update(tea.Msg) (tea.Model, tea.Cmd) {
+func (m *sidebarCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return m, nil
}
func (m *sidebarCmp) View() string {
- return styles.BaseStyle.Width(m.width).Render(
- lipgloss.JoinVertical(
- lipgloss.Top,
- m.header(),
- " ",
- m.session(),
- " ",
- m.modifiedFiles(),
- " ",
- m.lspsConfigured(),
- ),
- )
+ return styles.BaseStyle.
+ Width(m.width).
+ Height(m.height - 1).
+ Render(
+ lipgloss.JoinVertical(
+ lipgloss.Top,
+ header(m.width),
+ " ",
+ m.sessionSection(),
+ " ",
+ m.modifiedFiles(),
+ " ",
+ lspsConfigured(m.width),
+ ),
+ )
}
-func (m *sidebarCmp) session() string {
- sessionKey := styles.BaseStyle.Foreground(styles.PrimaryColor).Render("Session")
+func (m *sidebarCmp) sessionSection() string {
+ sessionKey := styles.BaseStyle.Foreground(styles.PrimaryColor).Bold(true).Render("Session")
sessionValue := styles.BaseStyle.
Foreground(styles.Forground).
Width(m.width - lipgloss.Width(sessionKey)).
@@ -53,11 +56,11 @@ func (m *sidebarCmp) session() string {
func (m *sidebarCmp) modifiedFile(filePath string, additions, removals int) string {
stats := ""
if additions > 0 && removals > 0 {
- stats = styles.BaseStyle.Foreground(styles.ForgroundDim).Render(fmt.Sprintf("%d additions and %d removals", additions, removals))
+ stats = styles.BaseStyle.Foreground(styles.ForgroundDim).Render(fmt.Sprintf(" %d additions and %d removals", additions, removals))
} else if additions > 0 {
- stats = styles.BaseStyle.Foreground(styles.ForgroundDim).Render(fmt.Sprintf("%d additions", additions))
+ stats = styles.BaseStyle.Foreground(styles.ForgroundDim).Render(fmt.Sprintf(" %d additions", additions))
} else if removals > 0 {
- stats = styles.BaseStyle.Foreground(styles.ForgroundDim).Render(fmt.Sprintf("%d removals", removals))
+ stats = styles.BaseStyle.Foreground(styles.ForgroundDim).Render(fmt.Sprintf(" %d removals", removals))
}
filePathStr := styles.BaseStyle.Foreground(styles.Forground).Render(filePath)
@@ -67,60 +70,13 @@ func (m *sidebarCmp) modifiedFile(filePath string, additions, removals int) stri
lipgloss.JoinHorizontal(
lipgloss.Left,
filePathStr,
- " ",
stats,
),
)
}
-func (m *sidebarCmp) lspsConfigured() string {
- lsps := styles.BaseStyle.Width(m.width).Foreground(styles.PrimaryColor).Render("LSP Configuration:")
- lspsConfigured := []struct {
- name string
- path string
- }{
- {"golsp", "path/to/lsp1"},
- {"vtsls", "path/to/lsp2"},
- }
-
- var lspViews []string
- for _, lsp := range lspsConfigured {
- lspName := styles.BaseStyle.Foreground(styles.Forground).Render(
- fmt.Sprintf("• %s", lsp.name),
- )
- lspPath := styles.BaseStyle.Foreground(styles.ForgroundDim).Render(
- fmt.Sprintf("(%s)", lsp.path),
- )
- lspViews = append(lspViews,
- styles.BaseStyle.
- Width(m.width).
- Render(
- lipgloss.JoinHorizontal(
- lipgloss.Left,
- lspName,
- " ",
- lspPath,
- ),
- ),
- )
-
- }
- return styles.BaseStyle.
- Width(m.width).
- Render(
- lipgloss.JoinVertical(
- lipgloss.Left,
- lsps,
- lipgloss.JoinVertical(
- lipgloss.Left,
- lspViews...,
- ),
- ),
- )
-}
-
func (m *sidebarCmp) modifiedFiles() string {
- modifiedFiles := styles.BaseStyle.Width(m.width).Foreground(styles.PrimaryColor).Render("Modified Files:")
+ modifiedFiles := styles.BaseStyle.Width(m.width).Foreground(styles.PrimaryColor).Bold(true).Render("Modified Files:")
files := []struct {
path string
additions int
@@ -149,41 +105,6 @@ func (m *sidebarCmp) modifiedFiles() string {
)
}
-func (m *sidebarCmp) logo() string {
- logo := fmt.Sprintf("%s %s", styles.OpenCodeIcon, "OpenCode")
-
- version := styles.BaseStyle.Foreground(styles.ForgroundDim).Render(version.Version)
-
- return styles.BaseStyle.
- Bold(true).
- Width(m.width).
- Render(
- lipgloss.JoinHorizontal(
- lipgloss.Left,
- logo,
- " ",
- version,
- ),
- )
-}
-
-func (m *sidebarCmp) header() string {
- header := lipgloss.JoinVertical(
- lipgloss.Top,
- m.logo(),
- m.cwd(),
- )
- return header
-}
-
-func (m *sidebarCmp) cwd() string {
- cwd := fmt.Sprintf("cwd: %s", config.WorkingDirectory())
- return styles.BaseStyle.
- Foreground(styles.ForgroundDim).
- Width(m.width).
- Render(cwd)
-}
-
func (m *sidebarCmp) SetSize(width, height int) {
m.width = width
m.height = height
@@ -193,6 +114,8 @@ func (m *sidebarCmp) GetSize() (int, int) {
return m.width, m.height
}
-func NewSidebarCmp() tea.Model {
- return &sidebarCmp{}
+func NewSidebarCmp(session session.Session) tea.Model {
+ return &sidebarCmp{
+ session: session,
+ }
}
@@ -1,287 +0,0 @@
-package core
-
-import (
- "github.com/charmbracelet/bubbles/key"
- tea "github.com/charmbracelet/bubbletea"
- "github.com/charmbracelet/lipgloss"
- "github.com/kujtimiihoxha/termai/internal/tui/styles"
-)
-
-// ButtonKeyMap defines key bindings for the button component
-type ButtonKeyMap struct {
- Enter key.Binding
-}
-
-// DefaultButtonKeyMap returns default key bindings for the button
-func DefaultButtonKeyMap() ButtonKeyMap {
- return ButtonKeyMap{
- Enter: key.NewBinding(
- key.WithKeys("enter"),
- key.WithHelp("enter", "select"),
- ),
- }
-}
-
-// ShortHelp returns keybinding help
-func (k ButtonKeyMap) ShortHelp() []key.Binding {
- return []key.Binding{k.Enter}
-}
-
-// FullHelp returns full help info for keybindings
-func (k ButtonKeyMap) FullHelp() [][]key.Binding {
- return [][]key.Binding{
- {k.Enter},
- }
-}
-
-// ButtonState represents the state of a button
-type ButtonState int
-
-const (
- // ButtonNormal is the default state
- ButtonNormal ButtonState = iota
- // ButtonHovered is when the button is focused/hovered
- ButtonHovered
- // ButtonPressed is when the button is being pressed
- ButtonPressed
- // ButtonDisabled is when the button is disabled
- ButtonDisabled
-)
-
-// ButtonVariant defines the visual style variant of a button
-type ButtonVariant int
-
-const (
- // ButtonPrimary uses primary color styling
- ButtonPrimary ButtonVariant = iota
- // ButtonSecondary uses secondary color styling
- ButtonSecondary
- // ButtonDanger uses danger/error color styling
- ButtonDanger
- // ButtonWarning uses warning color styling
- ButtonWarning
- // ButtonNeutral uses neutral color styling
- ButtonNeutral
-)
-
-// ButtonMsg is sent when a button is clicked
-type ButtonMsg struct {
- ID string
- Payload any
-}
-
-// ButtonCmp represents a clickable button component
-type ButtonCmp struct {
- id string
- label string
- width int
- height int
- state ButtonState
- variant ButtonVariant
- keyMap ButtonKeyMap
- payload any
- style lipgloss.Style
- hoverStyle lipgloss.Style
-}
-
-// NewButtonCmp creates a new button component
-func NewButtonCmp(id, label string) *ButtonCmp {
- b := &ButtonCmp{
- id: id,
- label: label,
- state: ButtonNormal,
- variant: ButtonPrimary,
- keyMap: DefaultButtonKeyMap(),
- width: len(label) + 4, // add some padding
- height: 1,
- }
- b.updateStyles()
- return b
-}
-
-// WithVariant sets the button variant
-func (b *ButtonCmp) WithVariant(variant ButtonVariant) *ButtonCmp {
- b.variant = variant
- b.updateStyles()
- return b
-}
-
-// WithPayload sets the payload sent with button events
-func (b *ButtonCmp) WithPayload(payload any) *ButtonCmp {
- b.payload = payload
- return b
-}
-
-// WithWidth sets a custom width
-func (b *ButtonCmp) WithWidth(width int) *ButtonCmp {
- b.width = width
- b.updateStyles()
- return b
-}
-
-// updateStyles recalculates styles based on current state and variant
-func (b *ButtonCmp) updateStyles() {
- // Base styles
- b.style = styles.Regular.
- Padding(0, 1).
- Width(b.width).
- Align(lipgloss.Center).
- BorderStyle(lipgloss.RoundedBorder())
-
- b.hoverStyle = b.style.
- Bold(true)
-
- // Variant-specific styling
- switch b.variant {
- case ButtonPrimary:
- b.style = b.style.
- Foreground(styles.Base).
- Background(styles.Primary).
- BorderForeground(styles.Primary)
-
- b.hoverStyle = b.hoverStyle.
- Foreground(styles.Base).
- Background(styles.Blue).
- BorderForeground(styles.Blue)
-
- case ButtonSecondary:
- b.style = b.style.
- Foreground(styles.Base).
- Background(styles.Secondary).
- BorderForeground(styles.Secondary)
-
- b.hoverStyle = b.hoverStyle.
- Foreground(styles.Base).
- Background(styles.Mauve).
- BorderForeground(styles.Mauve)
-
- case ButtonDanger:
- b.style = b.style.
- Foreground(styles.Base).
- Background(styles.Error).
- BorderForeground(styles.Error)
-
- b.hoverStyle = b.hoverStyle.
- Foreground(styles.Base).
- Background(styles.Red).
- BorderForeground(styles.Red)
-
- case ButtonWarning:
- b.style = b.style.
- Foreground(styles.Text).
- Background(styles.Warning).
- BorderForeground(styles.Warning)
-
- b.hoverStyle = b.hoverStyle.
- Foreground(styles.Text).
- Background(styles.Peach).
- BorderForeground(styles.Peach)
-
- case ButtonNeutral:
- b.style = b.style.
- Foreground(styles.Text).
- Background(styles.Grey).
- BorderForeground(styles.Grey)
-
- b.hoverStyle = b.hoverStyle.
- Foreground(styles.Text).
- Background(styles.DarkGrey).
- BorderForeground(styles.DarkGrey)
- }
-
- // Disabled style override
- if b.state == ButtonDisabled {
- b.style = b.style.
- Foreground(styles.SubText0).
- Background(styles.LightGrey).
- BorderForeground(styles.LightGrey)
- }
-}
-
-// SetSize sets the button size
-func (b *ButtonCmp) SetSize(width, height int) {
- b.width = width
- b.height = height
- b.updateStyles()
-}
-
-// Focus sets the button to focused state
-func (b *ButtonCmp) Focus() tea.Cmd {
- if b.state != ButtonDisabled {
- b.state = ButtonHovered
- }
- return nil
-}
-
-// Blur sets the button to normal state
-func (b *ButtonCmp) Blur() tea.Cmd {
- if b.state != ButtonDisabled {
- b.state = ButtonNormal
- }
- return nil
-}
-
-// Disable sets the button to disabled state
-func (b *ButtonCmp) Disable() {
- b.state = ButtonDisabled
- b.updateStyles()
-}
-
-// Enable enables the button if disabled
-func (b *ButtonCmp) Enable() {
- if b.state == ButtonDisabled {
- b.state = ButtonNormal
- b.updateStyles()
- }
-}
-
-// IsDisabled returns whether the button is disabled
-func (b *ButtonCmp) IsDisabled() bool {
- return b.state == ButtonDisabled
-}
-
-// IsFocused returns whether the button is focused
-func (b *ButtonCmp) IsFocused() bool {
- return b.state == ButtonHovered
-}
-
-// Init initializes the button
-func (b *ButtonCmp) Init() tea.Cmd {
- return nil
-}
-
-// Update handles messages and user input
-func (b *ButtonCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
- // Skip updates if disabled
- if b.state == ButtonDisabled {
- return b, nil
- }
-
- switch msg := msg.(type) {
- case tea.KeyMsg:
- // Handle key presses when focused
- if b.state == ButtonHovered {
- switch {
- case key.Matches(msg, b.keyMap.Enter):
- b.state = ButtonPressed
- return b, func() tea.Msg {
- return ButtonMsg{
- ID: b.id,
- Payload: b.payload,
- }
- }
- }
- }
- }
-
- return b, nil
-}
-
-// View renders the button
-func (b *ButtonCmp) View() string {
- if b.state == ButtonHovered || b.state == ButtonPressed {
- return b.hoverStyle.Render(b.label)
- }
- return b.style.Render(b.label)
-}
-
@@ -10,6 +10,9 @@ import (
type SplitPaneLayout interface {
tea.Model
Sizeable
+ SetLeftPanel(panel Container)
+ SetRightPanel(panel Container)
+ SetBottomPanel(panel Container)
}
type splitPaneLayout struct {
@@ -160,6 +163,27 @@ func (s *splitPaneLayout) GetSize() (int, int) {
return s.width, s.height
}
+func (s *splitPaneLayout) SetLeftPanel(panel Container) {
+ s.leftPanel = panel
+ if s.width > 0 && s.height > 0 {
+ s.SetSize(s.width, s.height)
+ }
+}
+
+func (s *splitPaneLayout) SetRightPanel(panel Container) {
+ s.rightPanel = panel
+ if s.width > 0 && s.height > 0 {
+ s.SetSize(s.width, s.height)
+ }
+}
+
+func (s *splitPaneLayout) SetBottomPanel(panel Container) {
+ s.bottomPanel = panel
+ if s.width > 0 && s.height > 0 {
+ s.SetSize(s.width, s.height)
+ }
+}
+
func (s *splitPaneLayout) BindingKeys() []key.Binding {
keys := []key.Binding{}
if s.leftPanel != nil {
@@ -3,28 +3,100 @@ package page
import (
tea "github.com/charmbracelet/bubbletea"
"github.com/kujtimiihoxha/termai/internal/app"
+ "github.com/kujtimiihoxha/termai/internal/message"
+ "github.com/kujtimiihoxha/termai/internal/session"
"github.com/kujtimiihoxha/termai/internal/tui/components/chat"
"github.com/kujtimiihoxha/termai/internal/tui/layout"
+ "github.com/kujtimiihoxha/termai/internal/tui/util"
)
var ChatPage PageID = "chat"
-func NewChatPage(app *app.App) tea.Model {
- messagesContainer := layout.NewContainer(
- chat.NewMessagesCmp(),
+type chatPage struct {
+ app *app.App
+ layout layout.SplitPaneLayout
+ session session.Session
+}
+
+func (p *chatPage) Init() tea.Cmd {
+ return p.layout.Init()
+}
+
+func (p *chatPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
+ switch msg := msg.(type) {
+ case tea.WindowSizeMsg:
+ p.layout.SetSize(msg.Width, msg.Height)
+ case chat.SendMsg:
+ cmd := p.sendMessage(msg.Text)
+ if cmd != nil {
+ return p, cmd
+ }
+ }
+ u, cmd := p.layout.Update(msg)
+ p.layout = u.(layout.SplitPaneLayout)
+ if cmd != nil {
+ return p, cmd
+ }
+ return p, nil
+}
+
+func (p *chatPage) setSidebar() tea.Cmd {
+ sidebarContainer := layout.NewContainer(
+ chat.NewSidebarCmp(p.session),
layout.WithPadding(1, 1, 1, 1),
)
- sidebarContainer := layout.NewContainer(
- chat.NewSidebarCmp(),
+ p.layout.SetRightPanel(sidebarContainer)
+ width, height := p.layout.GetSize()
+ p.layout.SetSize(width, height)
+ return sidebarContainer.Init()
+}
+
+func (p *chatPage) sendMessage(text string) tea.Cmd {
+ var cmds []tea.Cmd
+ if p.session.ID == "" {
+ session, err := p.app.Sessions.Create("New Session")
+ if err != nil {
+ return util.ReportError(err)
+ }
+
+ p.session = session
+ cmd := p.setSidebar()
+ if cmd != nil {
+ cmds = append(cmds, cmd)
+ }
+ cmds = append(cmds, util.CmdHandler(chat.SessionSelectedMsg(session)))
+ }
+ // TODO: actually call agent
+ p.app.Messages.Create(p.session.ID, message.CreateMessageParams{
+ Role: message.User,
+ Parts: []message.ContentPart{
+ message.TextContent{
+ Text: text,
+ },
+ },
+ })
+ return tea.Batch(cmds...)
+}
+
+func (p *chatPage) View() string {
+ return p.layout.View()
+}
+
+func NewChatPage(app *app.App) tea.Model {
+ messagesContainer := layout.NewContainer(
+ chat.NewMessagesCmp(app),
layout.WithPadding(1, 1, 1, 1),
)
+
editorContainer := layout.NewContainer(
chat.NewEditorCmp(),
layout.WithBorder(true, false, false, false),
)
- return layout.NewSplitPane(
- layout.WithRightPanel(sidebarContainer),
- layout.WithLeftPanel(messagesContainer),
- layout.WithBottomPanel(editorContainer),
- )
+ return &chatPage{
+ app: app,
+ layout: layout.NewSplitPane(
+ layout.WithLeftPanel(messagesContainer),
+ layout.WithBottomPanel(editorContainer),
+ ),
+ }
}
@@ -36,12 +36,13 @@ var catppuccinDark = ansi.StyleConfig{
Italic: boolPtr(true),
Prefix: "┃ ",
},
- Indent: uintPtr(1),
- Margin: uintPtr(defaultMargin),
+ Indent: uintPtr(1),
+ IndentToken: stringPtr(BaseStyle.Render(" ")),
},
List: ansi.StyleList{
LevelIndent: defaultMargin,
StyleBlock: ansi.StyleBlock{
+ IndentToken: stringPtr(BaseStyle.Render(" ")),
StylePrimitive: ansi.StylePrimitive{
Color: stringPtr(dark.Text().Hex),
},
@@ -496,3 +497,444 @@ var catppuccinLight = ansi.StyleConfig{
Color: stringPtr(light.Sapphire().Hex),
},
}
+
+func MarkdownTheme(focused bool) ansi.StyleConfig {
+ if !focused {
+ return ASCIIStyleConfig
+ } else {
+ return DraculaStyleConfig
+ }
+}
+
+const (
+ defaultListIndent = 2
+ defaultListLevelIndent = 4
+)
+
+var ASCIIStyleConfig = ansi.StyleConfig{
+ Document: ansi.StyleBlock{
+ StylePrimitive: ansi.StylePrimitive{
+ BackgroundColor: stringPtr(Background.Dark),
+ },
+ Indent: uintPtr(1),
+ IndentToken: stringPtr(BaseStyle.Render(" ")),
+ },
+ BlockQuote: ansi.StyleBlock{
+ StylePrimitive: ansi.StylePrimitive{
+ BackgroundColor: stringPtr(Background.Dark),
+ },
+ Indent: uintPtr(1),
+ IndentToken: stringPtr("| "),
+ },
+ Paragraph: ansi.StyleBlock{
+ StylePrimitive: ansi.StylePrimitive{
+ BackgroundColor: stringPtr(Background.Dark),
+ },
+ },
+ List: ansi.StyleList{
+ StyleBlock: ansi.StyleBlock{
+ IndentToken: stringPtr(BaseStyle.Render(" ")),
+ StylePrimitive: ansi.StylePrimitive{
+ BackgroundColor: stringPtr(Background.Dark),
+ },
+ },
+ LevelIndent: defaultListLevelIndent,
+ },
+ Heading: ansi.StyleBlock{
+ StylePrimitive: ansi.StylePrimitive{
+ BackgroundColor: stringPtr(Background.Dark),
+ BlockSuffix: "\n",
+ },
+ },
+ H1: ansi.StyleBlock{
+ StylePrimitive: ansi.StylePrimitive{
+ BackgroundColor: stringPtr(Background.Dark),
+ Prefix: "# ",
+ },
+ },
+ H2: ansi.StyleBlock{
+ StylePrimitive: ansi.StylePrimitive{
+ BackgroundColor: stringPtr(Background.Dark),
+ Prefix: "## ",
+ },
+ },
+ H3: ansi.StyleBlock{
+ StylePrimitive: ansi.StylePrimitive{
+ BackgroundColor: stringPtr(Background.Dark),
+ Prefix: "### ",
+ },
+ },
+ H4: ansi.StyleBlock{
+ StylePrimitive: ansi.StylePrimitive{
+ BackgroundColor: stringPtr(Background.Dark),
+ Prefix: "#### ",
+ },
+ },
+ H5: ansi.StyleBlock{
+ StylePrimitive: ansi.StylePrimitive{
+ BackgroundColor: stringPtr(Background.Dark),
+ Prefix: "##### ",
+ },
+ },
+ H6: ansi.StyleBlock{
+ StylePrimitive: ansi.StylePrimitive{
+ BackgroundColor: stringPtr(Background.Dark),
+ Prefix: "###### ",
+ },
+ },
+ Strikethrough: ansi.StylePrimitive{
+ BackgroundColor: stringPtr(Background.Dark),
+ BlockPrefix: "~~",
+ BlockSuffix: "~~",
+ },
+ Emph: ansi.StylePrimitive{
+ BackgroundColor: stringPtr(Background.Dark),
+ BlockPrefix: "*",
+ BlockSuffix: "*",
+ },
+ Strong: ansi.StylePrimitive{
+ BackgroundColor: stringPtr(Background.Dark),
+ BlockPrefix: "**",
+ BlockSuffix: "**",
+ },
+ HorizontalRule: ansi.StylePrimitive{
+ BackgroundColor: stringPtr(Background.Dark),
+ Format: "\n--------\n",
+ },
+ Item: ansi.StylePrimitive{
+ BlockPrefix: "• ",
+ BackgroundColor: stringPtr(Background.Dark),
+ },
+ Enumeration: ansi.StylePrimitive{
+ BlockPrefix: ". ",
+ BackgroundColor: stringPtr(Background.Dark),
+ },
+ Task: ansi.StyleTask{
+ Ticked: "[x] ",
+ Unticked: "[ ] ",
+ StylePrimitive: ansi.StylePrimitive{
+ BackgroundColor: stringPtr(Background.Dark),
+ },
+ },
+ ImageText: ansi.StylePrimitive{
+ BackgroundColor: stringPtr(Background.Dark),
+ Format: "Image: {{.text}} →",
+ },
+ Code: ansi.StyleBlock{
+ StylePrimitive: ansi.StylePrimitive{
+ BlockPrefix: "`",
+ BlockSuffix: "`",
+ BackgroundColor: stringPtr(Background.Dark),
+ },
+ },
+ CodeBlock: ansi.StyleCodeBlock{
+ StyleBlock: ansi.StyleBlock{
+ StylePrimitive: ansi.StylePrimitive{
+ BackgroundColor: stringPtr(Background.Dark),
+ },
+ Margin: uintPtr(defaultMargin),
+ },
+ },
+ Table: ansi.StyleTable{
+ StyleBlock: ansi.StyleBlock{
+ StylePrimitive: ansi.StylePrimitive{
+ BackgroundColor: stringPtr(Background.Dark),
+ },
+ IndentToken: stringPtr(BaseStyle.Render(" ")),
+ },
+ CenterSeparator: stringPtr("|"),
+ ColumnSeparator: stringPtr("|"),
+ RowSeparator: stringPtr("-"),
+ },
+ DefinitionDescription: ansi.StylePrimitive{
+ BackgroundColor: stringPtr(Background.Dark),
+ BlockPrefix: "\n* ",
+ },
+}
+
+var DraculaStyleConfig = ansi.StyleConfig{
+ Document: ansi.StyleBlock{
+ StylePrimitive: ansi.StylePrimitive{
+ Color: stringPtr(Forground.Dark),
+ BackgroundColor: stringPtr(Background.Dark),
+ },
+ Indent: uintPtr(defaultMargin),
+ IndentToken: stringPtr(BaseStyle.Render(" ")),
+ },
+ BlockQuote: ansi.StyleBlock{
+ StylePrimitive: ansi.StylePrimitive{
+ Color: stringPtr("#f1fa8c"),
+ Italic: boolPtr(true),
+ BackgroundColor: stringPtr(Background.Dark),
+ },
+ Indent: uintPtr(defaultMargin),
+ IndentToken: stringPtr(BaseStyle.Render(" ")),
+ },
+ Paragraph: ansi.StyleBlock{
+ StylePrimitive: ansi.StylePrimitive{
+ BackgroundColor: stringPtr(Background.Dark),
+ },
+ },
+ List: ansi.StyleList{
+ LevelIndent: defaultMargin,
+ StyleBlock: ansi.StyleBlock{
+ IndentToken: stringPtr(BaseStyle.Render(" ")),
+ StylePrimitive: ansi.StylePrimitive{
+ Color: stringPtr(Forground.Dark),
+ BackgroundColor: stringPtr(Background.Dark),
+ },
+ },
+ },
+ Heading: ansi.StyleBlock{
+ StylePrimitive: ansi.StylePrimitive{
+ BlockSuffix: "\n",
+ Color: stringPtr("#bd93f9"),
+ Bold: boolPtr(true),
+ BackgroundColor: stringPtr(Background.Dark),
+ },
+ },
+ H1: ansi.StyleBlock{
+ StylePrimitive: ansi.StylePrimitive{
+ Prefix: "# ",
+ BackgroundColor: stringPtr(Background.Dark),
+ },
+ },
+ H2: ansi.StyleBlock{
+ StylePrimitive: ansi.StylePrimitive{
+ Prefix: "## ",
+ BackgroundColor: stringPtr(Background.Dark),
+ },
+ },
+ H3: ansi.StyleBlock{
+ StylePrimitive: ansi.StylePrimitive{
+ Prefix: "### ",
+ BackgroundColor: stringPtr(Background.Dark),
+ },
+ },
+ H4: ansi.StyleBlock{
+ StylePrimitive: ansi.StylePrimitive{
+ Prefix: "#### ",
+ BackgroundColor: stringPtr(Background.Dark),
+ },
+ },
+ H5: ansi.StyleBlock{
+ StylePrimitive: ansi.StylePrimitive{
+ Prefix: "##### ",
+ BackgroundColor: stringPtr(Background.Dark),
+ },
+ },
+ H6: ansi.StyleBlock{
+ StylePrimitive: ansi.StylePrimitive{
+ Prefix: "###### ",
+ BackgroundColor: stringPtr(Background.Dark),
+ },
+ },
+ Strikethrough: ansi.StylePrimitive{
+ CrossedOut: boolPtr(true),
+ BackgroundColor: stringPtr(Background.Dark),
+ },
+ Emph: ansi.StylePrimitive{
+ Color: stringPtr("#f1fa8c"),
+ Italic: boolPtr(true),
+ BackgroundColor: stringPtr(Background.Dark),
+ },
+ Strong: ansi.StylePrimitive{
+ Bold: boolPtr(true),
+ Color: stringPtr("#ffb86c"),
+ BackgroundColor: stringPtr(Background.Dark),
+ },
+ HorizontalRule: ansi.StylePrimitive{
+ Color: stringPtr("#6272A4"),
+ Format: "\n--------\n",
+ BackgroundColor: stringPtr(Background.Dark),
+ },
+ Item: ansi.StylePrimitive{
+ BlockPrefix: "• ",
+ BackgroundColor: stringPtr(Background.Dark),
+ },
+ Enumeration: ansi.StylePrimitive{
+ BlockPrefix: ". ",
+ Color: stringPtr("#8be9fd"),
+ BackgroundColor: stringPtr(Background.Dark),
+ },
+ Task: ansi.StyleTask{
+ StylePrimitive: ansi.StylePrimitive{
+ BackgroundColor: stringPtr(Background.Dark),
+ },
+ Ticked: "[✓] ",
+ Unticked: "[ ] ",
+ },
+ Link: ansi.StylePrimitive{
+ Color: stringPtr("#8be9fd"),
+ Underline: boolPtr(true),
+ BackgroundColor: stringPtr(Background.Dark),
+ },
+ LinkText: ansi.StylePrimitive{
+ Color: stringPtr("#ff79c6"),
+ BackgroundColor: stringPtr(Background.Dark),
+ },
+ Image: ansi.StylePrimitive{
+ Color: stringPtr("#8be9fd"),
+ Underline: boolPtr(true),
+ BackgroundColor: stringPtr(Background.Dark),
+ },
+ ImageText: ansi.StylePrimitive{
+ Color: stringPtr("#ff79c6"),
+ Format: "Image: {{.text}} →",
+ BackgroundColor: stringPtr(Background.Dark),
+ },
+ Code: ansi.StyleBlock{
+ StylePrimitive: ansi.StylePrimitive{
+ Color: stringPtr("#50fa7b"),
+ BackgroundColor: stringPtr(Background.Dark),
+ },
+ },
+ Text: ansi.StylePrimitive{
+ BackgroundColor: stringPtr(Background.Dark),
+ },
+ DefinitionList: ansi.StyleBlock{},
+ CodeBlock: ansi.StyleCodeBlock{
+ StyleBlock: ansi.StyleBlock{
+ StylePrimitive: ansi.StylePrimitive{
+ Color: stringPtr("#ffb86c"),
+ BackgroundColor: stringPtr(Background.Dark),
+ },
+ Margin: uintPtr(defaultMargin),
+ },
+ Chroma: &ansi.Chroma{
+ NameOther: ansi.StylePrimitive{
+ BackgroundColor: stringPtr(Background.Dark),
+ },
+ Literal: ansi.StylePrimitive{
+ BackgroundColor: stringPtr(Background.Dark),
+ },
+ NameException: ansi.StylePrimitive{
+ BackgroundColor: stringPtr(Background.Dark),
+ },
+ LiteralDate: ansi.StylePrimitive{
+ BackgroundColor: stringPtr(Background.Dark),
+ },
+ Text: ansi.StylePrimitive{
+ Color: stringPtr(Forground.Dark),
+ BackgroundColor: stringPtr(Background.Dark),
+ },
+ Error: ansi.StylePrimitive{
+ Color: stringPtr("#f8f8f2"),
+ BackgroundColor: stringPtr("#ff5555"),
+ },
+ Comment: ansi.StylePrimitive{
+ Color: stringPtr("#6272A4"),
+ BackgroundColor: stringPtr(Background.Dark),
+ },
+ CommentPreproc: ansi.StylePrimitive{
+ Color: stringPtr("#ff79c6"),
+ BackgroundColor: stringPtr(Background.Dark),
+ },
+ Keyword: ansi.StylePrimitive{
+ Color: stringPtr("#ff79c6"),
+ BackgroundColor: stringPtr(Background.Dark),
+ },
+ KeywordReserved: ansi.StylePrimitive{
+ Color: stringPtr("#ff79c6"),
+ BackgroundColor: stringPtr(Background.Dark),
+ },
+ KeywordNamespace: ansi.StylePrimitive{
+ Color: stringPtr("#ff79c6"),
+ BackgroundColor: stringPtr(Background.Dark),
+ },
+ KeywordType: ansi.StylePrimitive{
+ Color: stringPtr("#8be9fd"),
+ BackgroundColor: stringPtr(Background.Dark),
+ },
+ Operator: ansi.StylePrimitive{
+ Color: stringPtr("#ff79c6"),
+ BackgroundColor: stringPtr(Background.Dark),
+ },
+ Punctuation: ansi.StylePrimitive{
+ Color: stringPtr(Forground.Dark),
+ BackgroundColor: stringPtr(Background.Dark),
+ },
+ Name: ansi.StylePrimitive{
+ Color: stringPtr("#8be9fd"),
+ BackgroundColor: stringPtr(Background.Dark),
+ },
+ NameBuiltin: ansi.StylePrimitive{
+ Color: stringPtr("#8be9fd"),
+ BackgroundColor: stringPtr(Background.Dark),
+ },
+ NameTag: ansi.StylePrimitive{
+ Color: stringPtr("#ff79c6"),
+ BackgroundColor: stringPtr(Background.Dark),
+ },
+ NameAttribute: ansi.StylePrimitive{
+ Color: stringPtr("#50fa7b"),
+ BackgroundColor: stringPtr(Background.Dark),
+ },
+ NameClass: ansi.StylePrimitive{
+ Color: stringPtr("#8be9fd"),
+ BackgroundColor: stringPtr(Background.Dark),
+ },
+ NameConstant: ansi.StylePrimitive{
+ Color: stringPtr("#bd93f9"),
+ BackgroundColor: stringPtr(Background.Dark),
+ },
+ NameDecorator: ansi.StylePrimitive{
+ Color: stringPtr("#50fa7b"),
+ BackgroundColor: stringPtr(Background.Dark),
+ },
+ NameFunction: ansi.StylePrimitive{
+ Color: stringPtr("#50fa7b"),
+ BackgroundColor: stringPtr(Background.Dark),
+ },
+ LiteralNumber: ansi.StylePrimitive{
+ Color: stringPtr("#6EEFC0"),
+ BackgroundColor: stringPtr(Background.Dark),
+ },
+ LiteralString: ansi.StylePrimitive{
+ Color: stringPtr("#f1fa8c"),
+ BackgroundColor: stringPtr(Background.Dark),
+ },
+ LiteralStringEscape: ansi.StylePrimitive{
+ Color: stringPtr("#ff79c6"),
+ BackgroundColor: stringPtr(Background.Dark),
+ },
+ GenericDeleted: ansi.StylePrimitive{
+ Color: stringPtr("#ff5555"),
+ BackgroundColor: stringPtr(Background.Dark),
+ },
+ GenericEmph: ansi.StylePrimitive{
+ Color: stringPtr("#f1fa8c"),
+ Italic: boolPtr(true),
+ BackgroundColor: stringPtr(Background.Dark),
+ },
+ GenericInserted: ansi.StylePrimitive{
+ Color: stringPtr("#50fa7b"),
+ BackgroundColor: stringPtr(Background.Dark),
+ },
+ GenericStrong: ansi.StylePrimitive{
+ Color: stringPtr("#ffb86c"),
+ Bold: boolPtr(true),
+ BackgroundColor: stringPtr(Background.Dark),
+ },
+ GenericSubheading: ansi.StylePrimitive{
+ Color: stringPtr("#bd93f9"),
+ BackgroundColor: stringPtr(Background.Dark),
+ },
+ Background: ansi.StylePrimitive{
+ BackgroundColor: stringPtr(Background.Dark),
+ },
+ },
+ },
+ Table: ansi.StyleTable{
+ StyleBlock: ansi.StyleBlock{
+ StylePrimitive: ansi.StylePrimitive{
+ BackgroundColor: stringPtr(Background.Dark),
+ },
+ IndentToken: stringPtr(BaseStyle.Render(" ")),
+ },
+ },
+ DefinitionDescription: ansi.StylePrimitive{
+ BlockPrefix: "\n* ",
+ BackgroundColor: stringPtr(Background.Dark),
+ },
+}