diff --git a/internal/db/messages.sql.go b/internal/db/messages.sql.go index 4309db181c29b117c3c436edbfb55ee1da0fdc03..0555b4330d79089c0d5a7127c311f55af567e604 100644 --- a/internal/db/messages.sql.go +++ b/internal/db/messages.sql.go @@ -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 } diff --git a/internal/db/migrations/000001_initial.up.sql b/internal/db/migrations/000001_initial.up.sql index 2fbe5547e9996e6f97110294aa91533f3ca71a0d..03479449d24492c04cda61f56056d9c3d7fb73fa 100644 --- a/internal/db/migrations/000001_initial.up.sql +++ b/internal/db/migrations/000001_initial.up.sql @@ -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 ); diff --git a/internal/db/models.go b/internal/db/models.go index 1ad8607a9654c227bc5a17c73667d2d94d0e137d..2fad913be831d4d475642def6d94f2f7fadd960d 100644 --- a/internal/db/models.go +++ b/internal/db/models.go @@ -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 { diff --git a/internal/db/sql/messages.sql b/internal/db/sql/messages.sql index 64571158fe55199fe79c4d1753180ee6ffbe383b..a59cebe7d00fe5fd7cbd449df681df45e832979a 100644 --- a/internal/db/sql/messages.sql +++ b/internal/db/sql/messages.sql @@ -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 = ?; diff --git a/internal/message/content.go b/internal/message/content.go index 2604cd68ada39656c9ad089c27b8f33401bf538e..cd263798b35e8fc1df3278dca1fff288c4a2806c 100644 --- a/internal/message/content.go +++ b/internal/message/content.go @@ -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) { diff --git a/internal/message/message.go b/internal/message/message.go index 13cf54048fe45ea31b14c2e74537d7fd2a499b6a..eeeb83ed2e8cebc6f69bc569cf0f9f2784879307 100644 --- a/internal/message/message.go +++ b/internal/message/message.go @@ -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 diff --git a/internal/tui/components/chat/chat.go b/internal/tui/components/chat/chat.go new file mode 100644 index 0000000000000000000000000000000000000000..e893ec2f5f962643024bd6003d3e97351858a8ae --- /dev/null +++ b/internal/tui/components/chat/chat.go @@ -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 +} diff --git a/internal/tui/components/chat/editor.go b/internal/tui/components/chat/editor.go index ea20d7e4420054486511a31f5a660194d236a29a..df336818ce1b28fe76fed100c7eeba68bc4772f8 100644 --- a/internal/tui/components/chat/editor.go +++ b/internal/tui/components/chat/editor.go @@ -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, diff --git a/internal/tui/components/chat/messages.go b/internal/tui/components/chat/messages.go index 691954767aa71de5e5a4b3c2b92f30f2ea084866..0a7e6e2a499e4d14ad19a327ba8de4dbfb1e59cd 100644 --- a/internal/tui/components/chat/messages.go +++ b/internal/tui/components/chat/messages.go @@ -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, + } } diff --git a/internal/tui/components/chat/sidebar.go b/internal/tui/components/chat/sidebar.go index 4a563157738246b6dc15c657146c01563b5bfd54..65c06f4a168e04a8bfc1bdff37368034518ea1dc 100644 --- a/internal/tui/components/chat/sidebar.go +++ b/internal/tui/components/chat/sidebar.go @@ -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, + } } diff --git a/internal/tui/components/core/button.go b/internal/tui/components/core/button.go deleted file mode 100644 index 090fbc1ee510fb6912a3b47a76556f5c1bf0140a..0000000000000000000000000000000000000000 --- a/internal/tui/components/core/button.go +++ /dev/null @@ -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) -} - diff --git a/internal/tui/layout/split.go b/internal/tui/layout/split.go index 2a6822c7edbd2ee5c8ef2db9500270271673bd46..0ed85dd6fc20f8859b550c9f4ead13b5876b8eec 100644 --- a/internal/tui/layout/split.go +++ b/internal/tui/layout/split.go @@ -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 { diff --git a/internal/tui/page/chat.go b/internal/tui/page/chat.go index de5b3910fec4e0efbca11a059cfeea33a235c953..7ac0d2293f5b49bed1fee844a43319266c5c5263 100644 --- a/internal/tui/page/chat.go +++ b/internal/tui/page/chat.go @@ -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), + ), + } } diff --git a/internal/tui/styles/markdown.go b/internal/tui/styles/markdown.go index 77dc314f51343797210093b7d83d90d01fe408ea..b4e71c51ef6615b9638156ac30129b1905669585 100644 --- a/internal/tui/styles/markdown.go +++ b/internal/tui/styles/markdown.go @@ -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), + }, +}